mirror of
https://github.com/Abdulazizzn/n8n-enterprise-unlocked.git
synced 2025-12-17 10:02:05 +00:00
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:
@@ -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];
|
||||||
|
|
||||||
|
|||||||
@@ -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"
|
||||||
|
|||||||
@@ -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 &&
|
||||||
|
|||||||
@@ -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;
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -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);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -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: {
|
||||||
|
|||||||
@@ -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,
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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),
|
||||||
|
|||||||
@@ -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],
|
||||||
|
|||||||
@@ -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);
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -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);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -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();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
17
packages/editor-ui/src/composables/useActiveNode.ts
Normal file
17
packages/editor-ui/src/composables/useActiveNode.ts
Normal 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,
|
||||||
|
};
|
||||||
|
}
|
||||||
@@ -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 ?? [];
|
||||||
|
|||||||
46
packages/editor-ui/src/composables/useNodeType.ts
Normal file
46
packages/editor-ui/src/composables/useNodeType.ts
Normal 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,
|
||||||
|
};
|
||||||
|
}
|
||||||
253
packages/editor-ui/src/composables/usePinnedData.ts
Normal file
253
packages/editor-ui/src/composables/usePinnedData.ts
Normal 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,
|
||||||
|
};
|
||||||
|
}
|
||||||
@@ -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>,
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
Reference in New Issue
Block a user