feat(editor): Migrate pinData mixin to usePinnedData composable (no-changelog) (#8207)

## Summary
Required as part of NodeView refactoring:
- Migrates `pinData` mixin to `usePinnedData` composable.
- Adds `useActiveNode` and `useNodeType` composables 

## Related tickets and issues
https://linear.app/n8n/issue/N8N-6355/pindata

## Review / Merge checklist
- [x] PR title and summary are descriptive. **Remember, the title
automatically goes into the changelog. Use `(no-changelog)` otherwise.**
([conventions](https://github.com/n8n-io/n8n/blob/master/.github/pull_request_title_conventions.md))
- [x] [Docs updated](https://github.com/n8n-io/n8n-docs) or follow-up
ticket created.
- [x] Tests included.
> A bug is not considered fixed, unless a test is added to prevent it
from happening again.
   > A feature is not complete without tests.
This commit is contained in:
Alex Grozav
2024-01-04 11:22:56 +02:00
committed by GitHub
parent f4092a9e49
commit b50d8058cf
21 changed files with 678 additions and 92 deletions

View File

@@ -286,7 +286,7 @@ export const jsonFieldCompletions = defineComponent({
nodeName = quotedNodeName.replace(/^"/, '').replace(/"$/, ''); nodeName = quotedNodeName.replace(/^"/, '').replace(/"$/, '');
} }
const pinData: IPinData | undefined = this.workflowsStore.getPinData; const pinData: IPinData | undefined = this.workflowsStore.pinnedWorkflowData;
const nodePinData = pinData?.[nodeName]; const nodePinData = pinData?.[nodeName];

View File

@@ -1,6 +1,6 @@
<template> <template>
<RunData <RunData
:node-ui="currentNode" :node="currentNode"
:run-index="runIndex" :run-index="runIndex"
:linked-runs="linkedRuns" :linked-runs="linkedRuns"
:can-link-runs="!mappedNode && canLinkRuns" :can-link-runs="!mappedNode && canLinkRuns"

View File

@@ -159,7 +159,6 @@ import {
} from '@/constants'; } from '@/constants';
import { nodeBase } from '@/mixins/nodeBase'; import { nodeBase } from '@/mixins/nodeBase';
import { workflowHelpers } from '@/mixins/workflowHelpers'; import { workflowHelpers } from '@/mixins/workflowHelpers';
import { pinData } from '@/mixins/pinData';
import type { import type {
ConnectionTypes, ConnectionTypes,
IExecutionsSummary, IExecutionsSummary,
@@ -187,6 +186,7 @@ import { FontAwesomeIcon } from '@fortawesome/vue-fontawesome';
import { type ContextMenuTarget, useContextMenu } from '@/composables/useContextMenu'; import { type ContextMenuTarget, useContextMenu } from '@/composables/useContextMenu';
import { useNodeHelpers } from '@/composables/useNodeHelpers'; import { useNodeHelpers } from '@/composables/useNodeHelpers';
import { useExternalHooks } from '@/composables/useExternalHooks'; import { useExternalHooks } from '@/composables/useExternalHooks';
import { usePinnedData } from '@/composables/usePinnedData';
export default defineComponent({ export default defineComponent({
name: 'Node', name: 'Node',
@@ -195,7 +195,7 @@ export default defineComponent({
FontAwesomeIcon, FontAwesomeIcon,
NodeIcon, NodeIcon,
}, },
mixins: [nodeBase, workflowHelpers, pinData, debounceHelper], mixins: [nodeBase, workflowHelpers, debounceHelper],
props: { props: {
isProductionExecutionPreview: { isProductionExecutionPreview: {
type: Boolean, type: Boolean,
@@ -210,17 +210,20 @@ export default defineComponent({
default: false, default: false,
}, },
}, },
setup() { setup(props) {
const workflowsStore = useWorkflowsStore();
const contextMenu = useContextMenu(); const contextMenu = useContextMenu();
const externalHooks = useExternalHooks(); const externalHooks = useExternalHooks();
const nodeHelpers = useNodeHelpers(); const nodeHelpers = useNodeHelpers();
const node = workflowsStore.getNodeByName(props.name);
const pinnedData = usePinnedData(node);
return { contextMenu, externalHooks, nodeHelpers }; return { contextMenu, externalHooks, nodeHelpers, pinnedData };
}, },
computed: { computed: {
...mapStores(useNodeTypesStore, useNDVStore, useUIStore, useWorkflowsStore), ...mapStores(useNodeTypesStore, useNDVStore, useUIStore, useWorkflowsStore),
showPinnedDataInfo(): boolean { showPinnedDataInfo(): boolean {
return this.hasPinData && !this.isProductionExecutionPreview; return this.pinnedData.hasData.value && !this.isProductionExecutionPreview;
}, },
isDuplicatable(): boolean { isDuplicatable(): boolean {
if (!this.nodeType) return true; if (!this.nodeType) return true;
@@ -247,7 +250,7 @@ export default defineComponent({
['crashed', 'error', 'failed'].includes(this.nodeExecutionStatus) ['crashed', 'error', 'failed'].includes(this.nodeExecutionStatus)
) )
return true; return true;
if (this.hasPinData) return false; if (this.pinnedData.hasData.value) return false;
if (this.data?.issues !== undefined && Object.keys(this.data.issues).length) { if (this.data?.issues !== undefined && Object.keys(this.data.issues).length) {
return true; return true;
} }
@@ -531,7 +534,7 @@ export default defineComponent({
!!this.node && !!this.node &&
this.isTriggerNode && this.isTriggerNode &&
!this.isPollingTypeNode && !this.isPollingTypeNode &&
!this.hasPinData && !this.pinnedData.hasData.value &&
!this.isNodeDisabled && !this.isNodeDisabled &&
this.workflowRunning && this.workflowRunning &&
this.workflowDataItems === 0 && this.workflowDataItems === 0 &&

View File

@@ -12,7 +12,7 @@
@dragend="onDragEnd" @dragend="onDragEnd"
> >
<template #icon> <template #icon>
<div v-if="isSubNode" :class="$style.subNodeBackground"></div> <div v-if="isSubNodeType" :class="$style.subNodeBackground"></div>
<NodeIcon :class="$style.nodeIcon" :node-type="nodeType" /> <NodeIcon :class="$style.nodeIcon" :node-type="nodeType" />
</template> </template>
@@ -57,7 +57,7 @@ import NodeIcon from '@/components/NodeIcon.vue';
import { useActions } from '../composables/useActions'; import { useActions } from '../composables/useActions';
import { useI18n } from '@/composables/useI18n'; import { useI18n } from '@/composables/useI18n';
import { useTelemetry } from '@/composables/useTelemetry'; import { useTelemetry } from '@/composables/useTelemetry';
import { NodeHelpers, NodeConnectionType } from 'n8n-workflow'; import { useNodeType } from '@/composables/useNodeType';
export interface Props { export interface Props {
nodeType: SimplifiedNodeType; nodeType: SimplifiedNodeType;
@@ -74,6 +74,9 @@ const telemetry = useTelemetry();
const { actions } = useNodeCreatorStore(); const { actions } = useNodeCreatorStore();
const { getAddedNodesAndConnections } = useActions(); const { getAddedNodesAndConnections } = useActions();
const { isSubNodeType } = useNodeType({
nodeType: props.nodeType,
});
const dragging = ref(false); const dragging = ref(false);
const draggablePosition = ref({ x: -100, y: -100 }); const draggablePosition = ref({ x: -100, y: -100 });
@@ -124,16 +127,6 @@ const displayName = computed<string>(() => {
}); });
}); });
const isSubNode = computed<boolean>(() => {
if (!props.nodeType.outputs || typeof props.nodeType.outputs === 'string') {
return false;
}
const outputTypes = NodeHelpers.getConnectionTypes(props.nodeType.outputs);
return outputTypes
? outputTypes.filter((output) => output !== NodeConnectionType.Main).length > 0
: false;
});
const isTrigger = computed<boolean>(() => { const isTrigger = computed<boolean>(() => {
return props.nodeType.group.includes('trigger') && !hasActions.value; return props.nodeType.group.includes('trigger') && !hasActions.value;
}); });

View File

@@ -135,7 +135,7 @@
<script lang="ts"> <script lang="ts">
import { defineComponent } from 'vue'; import { defineComponent } from 'vue';
import { mapStores } from 'pinia'; import { mapStores, storeToRefs } from 'pinia';
import { createEventBus } from 'n8n-design-system/utils'; import { createEventBus } from 'n8n-design-system/utils';
import type { import type {
INodeConnections, INodeConnections,
@@ -163,7 +163,6 @@ import {
STICKY_NODE_TYPE, STICKY_NODE_TYPE,
} from '@/constants'; } from '@/constants';
import { workflowActivate } from '@/mixins/workflowActivate'; import { workflowActivate } from '@/mixins/workflowActivate';
import { pinData } from '@/mixins/pinData';
import { dataPinningEventBus } from '@/event-bus'; import { dataPinningEventBus } from '@/event-bus';
import { useWorkflowsStore } from '@/stores/workflows.store'; import { useWorkflowsStore } from '@/stores/workflows.store';
import { useNDVStore } from '@/stores/ndv.store'; import { useNDVStore } from '@/stores/ndv.store';
@@ -174,6 +173,7 @@ import { useDeviceSupport } from 'n8n-design-system/composables/useDeviceSupport
import { useNodeHelpers } from '@/composables/useNodeHelpers'; import { useNodeHelpers } from '@/composables/useNodeHelpers';
import { useMessage } from '@/composables/useMessage'; import { useMessage } from '@/composables/useMessage';
import { useExternalHooks } from '@/composables/useExternalHooks'; import { useExternalHooks } from '@/composables/useExternalHooks';
import { usePinnedData } from '@/composables/usePinnedData';
export default defineComponent({ export default defineComponent({
name: 'NodeDetailsView', name: 'NodeDetailsView',
@@ -184,7 +184,7 @@ export default defineComponent({
NDVDraggablePanels, NDVDraggablePanels,
TriggerPanel, TriggerPanel,
}, },
mixins: [workflowHelpers, workflowActivate, pinData], mixins: [workflowHelpers, workflowActivate],
props: { props: {
readOnly: { readOnly: {
type: Boolean, type: Boolean,
@@ -198,12 +198,16 @@ export default defineComponent({
}, },
}, },
setup(props, ctx) { setup(props, ctx) {
const ndvStore = useNDVStore();
const externalHooks = useExternalHooks(); const externalHooks = useExternalHooks();
const nodeHelpers = useNodeHelpers(); const nodeHelpers = useNodeHelpers();
const { activeNode } = storeToRefs(ndvStore);
const pinnedData = usePinnedData(activeNode);
return { return {
externalHooks, externalHooks,
nodeHelpers, nodeHelpers,
pinnedData,
...useDeviceSupport(), ...useDeviceSupport(),
...useMessage(), ...useMessage(),
// eslint-disable-next-line @typescript-eslint/no-misused-promises // eslint-disable-next-line @typescript-eslint/no-misused-promises
@@ -301,12 +305,10 @@ export default defineComponent({
return []; return [];
}, },
parentNode(): string | undefined { parentNode(): string | undefined {
const pinData = this.workflowsStore.getPinData;
// Return the first parent node that contains data // Return the first parent node that contains data
for (const parentNodeName of this.parentNodes) { for (const parentNodeName of this.parentNodes) {
// Check first for pinned data // Check first for pinned data
if (pinData[parentNodeName]) { if (this.workflowsStore.pinnedWorkflowData[parentNodeName]) {
return parentNodeName; return parentNodeName;
} }
@@ -689,7 +691,7 @@ export default defineComponent({
if (shouldPinDataBeforeClosing === MODAL_CONFIRM) { if (shouldPinDataBeforeClosing === MODAL_CONFIRM) {
const { value } = this.outputPanelEditMode; const { value } = this.outputPanelEditMode;
try { try {
this.setPinData(this.activeNode, jsonParse(value), 'on-ndv-close-modal'); this.pinnedData.setData(jsonParse(value), 'on-ndv-close-modal');
} catch (error) { } catch (error) {
console.error(error); console.error(error);
} }

View File

@@ -33,20 +33,21 @@ import {
import type { INodeUi } from '@/Interface'; import type { INodeUi } from '@/Interface';
import type { INodeTypeDescription } from 'n8n-workflow'; import type { INodeTypeDescription } from 'n8n-workflow';
import { workflowRun } from '@/mixins/workflowRun'; import { workflowRun } from '@/mixins/workflowRun';
import { pinData } from '@/mixins/pinData';
import { useWorkflowsStore } from '@/stores/workflows.store'; import { useWorkflowsStore } from '@/stores/workflows.store';
import { useNDVStore } from '@/stores/ndv.store'; import { useNDVStore } from '@/stores/ndv.store';
import { useNodeTypesStore } from '@/stores/nodeTypes.store'; import { useNodeTypesStore } from '@/stores/nodeTypes.store';
import { useMessage } from '@/composables/useMessage'; import { useMessage } from '@/composables/useMessage';
import { useToast } from '@/composables/useToast'; import { useToast } from '@/composables/useToast';
import { useExternalHooks } from '@/composables/useExternalHooks'; import { useExternalHooks } from '@/composables/useExternalHooks';
import { usePinnedData } from '@/composables/usePinnedData';
export default defineComponent({ export default defineComponent({
mixins: [workflowRun, pinData], mixins: [workflowRun],
inheritAttrs: false, inheritAttrs: false,
props: { props: {
nodeName: { nodeName: {
type: String, type: String,
required: true,
}, },
disabled: { disabled: {
type: Boolean, type: Boolean,
@@ -70,10 +71,14 @@ export default defineComponent({
}, },
}, },
setup(props, ctx) { setup(props, ctx) {
const workflowsStore = useWorkflowsStore();
const node = workflowsStore.getNodeByName(props.nodeName);
const pinnedData = usePinnedData(node);
const externalHooks = useExternalHooks(); const externalHooks = useExternalHooks();
return { return {
externalHooks, externalHooks,
pinnedData,
...useToast(), ...useToast(),
...useMessage(), ...useMessage(),
// eslint-disable-next-line @typescript-eslint/no-misused-promises // eslint-disable-next-line @typescript-eslint/no-misused-promises
@@ -221,7 +226,7 @@ export default defineComponent({
this.$emit('stopExecution'); this.$emit('stopExecution');
} else { } else {
let shouldUnpinAndExecute = false; let shouldUnpinAndExecute = false;
if (this.hasPinData) { if (this.pinnedData.hasData.value) {
const confirmResult = await this.confirm( const confirmResult = await this.confirm(
this.$locale.baseText('ndv.pinData.unpinAndExecute.description'), this.$locale.baseText('ndv.pinData.unpinAndExecute.description'),
this.$locale.baseText('ndv.pinData.unpinAndExecute.title'), this.$locale.baseText('ndv.pinData.unpinAndExecute.title'),
@@ -233,11 +238,11 @@ export default defineComponent({
shouldUnpinAndExecute = confirmResult === MODAL_CONFIRM; shouldUnpinAndExecute = confirmResult === MODAL_CONFIRM;
if (shouldUnpinAndExecute && this.node) { if (shouldUnpinAndExecute && this.node) {
this.unsetPinData(this.node, 'unpin-and-execute-modal'); this.pinnedData.unsetData('unpin-and-execute-modal');
} }
} }
if (!this.hasPinData || shouldUnpinAndExecute) { if (!this.pinnedData.hasData.value || shouldUnpinAndExecute) {
const telemetryPayload = { const telemetryPayload = {
node_type: this.nodeType ? this.nodeType.name : null, node_type: this.nodeType ? this.nodeType.name : null,
workflow_id: this.workflowsStore.workflowId, workflow_id: this.workflowsStore.workflowId,

View File

@@ -1,6 +1,6 @@
<template> <template>
<RunData <RunData
:node-ui="node" :node="node"
:run-index="runIndex" :run-index="runIndex"
:linked-runs="linkedRuns" :linked-runs="linkedRuns"
:can-link-runs="canLinkRuns" :can-link-runs="canLinkRuns"
@@ -36,11 +36,11 @@
{{ $locale.baseText(outputPanelEditMode.enabled ? 'ndv.output.edit' : 'ndv.output') }} {{ $locale.baseText(outputPanelEditMode.enabled ? 'ndv.output.edit' : 'ndv.output') }}
</span> </span>
<RunInfo <RunInfo
v-if="hasNodeRun && !hasPinData && runsCount === 1" v-if="hasNodeRun && !pinnedData.hasData.value && runsCount === 1"
v-show="!outputPanelEditMode.enabled" v-show="!outputPanelEditMode.enabled"
:task-data="runTaskData" :task-data="runTaskData"
:has-stale-data="staleData" :has-stale-data="staleData"
:has-pin-data="hasPinData" :has-pin-data="pinnedData.hasData.value"
/> />
</div> </div>
</template> </template>
@@ -50,7 +50,7 @@
$locale.baseText('ndv.output.waitingToRun') $locale.baseText('ndv.output.waitingToRun')
}}</n8n-text> }}</n8n-text>
<n8n-text v-if="!workflowRunning" data-test-id="ndv-output-run-node-hint"> <n8n-text v-if="!workflowRunning" data-test-id="ndv-output-run-node-hint">
<template v-if="isSubNode"> <template v-if="isSubNodeType.value">
{{ $locale.baseText('ndv.output.runNodeHintSubNode') }} {{ $locale.baseText('ndv.output.runNodeHintSubNode') }}
</template> </template>
<template v-else> <template v-else>
@@ -93,7 +93,7 @@
</div> </div>
</template> </template>
<template v-if="!hasPinData && runsCount > 1" #run-info> <template v-if="!pinnedData.hasData.value && runsCount > 1" #run-info>
<RunInfo :task-data="runTaskData" /> <RunInfo :task-data="runTaskData" />
</template> </template>
</RunData> </RunData>
@@ -105,14 +105,15 @@ import type { IExecutionResponse, INodeUi } from '@/Interface';
import type { INodeTypeDescription, IRunData, IRunExecutionData, ITaskData } from 'n8n-workflow'; import type { INodeTypeDescription, IRunData, IRunExecutionData, ITaskData } from 'n8n-workflow';
import RunData from './RunData.vue'; import RunData from './RunData.vue';
import RunInfo from './RunInfo.vue'; import RunInfo from './RunInfo.vue';
import { pinData } from '@/mixins/pinData'; import { mapStores, storeToRefs } from 'pinia';
import { mapStores } from 'pinia';
import { useUIStore } from '@/stores/ui.store'; import { useUIStore } from '@/stores/ui.store';
import { useWorkflowsStore } from '@/stores/workflows.store'; import { useWorkflowsStore } from '@/stores/workflows.store';
import { useNDVStore } from '@/stores/ndv.store'; import { useNDVStore } from '@/stores/ndv.store';
import { useNodeTypesStore } from '@/stores/nodeTypes.store'; import { useNodeTypesStore } from '@/stores/nodeTypes.store';
import RunDataAi from './RunDataAi/RunDataAi.vue'; import RunDataAi from './RunDataAi/RunDataAi.vue';
import { ndvEventBus } from '@/event-bus'; import { ndvEventBus } from '@/event-bus';
import { useNodeType } from '@/composables/useNodeType';
import { usePinnedData } from '@/composables/usePinnedData';
type RunDataRef = InstanceType<typeof RunData>; type RunDataRef = InstanceType<typeof RunData>;
@@ -124,7 +125,6 @@ const OUTPUT_TYPE = {
export default defineComponent({ export default defineComponent({
name: 'OutputPanel', name: 'OutputPanel',
components: { RunData, RunInfo, RunDataAi }, components: { RunData, RunInfo, RunDataAi },
mixins: [pinData],
props: { props: {
runIndex: { runIndex: {
type: Number, type: Number,
@@ -155,6 +155,22 @@ export default defineComponent({
default: false, default: false,
}, },
}, },
setup(props) {
const ndvStore = useNDVStore();
const { activeNode } = storeToRefs(ndvStore);
const { isSubNodeType } = useNodeType({
node: activeNode,
});
const pinnedData = usePinnedData(activeNode, {
runIndex: props.runIndex,
displayMode: ndvStore.getPanelDisplayMode('output'),
});
return {
pinnedData,
isSubNodeType,
};
},
data() { data() {
return { return {
outputMode: 'regular', outputMode: 'regular',
@@ -271,7 +287,7 @@ export default defineComponent({
return this.ndvStore.outputPanelEditMode; return this.ndvStore.outputPanelEditMode;
}, },
canPinData(): boolean { canPinData(): boolean {
return this.isPinDataNodeType && !this.isReadOnly; return this.pinnedData.isValidNodeType.value && !this.isReadOnly;
}, },
}, },
methods: { methods: {

View File

@@ -1,7 +1,9 @@
<template> <template>
<div :class="['run-data', $style.container]" @mouseover="activatePane"> <div :class="['run-data', $style.container]" @mouseover="activatePane">
<n8n-callout <n8n-callout
v-if="canPinData && hasPinData && !editMode.enabled && !isProductionExecutionPreview" v-if="
canPinData && pinnedData.hasData.value && !editMode.enabled && !isProductionExecutionPreview
"
theme="secondary" theme="secondary"
icon="thumbtack" icon="thumbtack"
:class="$style.pinnedDataCallout" :class="$style.pinnedDataCallout"
@@ -98,11 +100,11 @@
<n8n-icon-button <n8n-icon-button
:class="['ml-2xs', $style.pinDataButton]" :class="['ml-2xs', $style.pinDataButton]"
type="tertiary" type="tertiary"
:active="hasPinData" :active="pinnedData.hasData.value"
icon="thumbtack" icon="thumbtack"
:disabled=" :disabled="
editMode.enabled || editMode.enabled ||
(rawInputData.length === 0 && !hasPinData) || (rawInputData.length === 0 && !pinnedData.hasData.value) ||
isReadOnlyRoute || isReadOnlyRoute ||
readOnlyEnv readOnlyEnv
" "
@@ -562,7 +564,7 @@
</template> </template>
<script lang="ts"> <script lang="ts">
import { defineAsyncComponent, defineComponent } from 'vue'; import { defineAsyncComponent, defineComponent, toRef } from 'vue';
import type { PropType } from 'vue'; import type { PropType } from 'vue';
import { mapStores } from 'pinia'; import { mapStores } from 'pinia';
import { useStorage } from '@/composables/useStorage'; import { useStorage } from '@/composables/useStorage';
@@ -606,8 +608,8 @@ import NodeErrorView from '@/components/Error/NodeErrorView.vue';
import JsonEditor from '@/components/JsonEditor/JsonEditor.vue'; import JsonEditor from '@/components/JsonEditor/JsonEditor.vue';
import { genericHelpers } from '@/mixins/genericHelpers'; import { genericHelpers } from '@/mixins/genericHelpers';
import { pinData } from '@/mixins/pinData'; import type { PinDataSource } from '@/composables/usePinnedData';
import type { PinDataSource } from '@/mixins/pinData'; import { usePinnedData } from '@/composables/usePinnedData';
import { dataPinningEventBus } from '@/event-bus'; import { dataPinningEventBus } from '@/event-bus';
import { clearJsonKey, isEmpty } from '@/utils/typesUtils'; import { clearJsonKey, isEmpty } from '@/utils/typesUtils';
import { executionDataToJson } from '@/utils/nodeTypesUtils'; import { executionDataToJson } from '@/utils/nodeTypesUtils';
@@ -642,10 +644,11 @@ export default defineComponent({
RunDataHtml, RunDataHtml,
RunDataSearch, RunDataSearch,
}, },
mixins: [genericHelpers, pinData], mixins: [genericHelpers],
props: { props: {
nodeUi: { node: {
type: Object as PropType<INodeUi>, type: Object as PropType<INodeUi>,
default: null,
}, },
runIndex: { runIndex: {
type: Number, type: Number,
@@ -674,6 +677,7 @@ export default defineComponent({
}, },
paneType: { paneType: {
type: String as PropType<NodePanelType>, type: String as PropType<NodePanelType>,
required: true,
}, },
overrideOutputs: { overrideOutputs: {
type: Array as PropType<number[]>, type: Array as PropType<number[]>,
@@ -697,14 +701,21 @@ export default defineComponent({
default: false, default: false,
}, },
}, },
setup() { setup(props) {
const ndvStore = useNDVStore();
const nodeHelpers = useNodeHelpers(); const nodeHelpers = useNodeHelpers();
const externalHooks = useExternalHooks(); const externalHooks = useExternalHooks();
const node = toRef(props, 'node');
const pinnedData = usePinnedData(node, {
runIndex: props.runIndex,
displayMode: ndvStore.getPanelDisplayMode(props.paneType),
});
return { return {
...useToast(), ...useToast(),
externalHooks, externalHooks,
nodeHelpers, nodeHelpers,
pinnedData,
}; };
}, },
data() { data() {
@@ -761,9 +772,6 @@ export default defineComponent({
displayMode(): IRunDataDisplayMode { displayMode(): IRunDataDisplayMode {
return this.ndvStore.getPanelDisplayMode(this.paneType); return this.ndvStore.getPanelDisplayMode(this.paneType);
}, },
node(): INodeUi | null {
return (this.nodeUi as INodeUi | null) || null;
},
nodeType(): INodeTypeDescription | null { nodeType(): INodeTypeDescription | null {
if (this.node) { if (this.node) {
return this.nodeTypesStore.getNodeType(this.node.type, this.node.typeVersion); return this.nodeTypesStore.getNodeType(this.node.type, this.node.typeVersion);
@@ -796,7 +804,7 @@ export default defineComponent({
return ( return (
!nonMainInputs && !nonMainInputs &&
!this.isPaneTypeInput && !this.isPaneTypeInput &&
this.isPinDataNodeType && this.pinnedData.isValidNodeType.value &&
!(this.binaryData && this.binaryData.length > 0) !(this.binaryData && this.binaryData.length > 0)
); );
}, },
@@ -832,7 +840,7 @@ export default defineComponent({
!this.isExecuting && !this.isExecuting &&
this.node && this.node &&
((this.workflowRunData && this.workflowRunData.hasOwnProperty(this.node.name)) || ((this.workflowRunData && this.workflowRunData.hasOwnProperty(this.node.name)) ||
this.hasPinData), this.pinnedData.hasData.value),
); );
}, },
isArtificialRecoveredEventItem(): boolean { isArtificialRecoveredEventItem(): boolean {
@@ -864,7 +872,9 @@ export default defineComponent({
return this.getDataCount(this.runIndex, this.currentOutputIndex); return this.getDataCount(this.runIndex, this.currentOutputIndex);
}, },
unfilteredDataCount(): number { unfilteredDataCount(): number {
return this.pinData ? this.pinData.length : this.rawInputData.length; return this.pinnedData.data.value
? this.pinnedData.data.value.length
: this.rawInputData.length;
}, },
dataSizeInMB(): string { dataSizeInMB(): string {
return (this.dataSize / 1024 / 1000).toLocaleString(); return (this.dataSize / 1024 / 1000).toLocaleString();
@@ -1072,8 +1082,8 @@ export default defineComponent({
useStorage(LOCAL_STORAGE_PIN_DATA_DISCOVERY_CANVAS_FLAG).value = 'true'; useStorage(LOCAL_STORAGE_PIN_DATA_DISCOVERY_CANVAS_FLAG).value = 'true';
}, },
enterEditMode({ origin }: EnterEditModeArgs) { enterEditMode({ origin }: EnterEditModeArgs) {
const inputData = this.pinData const inputData = this.pinnedData.data.value
? clearJsonKey(this.pinData) ? clearJsonKey(this.pinnedData.data.value)
: executionDataToJson(this.rawInputData); : executionDataToJson(this.rawInputData);
const data = inputData.length > 0 ? inputData : TEST_PIN_DATA; const data = inputData.length > 0 ? inputData : TEST_PIN_DATA;
@@ -1086,9 +1096,9 @@ export default defineComponent({
click_type: origin === 'editIconButton' ? 'button' : 'link', click_type: origin === 'editIconButton' ? 'button' : 'link',
session_id: this.sessionId, session_id: this.sessionId,
run_index: this.runIndex, run_index: this.runIndex,
is_output_present: this.hasNodeRun || this.hasPinData, is_output_present: this.hasNodeRun || this.pinnedData.hasData.value,
view: !this.hasNodeRun && !this.hasPinData ? 'undefined' : this.displayMode, view: !this.hasNodeRun && !this.pinnedData.hasData.value ? 'undefined' : this.displayMode,
is_data_pinned: this.hasPinData, is_data_pinned: this.pinnedData.hasData.value,
}); });
}, },
onClickCancelEdit() { onClickCancelEdit() {
@@ -1106,7 +1116,7 @@ export default defineComponent({
this.clearAllStickyNotifications(); this.clearAllStickyNotifications();
try { try {
this.setPinData(this.node, clearJsonKey(value) as INodeExecutionData[], 'save-edit'); this.pinnedData.setData(clearJsonKey(value) as INodeExecutionData[], 'save-edit');
} catch (error) { } catch (error) {
console.error(error); console.error(error);
return; return;
@@ -1135,7 +1145,7 @@ export default defineComponent({
node_type: this.activeNode.type, node_type: this.activeNode.type,
session_id: this.sessionId, session_id: this.sessionId,
run_index: this.runIndex, run_index: this.runIndex,
view: !this.hasNodeRun && !this.hasPinData ? 'none' : this.displayMode, view: !this.hasNodeRun && !this.pinnedData.hasData.value ? 'none' : this.displayMode,
}; };
void this.externalHooks.run('runData.onTogglePinData', telemetryPayload); void this.externalHooks.run('runData.onTogglePinData', telemetryPayload);
@@ -1144,13 +1154,13 @@ export default defineComponent({
this.nodeHelpers.updateNodeParameterIssues(this.node); this.nodeHelpers.updateNodeParameterIssues(this.node);
if (this.hasPinData) { if (this.pinnedData.hasData.value) {
this.unsetPinData(this.node, source); this.pinnedData.unsetData(source);
return; return;
} }
try { try {
this.setPinData(this.node, this.rawInputData, 'pin-icon-click'); this.pinnedData.setData(this.rawInputData, 'pin-icon-click');
} catch (error) { } catch (error) {
console.error(error); console.error(error);
return; return;
@@ -1299,14 +1309,14 @@ export default defineComponent({
return inputData; return inputData;
}, },
getPinDataOrLiveData(inputData: INodeExecutionData[]): INodeExecutionData[] { getPinDataOrLiveData(inputData: INodeExecutionData[]): INodeExecutionData[] {
if (this.pinData && !this.isProductionExecutionPreview) { if (this.pinnedData.data.value && !this.isProductionExecutionPreview) {
return Array.isArray(this.pinData) return Array.isArray(this.pinnedData.data.value)
? this.pinData.map((value) => ({ ? this.pinnedData.data.value.map((value) => ({
json: value, json: value,
})) }))
: [ : [
{ {
json: this.pinData, json: this.pinnedData.data.value,
}, },
]; ];
} }

View File

@@ -37,11 +37,10 @@
<script lang="ts"> <script lang="ts">
import { defineComponent } from 'vue'; import { defineComponent } from 'vue';
import type { PropType } from 'vue'; import type { PropType } from 'vue';
import { mapStores } from 'pinia'; import { mapStores, storeToRefs } from 'pinia';
import jp from 'jsonpath'; import jp from 'jsonpath';
import type { INodeUi } from '@/Interface'; import type { INodeUi } from '@/Interface';
import type { IDataObject } from 'n8n-workflow'; import type { IDataObject } from 'n8n-workflow';
import { pinData } from '@/mixins/pinData';
import { genericHelpers } from '@/mixins/genericHelpers'; import { genericHelpers } from '@/mixins/genericHelpers';
import { clearJsonKey, convertPath } from '@/utils/typesUtils'; import { clearJsonKey, convertPath } from '@/utils/typesUtils';
import { executionDataToJson } from '@/utils/nodeTypesUtils'; import { executionDataToJson } from '@/utils/nodeTypesUtils';
@@ -52,6 +51,7 @@ import { useToast } from '@/composables/useToast';
import { useI18n } from '@/composables/useI18n'; import { useI18n } from '@/composables/useI18n';
import { nonExistingJsonPath } from '@/constants'; import { nonExistingJsonPath } from '@/constants';
import { useClipboard } from '@/composables/useClipboard'; import { useClipboard } from '@/composables/useClipboard';
import { usePinnedData } from '@/composables/usePinnedData';
type JsonPathData = { type JsonPathData = {
path: string; path: string;
@@ -60,7 +60,7 @@ type JsonPathData = {
export default defineComponent({ export default defineComponent({
name: 'RunDataJsonActions', name: 'RunDataJsonActions',
mixins: [genericHelpers, pinData], mixins: [genericHelpers],
props: { props: {
node: { node: {
type: Object as PropType<INodeUi>, type: Object as PropType<INodeUi>,
@@ -93,14 +93,18 @@ export default defineComponent({
}, },
}, },
setup() { setup() {
const ndvStore = useNDVStore();
const i18n = useI18n(); const i18n = useI18n();
const nodeHelpers = useNodeHelpers(); const nodeHelpers = useNodeHelpers();
const clipboard = useClipboard(); const clipboard = useClipboard();
const { activeNode } = storeToRefs(ndvStore);
const pinnedData = usePinnedData(activeNode);
return { return {
i18n, i18n,
nodeHelpers, nodeHelpers,
clipboard, clipboard,
pinnedData,
...useToast(), ...useToast(),
}; };
}, },
@@ -123,8 +127,8 @@ export default defineComponent({
const inExecutionsFrame = const inExecutionsFrame =
window !== window.parent && window.parent.location.pathname.includes('/executions'); window !== window.parent && window.parent.location.pathname.includes('/executions');
if (this.hasPinData && !inExecutionsFrame) { if (this.pinnedData.hasData.value && !inExecutionsFrame) {
selectedValue = clearJsonKey(this.pinData as object); selectedValue = clearJsonKey(this.pinnedData.data.value as object);
} else { } else {
selectedValue = executionDataToJson( selectedValue = executionDataToJson(
this.nodeHelpers.getNodeInputData(this.node, this.runIndex, this.currentOutputIndex), this.nodeHelpers.getNodeInputData(this.node, this.runIndex, this.currentOutputIndex),

View File

@@ -84,7 +84,7 @@ describe('RunData', () => {
const render = (outputData: unknown[], displayMode: IRunDataDisplayMode) => const render = (outputData: unknown[], displayMode: IRunDataDisplayMode) =>
createComponentRenderer(RunData, { createComponentRenderer(RunData, {
props: { props: {
nodeUi: { node: {
name: 'Test Node', name: 'Test Node',
}, },
}, },
@@ -103,7 +103,7 @@ describe('RunData', () => {
}, },
})({ })({
props: { props: {
nodeUi: { node: {
id: '1', id: '1',
name: 'Test Node', name: 'Test Node',
position: [0, 0], position: [0, 0],

View File

@@ -0,0 +1,43 @@
import { describe, it, expect, vi } from 'vitest';
import { useActiveNode } from '@/composables/useActiveNode';
import { useNodeType } from '@/composables/useNodeType';
import { createTestNode } from '@/__tests__/mocks';
import { MANUAL_TRIGGER_NODE_TYPE } from '@/constants';
import { computed } from 'vue';
import { defaultMockNodeTypes } from '@/__tests__/defaults';
const node = computed(() => createTestNode({ name: 'Node', type: MANUAL_TRIGGER_NODE_TYPE }));
const nodeType = computed(() => defaultMockNodeTypes[MANUAL_TRIGGER_NODE_TYPE]);
vi.mock('@/stores/ndv.store', () => ({
useNDVStore: vi.fn(() => ({
activeNode: node,
})),
}));
vi.mock('@/composables/useNodeType', () => ({
useNodeType: vi.fn(() => ({
nodeType,
})),
}));
vi.mock('pinia', () => ({
storeToRefs: vi.fn((store) => store),
}));
describe('useActiveNode()', () => {
it('should call useNodeType()', () => {
useActiveNode();
expect(useNodeType).toHaveBeenCalledWith({
node,
});
});
it('should return activeNode and activeNodeType', () => {
const { activeNode, activeNodeType } = useActiveNode();
expect(activeNode).toBe(node);
expect(activeNodeType).toBe(nodeType);
});
});

View File

@@ -0,0 +1,51 @@
import { describe, it, expect, vi } from 'vitest';
import { useNodeType } from '@/composables/useNodeType';
import type { INodeUi, SimplifiedNodeType } from '@/Interface'; // Adjust the path accordingly
vi.mock('@/stores/nodeTypes.store', () => ({
useNodeTypesStore: vi.fn(() => ({
getNodeType: vi.fn().mockImplementation((type, version) => ({ type, version })),
})),
}));
describe('useNodeType()', () => {
describe('nodeType', () => {
it('returns correct nodeType from nodeType option', () => {
const nodeTypeOption = { name: 'testNodeType' } as SimplifiedNodeType;
const { nodeType } = useNodeType({ nodeType: nodeTypeOption });
expect(nodeType.value).toEqual(nodeTypeOption);
});
it('returns correct nodeType from node option', () => {
const nodeOption = { type: 'testType', typeVersion: 1 } as INodeUi;
const { nodeType } = useNodeType({ node: nodeOption });
expect(nodeType.value).toEqual({ type: 'testType', version: 1 });
});
});
describe('isSubNodeType', () => {
it('identifies sub node type correctly', () => {
const nodeTypeOption = {
name: 'testNodeType',
outputs: ['Main', 'Other'],
} as unknown as SimplifiedNodeType;
const { isSubNodeType } = useNodeType({ nodeType: nodeTypeOption });
expect(isSubNodeType.value).toBe(true);
});
});
describe('isMultipleOutputsNodeType', () => {
it('identifies multiple outputs node type correctly', () => {
const nodeTypeOption = {
name: 'testNodeType',
outputs: ['Main', 'Other'],
} as unknown as SimplifiedNodeType;
const { isMultipleOutputsNodeType } = useNodeType({ nodeType: nodeTypeOption });
expect(isMultipleOutputsNodeType.value).toBe(true);
});
});
});

View File

@@ -0,0 +1,136 @@
import { describe, it, expect, vi, beforeEach } from 'vitest';
import { setActivePinia, createPinia } from 'pinia';
import { ref } from 'vue';
import { usePinnedData } from '@/composables/usePinnedData';
import type { INodeUi } from '@/Interface';
import { MAX_PINNED_DATA_SIZE } from '@/constants';
import { useWorkflowsStore } from '@/stores/workflows.store';
import { useTelemetry } from '@/composables/useTelemetry';
vi.mock('@/composables/useToast', () => ({ useToast: vi.fn(() => ({ showError: vi.fn() })) }));
vi.mock('@/composables/useI18n', () => ({
useI18n: vi.fn(() => ({ baseText: vi.fn((key) => key) })),
}));
vi.mock('@/composables/useExternalHooks', () => ({
useExternalHooks: vi.fn(() => ({
run: vi.fn(),
})),
}));
describe('usePinnedData', () => {
beforeEach(() => {
setActivePinia(createPinia());
});
describe('isValidJSON()', () => {
it('should return true for valid JSON', () => {
const { isValidJSON } = usePinnedData(ref(null));
expect(isValidJSON('{"key":"value"}')).toBe(true);
});
it('should return false for invalid JSON', () => {
const { isValidJSON } = usePinnedData(ref(null));
const result = isValidJSON('invalid json');
expect(result).toBe(false);
});
});
describe('isValidSize()', () => {
it('should return true when data size is at upper limit', () => {
const { isValidSize } = usePinnedData(ref({ name: 'testNode' } as INodeUi));
const largeData = new Array(MAX_PINNED_DATA_SIZE + 1).join('a');
expect(isValidSize(largeData)).toBe(true);
});
it('should return false when data size is too large', () => {
const { isValidSize } = usePinnedData(ref({ name: 'testNode' } as INodeUi));
const largeData = new Array(MAX_PINNED_DATA_SIZE + 2).join('a');
expect(isValidSize(largeData)).toBe(false);
});
});
describe('setData()', () => {
it('should throw if data is not valid JSON', () => {
const { setData } = usePinnedData(ref({ name: 'testNode' } as INodeUi));
expect(() => setData('invalid json', 'pin-icon-click')).toThrow();
});
it('should throw if data size is too large', () => {
const { setData } = usePinnedData(ref({ name: 'testNode' } as INodeUi));
const largeData = new Array(MAX_PINNED_DATA_SIZE + 2).join('a');
expect(() => setData(largeData, 'pin-icon-click')).toThrow();
});
it('should set data correctly for valid inputs', () => {
const workflowsStore = useWorkflowsStore();
const node = ref({ name: 'testNode' } as INodeUi);
const { setData } = usePinnedData(node);
const testData = [{ json: { key: 'value' } }];
expect(() => setData(testData, 'pin-icon-click')).not.toThrow();
expect(workflowsStore.workflow.pinData?.[node.value.name]).toEqual(testData);
});
});
describe('unsetData()', () => {
it('should unset data correctly', () => {
const workflowsStore = useWorkflowsStore();
const node = ref({ name: 'testNode' } as INodeUi);
const { setData, unsetData } = usePinnedData(node);
const testData = [{ json: { key: 'value' } }];
setData(testData, 'pin-icon-click');
unsetData('context-menu');
expect(workflowsStore.workflow.pinData?.[node.value.name]).toBeUndefined();
});
});
describe('onSetDataSuccess()', () => {
it('should trigger telemetry on successful data setting', async () => {
const telemetry = useTelemetry();
const spy = vi.spyOn(telemetry, 'track');
const pinnedData = usePinnedData(ref({ name: 'testNode', type: 'someType' } as INodeUi), {
displayMode: ref('json'),
runIndex: ref(0),
});
pinnedData.onSetDataSuccess({ source: 'pin-icon-click' });
expect(spy).toHaveBeenCalled();
});
});
describe('onSetDataError()', () => {
it('should trigger telemetry tracking on error in data setting', () => {
const telemetry = useTelemetry();
const spy = vi.spyOn(telemetry, 'track');
const pinnedData = usePinnedData(ref({ name: 'testNode', type: 'someType' } as INodeUi), {
displayMode: ref('json'),
runIndex: ref(0),
});
pinnedData.onSetDataError({ errorType: 'data-too-large', source: 'pin-icon-click' });
expect(spy).toHaveBeenCalled();
});
});
describe('onUnsetData()', () => {
it('should trigger telemetry on successful data unsetting', async () => {
const telemetry = useTelemetry();
const spy = vi.spyOn(telemetry, 'track');
const pinnedData = usePinnedData(ref({ name: 'testNode', type: 'someType' } as INodeUi), {
displayMode: ref('json'),
runIndex: ref(0),
});
pinnedData.onUnsetData({ source: 'context-menu' });
expect(spy).toHaveBeenCalled();
});
});
});

View File

@@ -0,0 +1,17 @@
import { storeToRefs } from 'pinia';
import { useNDVStore } from '@/stores/ndv.store';
import { useNodeType } from '@/composables/useNodeType';
export function useActiveNode() {
const ndvStore = useNDVStore();
const { activeNode } = storeToRefs(ndvStore);
const { nodeType: activeNodeType } = useNodeType({
node: activeNode,
});
return {
activeNode,
activeNodeType,
};
}

View File

@@ -113,7 +113,7 @@ export function useNodeHelpers() {
workflow: Workflow, workflow: Workflow,
ignoreIssues?: string[], ignoreIssues?: string[],
): INodeIssues | null { ): INodeIssues | null {
const pinDataNodeNames = Object.keys(workflowsStore.getPinData ?? {}); const pinDataNodeNames = Object.keys(workflowsStore.pinnedWorkflowData ?? {});
let nodeIssues: INodeIssues | null = null; let nodeIssues: INodeIssues | null = null;
ignoreIssues = ignoreIssues ?? []; ignoreIssues = ignoreIssues ?? [];

View File

@@ -0,0 +1,46 @@
import type { MaybeRef } from 'vue';
import { computed, unref } from 'vue';
import type { INodeTypeDescription } from 'n8n-workflow';
import type { INodeUi, SimplifiedNodeType } from '@/Interface';
import { useNodeTypesStore } from '@/stores/nodeTypes.store';
import { NodeConnectionType, NodeHelpers } from 'n8n-workflow';
export function useNodeType(
options: {
node?: MaybeRef<INodeUi | null>;
nodeType?: MaybeRef<INodeTypeDescription | SimplifiedNodeType | null>;
} = {},
) {
const nodeTypesStore = useNodeTypesStore();
const nodeType = computed(() => {
if (options.nodeType) {
return unref(options.nodeType);
}
const activeNode = unref(options.node);
if (activeNode) {
return nodeTypesStore.getNodeType(activeNode.type, activeNode.typeVersion);
}
return null;
});
const isSubNodeType = computed(() => {
if (!nodeType.value?.outputs || typeof nodeType.value?.outputs === 'string') {
return false;
}
const outputTypes = NodeHelpers.getConnectionTypes(nodeType.value?.outputs);
return outputTypes
? outputTypes.filter((output) => output !== NodeConnectionType.Main).length > 0
: false;
});
const isMultipleOutputsNodeType = computed(() => (nodeType.value?.outputs ?? []).length > 1);
return {
nodeType,
isSubNodeType,
isMultipleOutputsNodeType,
};
}

View File

@@ -0,0 +1,253 @@
import { useToast } from '@/composables/useToast';
import { useI18n } from '@/composables/useI18n';
import type { INodeExecutionData, IPinData } from 'n8n-workflow';
import { jsonParse, jsonStringify } from 'n8n-workflow';
import {
MAX_EXPECTED_REQUEST_SIZE,
MAX_PINNED_DATA_SIZE,
MAX_WORKFLOW_SIZE,
PIN_DATA_NODE_TYPES_DENYLIST,
} from '@/constants';
import { stringSizeInBytes } from '@/utils/typesUtils';
import { useWorkflowsStore } from '@/stores/workflows.store';
import type { INodeUi, IRunDataDisplayMode } from '@/Interface';
import { useExternalHooks } from '@/composables/useExternalHooks';
import { useTelemetry } from '@/composables/useTelemetry';
import type { MaybeRef } from 'vue';
import { computed, unref } from 'vue';
import { useRootStore } from '@/stores/n8nRoot.store';
import { storeToRefs } from 'pinia';
import { useNodeType } from '@/composables/useNodeType';
export type PinDataSource =
| 'pin-icon-click'
| 'save-edit'
| 'on-ndv-close-modal'
| 'duplicate-node'
| 'add-nodes'
| 'context-menu'
| 'keyboard-shortcut';
export type UnpinDataSource = 'unpin-and-execute-modal' | 'context-menu' | 'keyboard-shortcut';
export function usePinnedData(
node: MaybeRef<INodeUi | null>,
options: {
displayMode?: MaybeRef<IRunDataDisplayMode>;
runIndex?: MaybeRef<number>;
} = {},
) {
const rootStore = useRootStore();
const workflowsStore = useWorkflowsStore();
const toast = useToast();
const i18n = useI18n();
const telemetry = useTelemetry();
const externalHooks = useExternalHooks();
const { sessionId } = storeToRefs(rootStore);
const { isSubNodeType, isMultipleOutputsNodeType } = useNodeType({
node,
});
const data = computed<IPinData[string] | undefined>(() => {
const targetNode = unref(node);
return targetNode ? workflowsStore.pinDataByNodeName(targetNode.name) : undefined;
});
const hasData = computed<boolean>(() => {
const targetNode = unref(node);
return !!targetNode && typeof data.value !== 'undefined';
});
const isValidNodeType = computed(() => {
const targetNode = unref(node);
return (
!!targetNode &&
!isSubNodeType.value &&
!isMultipleOutputsNodeType.value &&
!PIN_DATA_NODE_TYPES_DENYLIST.includes(targetNode.type)
);
});
function isValidJSON(data: string): boolean {
try {
JSON.parse(data);
return true;
} catch (error) {
const title = i18n.baseText('runData.editOutputInvalid');
const toRemove = new RegExp(/JSON\.parse:|of the JSON data/, 'g');
const message = error.message.replace(toRemove, '').trim();
const positionMatchRegEx = /at position (\d+)/;
const positionMatch = error.message.match(positionMatchRegEx);
error.message = message.charAt(0).toUpperCase() + message.slice(1);
error.message = error.message.replace(
"Unexpected token ' in JSON",
i18n.baseText('runData.editOutputInvalid.singleQuote'),
);
if (positionMatch) {
const position = parseInt(positionMatch[1], 10);
const lineBreaksUpToPosition = (data.slice(0, position).match(/\n/g) || []).length;
error.message = error.message.replace(
positionMatchRegEx,
i18n.baseText('runData.editOutputInvalid.atPosition', {
interpolate: {
position: `${position}`,
},
}),
);
error.message = `${i18n.baseText('runData.editOutputInvalid.onLine', {
interpolate: {
line: `${lineBreaksUpToPosition + 1}`,
},
})} ${error.message}`;
}
toast.showError(error, title);
return false;
}
}
function isValidSize(data: string | object): boolean {
const targetNode = unref(node);
if (!targetNode) {
return false;
}
if (typeof data === 'object') data = JSON.stringify(data);
const { pinData: currentPinData, ...workflow } = workflowsStore.getCurrentWorkflow();
const workflowJson = jsonStringify(workflow, { replaceCircularRefs: true });
const newPinData = { ...currentPinData, [targetNode.name]: data };
const newPinDataSize = workflowsStore.getPinDataSize(newPinData);
if (newPinDataSize > MAX_PINNED_DATA_SIZE) {
toast.showError(
new Error(i18n.baseText('ndv.pinData.error.tooLarge.description')),
i18n.baseText('ndv.pinData.error.tooLarge.title'),
);
return false;
}
if (
stringSizeInBytes(workflowJson) + newPinDataSize >
MAX_WORKFLOW_SIZE - MAX_EXPECTED_REQUEST_SIZE
) {
toast.showError(
new Error(i18n.baseText('ndv.pinData.error.tooLargeWorkflow.description')),
i18n.baseText('ndv.pinData.error.tooLargeWorkflow.title'),
);
return false;
}
return true;
}
function onSetDataSuccess({ source }: { source: PinDataSource }) {
const targetNode = unref(node);
const displayMode = unref(options.displayMode);
const runIndex = unref(options.runIndex);
const telemetryPayload = {
pinning_source: source,
node_type: targetNode?.type,
session_id: sessionId.value,
data_size: stringSizeInBytes(data.value),
view: displayMode,
run_index: runIndex,
};
void externalHooks.run('runData.onDataPinningSuccess', telemetryPayload);
telemetry.track('Ndv data pinning success', telemetryPayload);
}
function onSetDataError({
errorType,
source,
}: {
errorType: 'data-too-large' | 'invalid-json';
source: PinDataSource;
}) {
const targetNode = unref(node);
const displayMode = unref(options.displayMode);
const runIndex = unref(options.runIndex);
telemetry.track('Ndv data pinning failure', {
pinning_source: source,
node_type: targetNode?.type,
session_id: sessionId.value,
data_size: stringSizeInBytes(data.value),
view: displayMode,
run_index: runIndex,
error_type: errorType,
});
}
function setData(data: string | INodeExecutionData[], source: PinDataSource) {
const targetNode = unref(node);
if (!targetNode) {
return;
}
if (typeof data === 'string') {
if (!isValidJSON(data)) {
onSetDataError({ errorType: 'invalid-json', source });
throw new Error('Invalid JSON');
}
data = jsonParse(data);
}
if (!isValidSize(data)) {
onSetDataError({ errorType: 'data-too-large', source });
throw new Error('Data too large');
}
workflowsStore.pinData({ node: targetNode, data: data as INodeExecutionData[] });
onSetDataSuccess({ source });
}
function onUnsetData({ source }: { source: UnpinDataSource }) {
const targetNode = unref(node);
const runIndex = unref(options.runIndex);
telemetry.track('User unpinned ndv data', {
node_type: targetNode?.type,
session_id: sessionId.value,
run_index: runIndex,
source,
data_size: stringSizeInBytes(data.value),
});
}
function unsetData(source: UnpinDataSource): void {
const targetNode = unref(node);
if (!targetNode) {
return;
}
onUnsetData({ source });
workflowsStore.unpinData({ node: targetNode });
}
return {
data,
hasData,
isValidNodeType,
setData,
onSetDataSuccess,
onSetDataError,
unsetData,
onUnsetData,
isValidJSON,
isValidSize,
};
}

View File

@@ -86,6 +86,7 @@ export const nodeBase = defineComponent({
props: { props: {
name: { name: {
type: String, type: String,
required: true,
}, },
instance: { instance: {
type: Object as PropType<BrowserJsPlumbInstance>, type: Object as PropType<BrowserJsPlumbInstance>,

View File

@@ -402,7 +402,7 @@ export function executeData(
// Find the parent node which has data // Find the parent node which has data
for (const parentNodeName of parentNodes) { for (const parentNodeName of parentNodes) {
if (workflowsStore.shouldReplaceInputDataWithPinData) { if (workflowsStore.shouldReplaceInputDataWithPinData) {
const parentPinData = workflowsStore.getPinData![parentNodeName]; const parentPinData = workflowsStore.pinnedWorkflowData![parentNodeName];
// populate `executeData` from `pinData` // populate `executeData` from `pinData`
@@ -650,7 +650,7 @@ export const workflowHelpers = defineComponent({
const data: IWorkflowData = { const data: IWorkflowData = {
name: this.workflowsStore.workflowName, name: this.workflowsStore.workflowName,
nodes, nodes,
pinData: this.workflowsStore.getPinData, pinData: this.workflowsStore.pinnedWorkflowData,
connections: workflowConnections, connections: workflowConnections,
active: this.workflowsStore.isWorkflowActive, active: this.workflowsStore.isWorkflowActive,
settings: this.workflowsStore.workflow.settings, settings: this.workflowsStore.workflow.settings,

View File

@@ -241,7 +241,7 @@ export const useWorkflowsStore = defineStore(STORES.WORKFLOWS, {
} }
return false; return false;
}, },
getPinData(): IPinData | undefined { pinnedWorkflowData(): IPinData | undefined {
return this.workflow.pinData; return this.workflow.pinData;
}, },
shouldReplaceInputDataWithPinData(): boolean { shouldReplaceInputDataWithPinData(): boolean {
@@ -345,7 +345,7 @@ export const useWorkflowsStore = defineStore(STORES.WORKFLOWS, {
nodeTypes, nodeTypes,
settings: this.workflowSettings, settings: this.workflowSettings,
// @ts-ignore // @ts-ignore
pinData: this.getPinData, pinData: this.pinnedWorkflowData,
}); });
return cachedWorkflow; return cachedWorkflow;

View File

@@ -193,7 +193,7 @@
<script lang="ts"> <script lang="ts">
import { defineAsyncComponent, defineComponent, nextTick } from 'vue'; import { defineAsyncComponent, defineComponent, nextTick } from 'vue';
import { mapStores } from 'pinia'; import { mapStores, storeToRefs } from 'pinia';
import type { import type {
Endpoint, Endpoint,
@@ -255,7 +255,6 @@ import { useMessage } from '@/composables/useMessage';
import { useToast } from '@/composables/useToast'; import { useToast } from '@/composables/useToast';
import { workflowHelpers } from '@/mixins/workflowHelpers'; import { workflowHelpers } from '@/mixins/workflowHelpers';
import { workflowRun } from '@/mixins/workflowRun'; import { workflowRun } from '@/mixins/workflowRun';
import { type PinDataSource, pinData } from '@/mixins/pinData';
import NodeDetailsView from '@/components/NodeDetailsView.vue'; import NodeDetailsView from '@/components/NodeDetailsView.vue';
import ContextMenu from '@/components/ContextMenu/ContextMenu.vue'; import ContextMenu from '@/components/ContextMenu/ContextMenu.vue';
@@ -372,6 +371,7 @@ import { getConnectorPaintStyleData, OVERLAY_ENDPOINT_ARROW_ID } from '@/utils/n
import { useViewStacks } from '@/components/Node/NodeCreator/composables/useViewStacks'; import { useViewStacks } from '@/components/Node/NodeCreator/composables/useViewStacks';
import { useExternalHooks } from '@/composables/useExternalHooks'; import { useExternalHooks } from '@/composables/useExternalHooks';
import { useClipboard } from '@/composables/useClipboard'; import { useClipboard } from '@/composables/useClipboard';
import { usePinnedData } from '@/composables/usePinnedData';
interface AddNodeOptions { interface AddNodeOptions {
position?: XYPosition; position?: XYPosition;
@@ -394,7 +394,7 @@ export default defineComponent({
CanvasControls, CanvasControls,
ContextMenu, ContextMenu,
}, },
mixins: [genericHelpers, moveNodeWorkflow, workflowHelpers, workflowRun, debounceHelper, pinData], mixins: [genericHelpers, moveNodeWorkflow, workflowHelpers, workflowRun, debounceHelper],
async beforeRouteLeave(to, from, next) { async beforeRouteLeave(to, from, next) {
if ( if (
getNodeViewTab(to) === MAIN_HEADER_TABS.EXECUTIONS || getNodeViewTab(to) === MAIN_HEADER_TABS.EXECUTIONS ||
@@ -456,12 +456,15 @@ export default defineComponent({
} }
}, },
setup(props, ctx) { setup(props, ctx) {
const ndvStore = useNDVStore();
const externalHooks = useExternalHooks(); const externalHooks = useExternalHooks();
const locale = useI18n(); const locale = useI18n();
const contextMenu = useContextMenu(); const contextMenu = useContextMenu();
const dataSchema = useDataSchema(); const dataSchema = useDataSchema();
const nodeHelpers = useNodeHelpers(); const nodeHelpers = useNodeHelpers();
const clipboard = useClipboard(); const clipboard = useClipboard();
const { activeNode } = storeToRefs(ndvStore);
const pinnedData = usePinnedData(activeNode);
return { return {
locale, locale,
@@ -470,6 +473,7 @@ export default defineComponent({
nodeHelpers, nodeHelpers,
externalHooks, externalHooks,
clipboard, clipboard,
pinnedData,
...useCanvasMouseSelect(), ...useCanvasMouseSelect(),
...useGlobalLinkActions(), ...useGlobalLinkActions(),
...useTitleChange(), ...useTitleChange(),
@@ -799,7 +803,7 @@ export default defineComponent({
setTimeout(() => { setTimeout(() => {
void this.usersStore.showPersonalizationSurvey(); void this.usersStore.showPersonalizationSurvey();
this.addPinDataConnections(this.workflowsStore.getPinData || ({} as IPinData)); this.addPinDataConnections(this.workflowsStore.pinnedWorkflowData || ({} as IPinData));
}, 0); }, 0);
}); });
@@ -1675,7 +1679,7 @@ export default defineComponent({
this.nodeHelpers.disableNodes(nodes, true); this.nodeHelpers.disableNodes(nodes, true);
}, },
togglePinNodes(nodes: INode[], source: PinDataSource) { togglePinNodes(nodes: INode[], source: 'keyboard-shortcut' | 'context-menu') {
if (!this.editAllowedCheck()) { if (!this.editAllowedCheck()) {
return; return;
} }
@@ -1687,13 +1691,14 @@ export default defineComponent({
); );
for (const node of nodes) { for (const node of nodes) {
const pinnedDataForNode = usePinnedData(node);
if (nextStatePinned) { if (nextStatePinned) {
const dataToPin = this.dataSchema.getInputDataWithPinned(node); const dataToPin = this.dataSchema.getInputDataWithPinned(node);
if (dataToPin.length !== 0) { if (dataToPin.length !== 0) {
this.setPinData(node, dataToPin, source); pinnedDataForNode.setData(dataToPin, source);
} }
} else { } else {
this.unsetPinData(node, source); pinnedDataForNode.unsetData(source);
} }
} }
@@ -3769,7 +3774,7 @@ export default defineComponent({
data: ITaskData[] | null; data: ITaskData[] | null;
waiting: boolean; waiting: boolean;
}) { }) {
const pinData = this.workflowsStore.getPinData; const pinData = this.workflowsStore.pinnedWorkflowData;
if (pinData?.[name]) return; if (pinData?.[name]) return;
@@ -4394,7 +4399,8 @@ export default defineComponent({
const node = tempWorkflow.nodes[nodeNameTable[nodeName]]; const node = tempWorkflow.nodes[nodeNameTable[nodeName]];
try { try {
this.setPinData(node, data.pinData[nodeName], 'add-nodes'); const pinnedDataForNode = usePinnedData(node);
pinnedDataForNode.setData(data.pinData[nodeName], 'add-nodes');
pinDataSuccess = true; pinDataSuccess = true;
} catch (error) { } catch (error) {
pinDataSuccess = false; pinDataSuccess = false;