feat: Update Workflow class usage on the Frontend for better performance (no-changelog) (#17680)

This commit is contained in:
Alex Grozav
2025-08-04 15:04:00 +03:00
committed by GitHub
parent ff8531d544
commit 279dce639a
66 changed files with 659 additions and 660 deletions

View File

@@ -1965,7 +1965,7 @@ describe('WorkflowExecute', () => {
test('should return true when node has no input connections', () => { test('should return true when node has no input connections', () => {
workflow.nodes = {}; workflow.nodes = {};
workflow.connectionsByDestinationNode = {}; workflow.setConnections({});
const hasInputData = workflowExecute.ensureInputData(workflow, node, executionData); const hasInputData = workflowExecute.ensureInputData(workflow, node, executionData);
@@ -1978,11 +1978,11 @@ describe('WorkflowExecute', () => {
[parentNode.name]: parentNode, [parentNode.name]: parentNode,
}; };
workflow.connectionsByDestinationNode = { workflow.setConnections({
[node.name]: { [parentNode.name]: {
main: [[{ node: parentNode.name, type: NodeConnectionTypes.Main, index: 0 }]], main: [[{ node: node.name, type: NodeConnectionTypes.Main, index: 0 }]],
}, },
}; });
const hasInputData = workflowExecute.ensureInputData(workflow, node, executionData); const hasInputData = workflowExecute.ensureInputData(workflow, node, executionData);
@@ -1996,11 +1996,11 @@ describe('WorkflowExecute', () => {
[parentNode.name]: parentNode, [parentNode.name]: parentNode,
}; };
workflow.connectionsByDestinationNode = { workflow.setConnections({
[node.name]: { [parentNode.name]: {
main: [[{ node: parentNode.name, type: NodeConnectionTypes.Main, index: 0 }]], main: [[{ node: node.name, type: NodeConnectionTypes.Main, index: 0 }]],
}, },
}; });
executionData.data = { main: [[{ json: { test: 'data' } }]] }; executionData.data = { main: [[{ json: { test: 'data' } }]] };
@@ -2015,11 +2015,11 @@ describe('WorkflowExecute', () => {
[parentNode.name]: parentNode, [parentNode.name]: parentNode,
}; };
workflow.connectionsByDestinationNode = { workflow.setConnections({
[node.name]: { [parentNode.name]: {
main: [[{ node: parentNode.name, type: NodeConnectionTypes.Main, index: 0 }]], main: [[{ node: node.name, type: NodeConnectionTypes.Main, index: 0 }]],
}, },
}; });
executionData.data = { main: [null] }; executionData.data = { main: [null] };

View File

@@ -55,7 +55,6 @@ vi.mock('vue-router', () => {
vi.mock('@/composables/useWorkflowSaving', () => ({ vi.mock('@/composables/useWorkflowSaving', () => ({
useWorkflowSaving: vi.fn().mockReturnValue({ useWorkflowSaving: vi.fn().mockReturnValue({
getCurrentWorkflow: vi.fn(),
saveCurrentWorkflow: vi.fn(), saveCurrentWorkflow: vi.fn(),
getWorkflowDataToSave: vi.fn(), getWorkflowDataToSave: vi.fn(),
setDocumentTitle: vi.fn(), setDocumentTitle: vi.fn(),

View File

@@ -58,9 +58,9 @@ describe('ButtonParameter', () => {
} as any); } as any);
vi.mocked(useWorkflowsStore).mockReturnValue({ vi.mocked(useWorkflowsStore).mockReturnValue({
getCurrentWorkflow: vi.fn().mockReturnValue({ workflowObject: {
getParentNodesByDepth: vi.fn().mockReturnValue([]), getParentNodesByDepth: vi.fn().mockReturnValue([]),
}), },
getNodeByName: vi.fn().mockReturnValue({}), getNodeByName: vi.fn().mockReturnValue({}),
} as any); } as any);

View File

@@ -19,12 +19,11 @@ export type TextareaRowData = {
export function getParentNodes() { export function getParentNodes() {
const activeNode = useNDVStore().activeNode; const activeNode = useNDVStore().activeNode;
const { getCurrentWorkflow, getNodeByName } = useWorkflowsStore(); const { workflowObject, getNodeByName } = useWorkflowsStore();
const workflow = getCurrentWorkflow();
if (!activeNode || !workflow) return []; if (!activeNode || !workflowObject) return [];
return workflow return workflowObject
.getParentNodesByDepth(activeNode?.name) .getParentNodesByDepth(activeNode?.name)
.filter(({ name }, i, nodes) => { .filter(({ name }, i, nodes) => {
return name !== activeNode.name && nodes.findIndex((node) => node.name === name) === i; return name !== activeNode.name && nodes.findIndex((node) => node.name === name) === i;

View File

@@ -91,12 +91,11 @@ function getErrorMessageByStatusCode(statusCode: number, message: string | undef
function getParentNodes() { function getParentNodes() {
const activeNode = useNDVStore().activeNode; const activeNode = useNDVStore().activeNode;
const { getCurrentWorkflow, getNodeByName } = useWorkflowsStore(); const { workflowObject, getNodeByName } = useWorkflowsStore();
const workflow = getCurrentWorkflow();
if (!activeNode || !workflow) return []; if (!activeNode || !workflowObject) return [];
return workflow return workflowObject
.getParentNodesByDepth(activeNode?.name) .getParentNodesByDepth(activeNode?.name)
.filter(({ name }, i, nodes) => { .filter(({ name }, i, nodes) => {
return name !== activeNode.name && nodes.findIndex((node) => node.name === name) === i; return name !== activeNode.name && nodes.findIndex((node) => node.name === name) === i;

View File

@@ -1,15 +1,3 @@
import type { EditorView } from '@codemirror/view';
import type { Workflow, CodeExecutionMode, CodeNodeEditorLanguage } from 'n8n-workflow';
import type { Node } from 'estree'; import type { Node } from 'estree';
import type { DefineComponent } from 'vue';
export type CodeNodeEditorMixin = InstanceType<
DefineComponent & {
editor: EditorView | null;
mode: CodeExecutionMode;
language: CodeNodeEditorLanguage;
getCurrentWorkflow(): Workflow;
}
>;
export type RangeNode = Node & { range: [number, number] }; export type RangeNode = Node & { range: [number, number] };

View File

@@ -66,12 +66,11 @@ const expressionResultRef = ref<InstanceType<typeof ExpressionOutput>>();
const theme = outputTheme(); const theme = outputTheme();
const activeNode = computed(() => ndvStore.activeNode); const activeNode = computed(() => ndvStore.activeNode);
const workflow = computed(() => workflowsStore.getCurrentWorkflow());
const inputEditor = computed(() => expressionInputRef.value?.editor); const inputEditor = computed(() => expressionInputRef.value?.editor);
const parentNodes = computed(() => { const parentNodes = computed(() => {
const node = activeNode.value; const node = activeNode.value;
if (!node) return []; if (!node) return [];
const nodes = workflow.value.getParentNodesByDepth(node.name); const nodes = workflowsStore.workflowObject.getParentNodesByDepth(node.name);
return nodes.filter(({ name }) => name !== node.name); return nodes.filter(({ name }) => name !== node.name);
}); });

View File

@@ -120,9 +120,7 @@ const { workflowRunData } = useExecutionData({ node });
const hasNodeRun = computed(() => { const hasNodeRun = computed(() => {
if (!node.value) return true; if (!node.value) return true;
const parentNode = workflowsStore const parentNode = workflowsStore.workflowObject.getParentNodes(node.value.name, 'main', 1)[0];
.getCurrentWorkflow()
.getParentNodes(node.value.name, 'main', 1)[0];
return Boolean( return Boolean(
parentNode && parentNode &&
workflowRunData.value && workflowRunData.value &&

View File

@@ -7,9 +7,12 @@ import userEvent from '@testing-library/user-event';
import { useWorkflowsStore } from '@/stores/workflows.store'; import { useWorkflowsStore } from '@/stores/workflows.store';
import { useAgentRequestStore } from '@n8n/stores/useAgentRequestStore'; import { useAgentRequestStore } from '@n8n/stores/useAgentRequestStore';
import { useRouter } from 'vue-router'; import { useRouter } from 'vue-router';
import type { Workflow } from 'n8n-workflow';
import { NodeConnectionTypes } from 'n8n-workflow'; import { NodeConnectionTypes } from 'n8n-workflow';
import { useNodeTypesStore } from '@/stores/nodeTypes.store'; import { useNodeTypesStore } from '@/stores/nodeTypes.store';
import { nextTick } from 'vue'; import { nextTick } from 'vue';
import { mock } from 'vitest-mock-extended';
import { createTestWorkflow } from '@/__tests__/mocks';
const ModalStub = { const ModalStub = {
template: ` template: `
@@ -63,10 +66,14 @@ const mockRunData = {
}, },
}; };
const mockWorkflow = { const mockWorkflow = createTestWorkflow({
id: 'test-workflow', id: 'test-workflow',
});
const mockWorkflowObject = mock<Workflow>({
id: mockWorkflow.id,
getChildNodes: () => ['Parent Node'], getChildNodes: () => ['Parent Node'],
}; });
const mockTools = [ const mockTools = [
{ {
@@ -106,6 +113,7 @@ describe('FromAiParametersModal', () => {
}, },
[STORES.WORKFLOWS]: { [STORES.WORKFLOWS]: {
workflow: mockWorkflow, workflow: mockWorkflow,
workflowObject: mockWorkflowObject,
workflowExecutionData: mockRunData, workflowExecutionData: mockRunData,
}, },
}, },
@@ -121,7 +129,6 @@ describe('FromAiParametersModal', () => {
return mockParentNode; return mockParentNode;
} }
}); });
workflowsStore.getCurrentWorkflow = vi.fn().mockReturnValue(mockWorkflow);
agentRequestStore = useAgentRequestStore(); agentRequestStore = useAgentRequestStore();
agentRequestStore.clearAgentRequests = vi.fn(); agentRequestStore.clearAgentRequests = vi.fn();
agentRequestStore.setAgentRequestForNode = vi.fn(); agentRequestStore.setAgentRequestForNode = vi.fn();

View File

@@ -45,8 +45,7 @@ const node = computed(() =>
const parentNode = computed(() => { const parentNode = computed(() => {
if (!node.value) return undefined; if (!node.value) return undefined;
const workflow = workflowsStore.getCurrentWorkflow(); const parentNodes = workflowsStore.workflowObject.getChildNodes(node.value.name, 'ALL', 1);
const parentNodes = workflow.getChildNodes(node.value.name, 'ALL', 1);
if (parentNodes.length === 0) return undefined; if (parentNodes.length === 0) return undefined;
return workflowsStore.getNodeByName(parentNodes[0])?.name; return workflowsStore.getNodeByName(parentNodes[0])?.name;
}); });

View File

@@ -97,7 +97,7 @@ const render = (props: Partial<Props> = {}, pinData?: INodeExecutionData[], runD
runIndex: 0, runIndex: 0,
currentNodeName: nodes[0].name, currentNodeName: nodes[0].name,
activeNodeName: nodes[1].name, activeNodeName: nodes[1].name,
workflow: workflowObject, workflowObject,
displayMode: 'schema', displayMode: 'schema',
focusedMappableInput: '', focusedMappableInput: '',
isMappingOnboarded: false, isMappingOnboarded: false,

View File

@@ -35,7 +35,7 @@ type MappingMode = 'debugging' | 'mapping';
export type Props = { export type Props = {
runIndex: number; runIndex: number;
workflow: Workflow; workflowObject: Workflow;
pushRef: string; pushRef: string;
activeNodeName: string; activeNodeName: string;
currentNodeName?: string; currentNodeName?: string;
@@ -103,7 +103,7 @@ const activeNode = computed(() => workflowsStore.getNodeByName(props.activeNodeN
const rootNode = computed(() => { const rootNode = computed(() => {
if (!activeNode.value) return null; if (!activeNode.value) return null;
return props.workflow.getChildNodes(activeNode.value.name, 'ALL').at(0) ?? null; return props.workflowObject.getChildNodes(activeNode.value.name, 'ALL').at(0) ?? null;
}); });
const hasRootNodeRun = computed(() => { const hasRootNodeRun = computed(() => {
@@ -134,12 +134,12 @@ const isActiveNodeConfig = computed(() => {
let inputs = activeNodeType.value?.inputs ?? []; let inputs = activeNodeType.value?.inputs ?? [];
let outputs = activeNodeType.value?.outputs ?? []; let outputs = activeNodeType.value?.outputs ?? [];
if (props.workflow && activeNode.value) { if (props.workflowObject && activeNode.value) {
const node = props.workflow.getNode(activeNode.value.name); const node = props.workflowObject.getNode(activeNode.value.name);
if (node && activeNodeType.value) { if (node && activeNodeType.value) {
inputs = NodeHelpers.getNodeInputs(props.workflow, node, activeNodeType.value); inputs = NodeHelpers.getNodeInputs(props.workflowObject, node, activeNodeType.value);
outputs = NodeHelpers.getNodeOutputs(props.workflow, node, activeNodeType.value); outputs = NodeHelpers.getNodeOutputs(props.workflowObject, node, activeNodeType.value);
} }
} }
@@ -192,7 +192,7 @@ const isExecutingPrevious = computed(() => {
const rootNodesParents = computed(() => { const rootNodesParents = computed(() => {
if (!rootNode.value) return []; if (!rootNode.value) return [];
return props.workflow.getParentNodesByDepth(rootNode.value); return props.workflowObject.getParentNodesByDepth(rootNode.value);
}); });
const currentNode = computed(() => { const currentNode = computed(() => {
@@ -219,7 +219,7 @@ const parentNodes = computed(() => {
return []; return [];
} }
const parents = props.workflow const parents = props.workflowObject
.getParentNodesByDepth(activeNode.value.name) .getParentNodesByDepth(activeNode.value.name)
.filter((parent) => parent.name !== activeNode.value?.name); .filter((parent) => parent.name !== activeNode.value?.name);
return uniqBy(parents, (parent) => parent.name); return uniqBy(parents, (parent) => parent.name);
@@ -376,7 +376,7 @@ function handleChangeCollapsingColumn(columnName: string | null) {
:class="[$style.runData, { [$style.runDataV2]: isNDVV2 }]" :class="[$style.runData, { [$style.runDataV2]: isNDVV2 }]"
:node="currentNode" :node="currentNode"
:nodes="isMappingMode ? rootNodesParents : parentNodes" :nodes="isMappingMode ? rootNodesParents : parentNodes"
:workflow="workflow" :workflow-object="workflowObject"
:run-index="isMappingMode ? 0 : runIndex" :run-index="isMappingMode ? 0 : runIndex"
:linked-runs="linkedRuns" :linked-runs="linkedRuns"
:can-link-runs="!mappedNode && canLinkRuns" :can-link-runs="!mappedNode && canLinkRuns"
@@ -430,7 +430,7 @@ function handleChangeCollapsingColumn(columnName: string | null) {
<InputNodeSelect <InputNodeSelect
v-if="parentNodes.length && currentNodeName" v-if="parentNodes.length && currentNodeName"
:model-value="currentNodeName" :model-value="currentNodeName"
:workflow="workflow" :workflow="workflowObject"
:nodes="parentNodes" :nodes="parentNodes"
@update:model-value="onInputNodeChange" @update:model-value="onInputNodeChange"
/> />
@@ -444,7 +444,7 @@ function handleChangeCollapsingColumn(columnName: string | null) {
<div :class="$style.mappedNode"> <div :class="$style.mappedNode">
<InputNodeSelect <InputNodeSelect
:model-value="mappedNode" :model-value="mappedNode"
:workflow="workflow" :workflow="workflowObject"
:nodes="rootNodesParents" :nodes="rootNodesParents"
@update:model-value="onMappedNodeSelected" @update:model-value="onMappedNodeSelected"
/> />

View File

@@ -76,18 +76,18 @@ function getINodesFromNames(names: string[]): NodeConfig[] {
const connectedNodes = computed< const connectedNodes = computed<
Record<FloatingNodePosition, Array<{ node: INodeUi; nodeType: INodeTypeDescription }>> Record<FloatingNodePosition, Array<{ node: INodeUi; nodeType: INodeTypeDescription }>>
>(() => { >(() => {
const workflow = workflowsStore.getCurrentWorkflow(); const workflowObject = workflowsStore.workflowObject;
const rootName = props.rootNode.name; const rootName = props.rootNode.name;
return { return {
[FloatingNodePosition.top]: getINodesFromNames( [FloatingNodePosition.top]: getINodesFromNames(
workflow.getChildNodes(rootName, 'ALL_NON_MAIN'), workflowObject.getChildNodes(rootName, 'ALL_NON_MAIN'),
), ),
[FloatingNodePosition.right]: getINodesFromNames( [FloatingNodePosition.right]: getINodesFromNames(
workflow.getChildNodes(rootName, NodeConnectionTypes.Main, 1), workflowObject.getChildNodes(rootName, NodeConnectionTypes.Main, 1),
).reverse(), ).reverse(),
[FloatingNodePosition.left]: getINodesFromNames( [FloatingNodePosition.left]: getINodesFromNames(
workflow.getParentNodes(rootName, NodeConnectionTypes.Main, 1), workflowObject.getParentNodes(rootName, NodeConnectionTypes.Main, 1),
).reverse(), ).reverse(),
}; };
}); });

View File

@@ -69,7 +69,7 @@ vi.mock('@/stores/nodeTypes.store', () => ({
vi.mock('@/stores/workflows.store', () => ({ vi.mock('@/stores/workflows.store', () => ({
useWorkflowsStore: vi.fn(() => ({ useWorkflowsStore: vi.fn(() => ({
getCurrentWorkflow: vi.fn(() => new Workflow(mockWorkflowData)), workflowObject: new Workflow(mockWorkflowData),
getNodeByName: mockGetNodeByName, getNodeByName: mockGetNodeByName,
})), })),
})); }));

View File

@@ -11,6 +11,7 @@ import type {
NodeConnectionType, NodeConnectionType,
INodeInputConfiguration, INodeInputConfiguration,
INodeTypeDescription, INodeTypeDescription,
Workflow,
} from 'n8n-workflow'; } from 'n8n-workflow';
import { useDebounce } from '@/composables/useDebounce'; import { useDebounce } from '@/composables/useDebounce';
import { OnClickOutside } from '@vueuse/components'; import { OnClickOutside } from '@vueuse/components';
@@ -59,10 +60,11 @@ const nodeType = computed(() =>
const nodeData = computed(() => workflowsStore.getNodeByName(props.rootNode.name)); const nodeData = computed(() => workflowsStore.getNodeByName(props.rootNode.name));
const ndvStore = useNDVStore(); const ndvStore = useNDVStore();
const workflow = computed(() => workflowsStore.getCurrentWorkflow());
const workflowObject = computed(() => workflowsStore.workflowObject as Workflow);
const nodeInputIssues = computed(() => { const nodeInputIssues = computed(() => {
const issues = nodeHelpers.getNodeIssues(nodeType.value, props.rootNode, workflow.value, [ const issues = nodeHelpers.getNodeIssues(nodeType.value, props.rootNode, workflowObject.value, [
'typeUnknown', 'typeUnknown',
'parameters', 'parameters',
'credentials', 'credentials',
@@ -82,7 +84,8 @@ const connectedNodes = computed<Record<string, NodeConfig[]>>(() => {
// Get input-index-specific connections using the per-type index // Get input-index-specific connections using the per-type index
const nodeConnections = const nodeConnections =
workflow.value.connectionsByDestinationNode[props.rootNode.name]?.[connection.type] ?? []; workflowObject.value.connectionsByDestinationNode[props.rootNode.name]?.[connection.type] ??
[];
const inputConnections = nodeConnections[typeIndex] ?? []; const inputConnections = nodeConnections[typeIndex] ?? [];
const nodeNames = inputConnections.map((conn) => conn.node); const nodeNames = inputConnections.map((conn) => conn.node);
const nodes = getINodesFromNames(nodeNames); const nodes = getINodesFromNames(nodeNames);
@@ -159,7 +162,7 @@ function getINodesFromNames(names: string[]): NodeConfig[] {
if (node) { if (node) {
const matchedNodeType = nodeTypesStore.getNodeType(node.type); const matchedNodeType = nodeTypesStore.getNodeType(node.type);
if (matchedNodeType) { if (matchedNodeType) {
const issues = nodeHelpers.getNodeIssues(matchedNodeType, node, workflow.value); const issues = nodeHelpers.getNodeIssues(matchedNodeType, node, workflowObject.value);
const stringifiedIssues = issues ? nodeHelpers.nodeIssuesToString(issues, node) : ''; const stringifiedIssues = issues ? nodeHelpers.nodeIssuesToString(issues, node) : '';
return { node, nodeType: matchedNodeType, issues: stringifiedIssues }; return { node, nodeType: matchedNodeType, issues: stringifiedIssues };
} }
@@ -187,7 +190,7 @@ function isNodeInputConfiguration(
function getPossibleSubInputConnections(): INodeInputConfiguration[] { function getPossibleSubInputConnections(): INodeInputConfiguration[] {
if (!nodeType.value || !props.rootNode) return []; if (!nodeType.value || !props.rootNode) return [];
const inputs = NodeHelpers.getNodeInputs(workflow.value, props.rootNode, nodeType.value); const inputs = NodeHelpers.getNodeInputs(workflowObject.value, props.rootNode, nodeType.value);
const nonMainInputs = inputs.filter((input): input is INodeInputConfiguration => { const nonMainInputs = inputs.filter((input): input is INodeInputConfiguration => {
if (!isNodeInputConfiguration(input)) return false; if (!isNodeInputConfiguration(input)) return false;

View File

@@ -1,10 +1,8 @@
import { createPinia, setActivePinia } from 'pinia'; import { createPinia, setActivePinia } from 'pinia';
import { waitFor, waitForElementToBeRemoved, fireEvent } from '@testing-library/vue'; import { waitFor, fireEvent } from '@testing-library/vue';
import { mock } from 'vitest-mock-extended';
import NodeDetailsView from '@/components/NodeDetailsView.vue'; import NodeDetailsView from '@/components/NodeDetailsView.vue';
import { VIEWS } from '@/constants'; import { VIEWS } from '@/constants';
import type { IWorkflowDb } from '@/Interface';
import { useSettingsStore } from '@/stores/settings.store'; import { useSettingsStore } from '@/stores/settings.store';
import { useUsersStore } from '@/stores/users.store'; import { useUsersStore } from '@/stores/users.store';
import { useNDVStore } from '@/stores/ndv.store'; import { useNDVStore } from '@/stores/ndv.store';
@@ -13,7 +11,12 @@ import { useWorkflowsStore } from '@/stores/workflows.store';
import { createComponentRenderer } from '@/__tests__/render'; import { createComponentRenderer } from '@/__tests__/render';
import { setupServer } from '@/__tests__/server'; import { setupServer } from '@/__tests__/server';
import { defaultNodeDescriptions, mockNodes } from '@/__tests__/mocks'; import {
createTestWorkflow,
createTestWorkflowObject,
defaultNodeDescriptions,
mockNodes,
} from '@/__tests__/mocks';
import { cleanupAppModals, createAppModals } from '@/__tests__/utils'; import { cleanupAppModals, createAppModals } from '@/__tests__/utils';
vi.mock('vue-router', () => { vi.mock('vue-router', () => {
@@ -26,7 +29,7 @@ vi.mock('vue-router', () => {
async function createPiniaStore(isActiveNode: boolean) { async function createPiniaStore(isActiveNode: boolean) {
const node = mockNodes[0]; const node = mockNodes[0];
const workflow = mock<IWorkflowDb>({ const workflow = createTestWorkflow({
connections: {}, connections: {},
active: true, active: true,
nodes: [node], nodes: [node],
@@ -41,6 +44,7 @@ async function createPiniaStore(isActiveNode: boolean) {
nodeTypesStore.setNodeTypes(defaultNodeDescriptions); nodeTypesStore.setNodeTypes(defaultNodeDescriptions);
workflowsStore.workflow = workflow; workflowsStore.workflow = workflow;
workflowsStore.workflowObject = createTestWorkflowObject(workflow);
workflowsStore.nodeMetadata[node.name] = { pristine: true }; workflowsStore.nodeMetadata[node.name] = { pristine: true };
if (isActiveNode) { if (isActiveNode) {
@@ -52,7 +56,7 @@ async function createPiniaStore(isActiveNode: boolean) {
return { return {
pinia, pinia,
currentWorkflow: workflowsStore.getCurrentWorkflow(), workflowObject: workflowsStore.workflowObject,
nodeName: node.name, nodeName: node.name,
}; };
} }
@@ -78,13 +82,13 @@ describe('NodeDetailsView', () => {
}); });
it('should render correctly', async () => { it('should render correctly', async () => {
const { pinia, currentWorkflow } = await createPiniaStore(true); const { pinia, workflowObject } = await createPiniaStore(true);
const renderComponent = createComponentRenderer(NodeDetailsView, { const renderComponent = createComponentRenderer(NodeDetailsView, {
props: { props: {
teleported: false, teleported: false,
appendToBody: false, appendToBody: false,
workflowObject: currentWorkflow, workflowObject,
}, },
global: { global: {
mocks: { mocks: {
@@ -104,14 +108,13 @@ describe('NodeDetailsView', () => {
describe('keyboard listener', () => { describe('keyboard listener', () => {
test('should register and unregister keydown listener based on modal open state', async () => { test('should register and unregister keydown listener based on modal open state', async () => {
const { pinia, currentWorkflow, nodeName } = await createPiniaStore(false); const { pinia, workflowObject } = await createPiniaStore(true);
const ndvStore = useNDVStore();
const renderComponent = createComponentRenderer(NodeDetailsView, { const renderComponent = createComponentRenderer(NodeDetailsView, {
props: { props: {
teleported: false, teleported: false,
appendToBody: false, appendToBody: false,
workflowObject: currentWorkflow, workflowObject,
}, },
global: { global: {
mocks: { mocks: {
@@ -122,15 +125,13 @@ describe('NodeDetailsView', () => {
}, },
}); });
const { getByTestId, queryByTestId } = renderComponent({ const { getByTestId, queryByTestId, unmount } = renderComponent({
pinia, pinia,
}); });
const addEventListenerSpy = vi.spyOn(document, 'addEventListener'); const addEventListenerSpy = vi.spyOn(document, 'addEventListener');
const removeEventListenerSpy = vi.spyOn(document, 'removeEventListener'); const removeEventListenerSpy = vi.spyOn(document, 'removeEventListener');
ndvStore.activeNodeName = nodeName;
await waitFor(() => expect(getByTestId('ndv')).toBeInTheDocument()); await waitFor(() => expect(getByTestId('ndv')).toBeInTheDocument());
await waitFor(() => expect(queryByTestId('ndv-modal')).toBeInTheDocument()); await waitFor(() => expect(queryByTestId('ndv-modal')).toBeInTheDocument());
@@ -141,9 +142,7 @@ describe('NodeDetailsView', () => {
true, true,
); );
ndvStore.activeNodeName = null; unmount();
await waitForElementToBeRemoved(queryByTestId('ndv-modal'));
expect(removeEventListenerSpy).toHaveBeenCalledWith('keydown', expect.any(Function), true); expect(removeEventListenerSpy).toHaveBeenCalledWith('keydown', expect.any(Function), true);
@@ -152,14 +151,14 @@ describe('NodeDetailsView', () => {
}); });
test('should unregister keydown listener on unmount', async () => { test('should unregister keydown listener on unmount', async () => {
const { pinia, currentWorkflow, nodeName } = await createPiniaStore(false); const { pinia, workflowObject, nodeName } = await createPiniaStore(false);
const ndvStore = useNDVStore(); const ndvStore = useNDVStore();
const renderComponent = createComponentRenderer(NodeDetailsView, { const renderComponent = createComponentRenderer(NodeDetailsView, {
props: { props: {
teleported: false, teleported: false,
appendToBody: false, appendToBody: false,
workflowObject: currentWorkflow, workflowObject,
}, },
global: { global: {
mocks: { mocks: {
@@ -194,14 +193,13 @@ describe('NodeDetailsView', () => {
}); });
test("should emit 'saveKeyboardShortcut' when save shortcut keybind is pressed", async () => { test("should emit 'saveKeyboardShortcut' when save shortcut keybind is pressed", async () => {
const { pinia, currentWorkflow, nodeName } = await createPiniaStore(false); const { pinia, workflowObject } = await createPiniaStore(true);
const ndvStore = useNDVStore();
const renderComponent = createComponentRenderer(NodeDetailsView, { const renderComponent = createComponentRenderer(NodeDetailsView, {
props: { props: {
teleported: false, teleported: false,
appendToBody: false, appendToBody: false,
workflowObject: currentWorkflow, workflowObject,
}, },
global: { global: {
mocks: { mocks: {
@@ -216,8 +214,6 @@ describe('NodeDetailsView', () => {
pinia, pinia,
}); });
ndvStore.activeNodeName = nodeName;
await waitFor(() => expect(getByTestId('ndv')).toBeInTheDocument()); await waitFor(() => expect(getByTestId('ndv')).toBeInTheDocument());
await waitFor(() => expect(queryByTestId('ndv-modal')).toBeInTheDocument()); await waitFor(() => expect(queryByTestId('ndv-modal')).toBeInTheDocument());

View File

@@ -756,7 +756,7 @@ onBeforeUnmount(() => {
/> />
<InputPanel <InputPanel
v-else-if="!isTriggerNode" v-else-if="!isTriggerNode"
:workflow="workflowObject" :workflow-object="workflowObject"
:can-link-runs="canLinkRuns" :can-link-runs="canLinkRuns"
:run-index="inputRun" :run-index="inputRun"
:linked-runs="linked" :linked-runs="linked"
@@ -785,7 +785,7 @@ onBeforeUnmount(() => {
<template #output> <template #output>
<OutputPanel <OutputPanel
data-test-id="output-panel" data-test-id="output-panel"
:workflow="workflowObject" :workflow-object="workflowObject"
:can-link-runs="canLinkRuns" :can-link-runs="canLinkRuns"
:run-index="outputRun" :run-index="outputRun"
:linked-runs="linked" :linked-runs="linked"

View File

@@ -1,10 +1,8 @@
import { createPinia, setActivePinia } from 'pinia'; import { createPinia, setActivePinia } from 'pinia';
import { waitFor, waitForElementToBeRemoved, fireEvent } from '@testing-library/vue'; import { waitFor, fireEvent } from '@testing-library/vue';
import { mock } from 'vitest-mock-extended';
import NodeDetailsViewV2 from '@/components/NodeDetailsViewV2.vue'; import NodeDetailsViewV2 from '@/components/NodeDetailsViewV2.vue';
import { VIEWS } from '@/constants'; import { VIEWS } from '@/constants';
import type { IWorkflowDb } from '@/Interface';
import { useSettingsStore } from '@/stores/settings.store'; import { useSettingsStore } from '@/stores/settings.store';
import { useUsersStore } from '@/stores/users.store'; import { useUsersStore } from '@/stores/users.store';
import { useNDVStore } from '@/stores/ndv.store'; import { useNDVStore } from '@/stores/ndv.store';
@@ -13,7 +11,12 @@ import { useWorkflowsStore } from '@/stores/workflows.store';
import { createComponentRenderer } from '@/__tests__/render'; import { createComponentRenderer } from '@/__tests__/render';
import { setupServer } from '@/__tests__/server'; import { setupServer } from '@/__tests__/server';
import { defaultNodeDescriptions, mockNodes } from '@/__tests__/mocks'; import {
createTestWorkflow,
createTestWorkflowObject,
defaultNodeDescriptions,
mockNodes,
} from '@/__tests__/mocks';
import { cleanupAppModals, createAppModals } from '@/__tests__/utils'; import { cleanupAppModals, createAppModals } from '@/__tests__/utils';
vi.mock('vue-router', () => { vi.mock('vue-router', () => {
@@ -27,7 +30,7 @@ vi.mock('vue-router', () => {
async function createPiniaStore( async function createPiniaStore(
{ activeNodeName }: { activeNodeName: string | null } = { activeNodeName: null }, { activeNodeName }: { activeNodeName: string | null } = { activeNodeName: null },
) { ) {
const workflow = mock<IWorkflowDb>({ const workflow = createTestWorkflow({
connections: {}, connections: {},
active: true, active: true,
nodes: mockNodes, nodes: mockNodes,
@@ -42,6 +45,7 @@ async function createPiniaStore(
nodeTypesStore.setNodeTypes(defaultNodeDescriptions); nodeTypesStore.setNodeTypes(defaultNodeDescriptions);
workflowsStore.workflow = workflow; workflowsStore.workflow = workflow;
workflowsStore.workflowObject = createTestWorkflowObject(workflow);
workflowsStore.nodeMetadata = mockNodes.reduce( workflowsStore.nodeMetadata = mockNodes.reduce(
(acc, node) => ({ ...acc, [node.name]: { pristine: true } }), (acc, node) => ({ ...acc, [node.name]: { pristine: true } }),
{}, {},
@@ -54,7 +58,7 @@ async function createPiniaStore(
return { return {
pinia, pinia,
currentWorkflow: workflowsStore.getCurrentWorkflow(), workflowObject: workflowsStore.workflowObject,
}; };
} }
@@ -80,13 +84,13 @@ describe('NodeDetailsViewV2', () => {
}); });
test('should render correctly', async () => { test('should render correctly', async () => {
const { pinia, currentWorkflow } = await createPiniaStore({ activeNodeName: 'Manual Trigger' }); const { pinia, workflowObject } = await createPiniaStore({ activeNodeName: 'Manual Trigger' });
const renderComponent = createComponentRenderer(NodeDetailsViewV2, { const renderComponent = createComponentRenderer(NodeDetailsViewV2, {
props: { props: {
teleported: false, teleported: false,
appendToBody: false, appendToBody: false,
workflowObject: currentWorkflow, workflowObject,
}, },
global: { global: {
mocks: { mocks: {
@@ -105,13 +109,13 @@ describe('NodeDetailsViewV2', () => {
}); });
test('should not render for stickies', async () => { test('should not render for stickies', async () => {
const { pinia, currentWorkflow } = await createPiniaStore({ activeNodeName: 'Sticky' }); const { pinia, workflowObject } = await createPiniaStore({ activeNodeName: 'Sticky' });
const renderComponent = createComponentRenderer(NodeDetailsViewV2, { const renderComponent = createComponentRenderer(NodeDetailsViewV2, {
props: { props: {
teleported: false, teleported: false,
appendToBody: false, appendToBody: false,
workflowObject: currentWorkflow, workflowObject,
}, },
global: { global: {
mocks: { mocks: {
@@ -131,14 +135,18 @@ describe('NodeDetailsViewV2', () => {
describe('keyboard listener', () => { describe('keyboard listener', () => {
test('should register and unregister keydown listener based on modal open state', async () => { test('should register and unregister keydown listener based on modal open state', async () => {
const { pinia, currentWorkflow } = await createPiniaStore(); const addEventListenerSpy = vi.spyOn(document, 'addEventListener');
const ndvStore = useNDVStore(); const removeEventListenerSpy = vi.spyOn(document, 'removeEventListener');
const { pinia, workflowObject } = await createPiniaStore({
activeNodeName: 'Manual Trigger',
});
const renderComponent = createComponentRenderer(NodeDetailsViewV2, { const renderComponent = createComponentRenderer(NodeDetailsViewV2, {
props: { props: {
teleported: false, teleported: false,
appendToBody: false, appendToBody: false,
workflowObject: currentWorkflow, workflowObject,
}, },
global: { global: {
mocks: { mocks: {
@@ -149,15 +157,10 @@ describe('NodeDetailsViewV2', () => {
}, },
}); });
const { getByTestId, queryByTestId } = renderComponent({ const { getByTestId, unmount } = renderComponent({
pinia, pinia,
}); });
const addEventListenerSpy = vi.spyOn(document, 'addEventListener');
const removeEventListenerSpy = vi.spyOn(document, 'removeEventListener');
ndvStore.activeNodeName = 'Manual Trigger';
await waitFor(() => expect(getByTestId('ndv')).toBeInTheDocument()); await waitFor(() => expect(getByTestId('ndv')).toBeInTheDocument());
expect(addEventListenerSpy).toHaveBeenCalledWith('keydown', expect.any(Function), true); expect(addEventListenerSpy).toHaveBeenCalledWith('keydown', expect.any(Function), true);
@@ -167,9 +170,7 @@ describe('NodeDetailsViewV2', () => {
true, true,
); );
ndvStore.activeNodeName = null; unmount();
await waitForElementToBeRemoved(queryByTestId('ndv'));
expect(removeEventListenerSpy).toHaveBeenCalledWith('keydown', expect.any(Function), true); expect(removeEventListenerSpy).toHaveBeenCalledWith('keydown', expect.any(Function), true);
@@ -178,14 +179,14 @@ describe('NodeDetailsViewV2', () => {
}); });
test('should unregister keydown listener on unmount', async () => { test('should unregister keydown listener on unmount', async () => {
const { pinia, currentWorkflow } = await createPiniaStore(); const { pinia, workflowObject } = await createPiniaStore();
const ndvStore = useNDVStore(); const ndvStore = useNDVStore();
const renderComponent = createComponentRenderer(NodeDetailsViewV2, { const renderComponent = createComponentRenderer(NodeDetailsViewV2, {
props: { props: {
teleported: false, teleported: false,
appendToBody: false, appendToBody: false,
workflowObject: currentWorkflow, workflowObject,
}, },
global: { global: {
mocks: { mocks: {
@@ -219,14 +220,15 @@ describe('NodeDetailsViewV2', () => {
}); });
test("should emit 'saveKeyboardShortcut' when save shortcut keybind is pressed", async () => { test("should emit 'saveKeyboardShortcut' when save shortcut keybind is pressed", async () => {
const { pinia, currentWorkflow } = await createPiniaStore(); const { pinia, workflowObject } = await createPiniaStore({
const ndvStore = useNDVStore(); activeNodeName: 'Manual Trigger',
});
const renderComponent = createComponentRenderer(NodeDetailsViewV2, { const renderComponent = createComponentRenderer(NodeDetailsViewV2, {
props: { props: {
teleported: false, teleported: false,
appendToBody: false, appendToBody: false,
workflowObject: currentWorkflow, workflowObject,
}, },
global: { global: {
mocks: { mocks: {
@@ -241,8 +243,6 @@ describe('NodeDetailsViewV2', () => {
pinia, pinia,
}); });
ndvStore.activeNodeName = 'Manual Trigger';
await waitFor(() => expect(getByTestId('ndv')).toBeInTheDocument()); await waitFor(() => expect(getByTestId('ndv')).toBeInTheDocument());
await fireEvent.keyDown(getByTestId('ndv'), { await fireEvent.keyDown(getByTestId('ndv'), {

View File

@@ -738,7 +738,7 @@ onBeforeUnmount(() => {
/> />
<InputPanel <InputPanel
v-else-if="!isTriggerNode" v-else-if="!isTriggerNode"
:workflow="workflowObject" :workflow-object="workflowObject"
:can-link-runs="canLinkRuns" :can-link-runs="canLinkRuns"
:run-index="inputRun" :run-index="inputRun"
:linked-runs="linked" :linked-runs="linked"
@@ -814,7 +814,7 @@ onBeforeUnmount(() => {
> >
<OutputPanel <OutputPanel
data-test-id="output-panel" data-test-id="output-panel"
:workflow="workflowObject" :workflow-object="workflowObject"
:can-link-runs="canLinkRuns" :can-link-runs="canLinkRuns"
:run-index="outputRun" :run-index="outputRun"
:linked-runs="linked" :linked-runs="linked"

View File

@@ -135,9 +135,8 @@ const subConnections = ref<InstanceType<typeof NDVSubConnections> | null>(null);
const installedPackage = ref<PublicInstalledPackage | undefined>(undefined); const installedPackage = ref<PublicInstalledPackage | undefined>(undefined);
const currentWorkflowInstance = computed(() => workflowsStore.getCurrentWorkflow()); const currentWorkflow = computed(
const currentWorkflow = computed(() => () => workflowsStore.getWorkflowById(workflowsStore.workflowObject.id), // @TODO check if we actually need workflowObject here
workflowsStore.getWorkflowById(currentWorkflowInstance.value.id),
); );
const hasForeignCredential = computed(() => props.foreignCredentials.length > 0); const hasForeignCredential = computed(() => props.foreignCredentials.length > 0);
const isHomeProjectTeam = computed( const isHomeProjectTeam = computed(

View File

@@ -37,7 +37,7 @@ type OutputTypeKey = keyof typeof OUTPUT_TYPE;
type OutputType = (typeof OUTPUT_TYPE)[OutputTypeKey]; type OutputType = (typeof OUTPUT_TYPE)[OutputTypeKey];
type Props = { type Props = {
workflow: Workflow; workflowObject: Workflow;
runIndex: number; runIndex: number;
isReadOnly?: boolean; isReadOnly?: boolean;
linkedRuns?: boolean; linkedRuns?: boolean;
@@ -119,7 +119,7 @@ const hasAiMetadata = computed(() => {
} }
if (node.value) { if (node.value) {
const connectedSubNodes = props.workflow.getParentNodes(node.value.name, 'ALL_NON_MAIN'); const connectedSubNodes = props.workflowObject.getParentNodes(node.value.name, 'ALL_NON_MAIN');
const resultData = connectedSubNodes.map(workflowsStore.getWorkflowResultDataByNodeName); const resultData = connectedSubNodes.map(workflowsStore.getWorkflowResultDataByNodeName);
return resultData && Array.isArray(resultData) && resultData.length > 0; return resultData && Array.isArray(resultData) && resultData.length > 0;
@@ -215,7 +215,7 @@ const allToolsWereUnusedNotice = computed(() => {
// as it likely ends up unactionable noise to the user // as it likely ends up unactionable noise to the user
if (pinnedData.hasData.value) return undefined; if (pinnedData.hasData.value) return undefined;
const toolsAvailable = props.workflow.getParentNodes( const toolsAvailable = props.workflowObject.getParentNodes(
node.value.name, node.value.name,
NodeConnectionTypes.AiTool, NodeConnectionTypes.AiTool,
1, 1,
@@ -308,7 +308,7 @@ function handleChangeCollapsingColumn(columnName: string | null) {
ref="runDataRef" ref="runDataRef"
:class="[$style.runData, { [$style.runDataV2]: isNDVV2 }]" :class="[$style.runData, { [$style.runDataV2]: isNDVV2 }]"
:node="node" :node="node"
:workflow="workflow" :workflow-object="workflowObject"
:run-index="runIndex" :run-index="runIndex"
:linked-runs="linkedRuns" :linked-runs="linkedRuns"
:can-link-runs="canLinkRuns" :can-link-runs="canLinkRuns"
@@ -460,7 +460,7 @@ function handleChangeCollapsingColumn(columnName: string | null) {
</template> </template>
<template v-if="outputMode === 'logs' && node" #content> <template v-if="outputMode === 'logs' && node" #content>
<RunDataAi :node="node" :run-index="runIndex" :workflow="workflow" /> <RunDataAi :node="node" :run-index="runIndex" :workflow-object="workflowObject" />
</template> </template>
<template #recovered-artificial-output-data> <template #recovered-artificial-output-data>

View File

@@ -14,14 +14,10 @@ import {
TEST_ISSUE, TEST_ISSUE,
} from './ParameterInputList.test.constants'; } from './ParameterInputList.test.constants';
import { FORM_NODE_TYPE, FORM_TRIGGER_NODE_TYPE } from 'n8n-workflow'; import { FORM_NODE_TYPE, FORM_TRIGGER_NODE_TYPE } from 'n8n-workflow';
import type { Workflow } from 'n8n-workflow';
import type { INodeUi } from '../Interface'; import type { INodeUi } from '../Interface';
import type { MockInstance } from 'vitest'; import type { MockInstance } from 'vitest';
import { useWorkflowsStore } from '@/stores/workflows.store';
vi.mock('@/composables/useWorkflowHelpers', () => ({
useWorkflowHelpers: vi.fn().mockReturnValue({
getCurrentWorkflow: vi.fn(),
}),
}));
vi.mock('vue-router', async () => { vi.mock('vue-router', async () => {
const actual = await vi.importActual('vue-router'); const actual = await vi.importActual('vue-router');
@@ -40,6 +36,7 @@ vi.mock('vue-router', async () => {
}); });
let ndvStore: ReturnType<typeof mockedStore<typeof useNDVStore>>; let ndvStore: ReturnType<typeof mockedStore<typeof useNDVStore>>;
let workflowStore: ReturnType<typeof mockedStore<typeof useWorkflowsStore>>;
const renderComponent = createComponentRenderer(ParameterInputList, { const renderComponent = createComponentRenderer(ParameterInputList, {
props: { props: {
@@ -59,6 +56,7 @@ describe('ParameterInputList', () => {
beforeEach(() => { beforeEach(() => {
createTestingPinia(); createTestingPinia();
ndvStore = mockedStore(useNDVStore); ndvStore = mockedStore(useNDVStore);
workflowStore = mockedStore(useWorkflowsStore);
}); });
it('renders', () => { it('renders', () => {
@@ -179,20 +177,15 @@ describe('ParameterInputList', () => {
it('should not show triggerNotice if Form Trigger is connected', () => { it('should not show triggerNotice if Form Trigger is connected', () => {
ndvStore.activeNode = { name: 'From', type: FORM_NODE_TYPE, parameters: {} } as INodeUi; ndvStore.activeNode = { name: 'From', type: FORM_NODE_TYPE, parameters: {} } as INodeUi;
workflowStore.workflowObject = {
workflowHelpersMock.mockReturnValue({ getParentNodes: vi.fn(() => ['Form Trigger']),
getCurrentWorkflow: vi.fn(() => { nodes: {
return { 'Form Trigger': {
getParentNodes: vi.fn(() => ['Form Trigger']), type: FORM_TRIGGER_NODE_TYPE,
nodes: { parameters: {},
'Form Trigger': { },
type: FORM_TRIGGER_NODE_TYPE, },
parameters: {}, } as unknown as Workflow;
},
},
};
}),
});
const { queryByText } = renderComponent({ const { queryByText } = renderComponent({
props: { props: {

View File

@@ -46,6 +46,7 @@ import {
import { storeToRefs } from 'pinia'; import { storeToRefs } from 'pinia';
import { useCalloutHelpers } from '@/composables/useCalloutHelpers'; import { useCalloutHelpers } from '@/composables/useCalloutHelpers';
import { getParameterTypeOption } from '@/utils/nodeSettingsUtils'; import { getParameterTypeOption } from '@/utils/nodeSettingsUtils';
import { useWorkflowsStore } from '@/stores/workflows.store';
const LazyFixedCollectionParameter = defineAsyncComponent( const LazyFixedCollectionParameter = defineAsyncComponent(
async () => await import('./FixedCollectionParameter.vue'), async () => await import('./FixedCollectionParameter.vue'),
@@ -78,6 +79,7 @@ const emit = defineEmits<{
const nodeTypesStore = useNodeTypesStore(); const nodeTypesStore = useNodeTypesStore();
const ndvStore = useNDVStore(); const ndvStore = useNDVStore();
const workflowsStore = useWorkflowsStore();
const message = useMessage(); const message = useMessage();
const nodeSettingsParameters = useNodeSettingsParameters(); const nodeSettingsParameters = useNodeSettingsParameters();
@@ -192,11 +194,11 @@ watch(filteredParameterNames, (newValue, oldValue) => {
}); });
function updateFormTriggerParameters(parameters: INodeProperties[], triggerName: string) { function updateFormTriggerParameters(parameters: INodeProperties[], triggerName: string) {
const workflow = workflowHelpers.getCurrentWorkflow(); const workflowObject = workflowsStore.workflowObject;
const connectedNodes = workflow.getChildNodes(triggerName); const connectedNodes = workflowObject.getChildNodes(triggerName);
const hasFormPage = connectedNodes.some((nodeName) => { const hasFormPage = connectedNodes.some((nodeName) => {
const _node = workflow.getNode(nodeName); const _node = workflowObject.getNode(nodeName);
return _node && _node.type === FORM_NODE_TYPE; return _node && _node.type === FORM_NODE_TYPE;
}); });
@@ -237,18 +239,18 @@ function updateFormTriggerParameters(parameters: INodeProperties[], triggerName:
} }
function updateWaitParameters(parameters: INodeProperties[], nodeName: string) { function updateWaitParameters(parameters: INodeProperties[], nodeName: string) {
const workflow = workflowHelpers.getCurrentWorkflow(); const workflowObject = workflowsStore.workflowObject;
const parentNodes = workflow.getParentNodes(nodeName); const parentNodes = workflowObject.getParentNodes(nodeName);
const formTriggerName = parentNodes.find( const formTriggerName = parentNodes.find(
(_node) => workflow.nodes[_node].type === FORM_TRIGGER_NODE_TYPE, (_node) => workflowObject.nodes[_node].type === FORM_TRIGGER_NODE_TYPE,
); );
if (!formTriggerName) return parameters; if (!formTriggerName) return parameters;
const connectedNodes = workflow.getChildNodes(formTriggerName); const connectedNodes = workflowObject.getChildNodes(formTriggerName);
const hasFormPage = connectedNodes.some((_nodeName) => { const hasFormPage = connectedNodes.some((_nodeName) => {
const _node = workflow.getNode(_nodeName); const _node = workflowObject.getNode(_nodeName);
return _node && _node.type === FORM_NODE_TYPE; return _node && _node.type === FORM_NODE_TYPE;
}); });
@@ -276,11 +278,11 @@ function updateWaitParameters(parameters: INodeProperties[], nodeName: string) {
} }
function updateFormParameters(parameters: INodeProperties[], nodeName: string) { function updateFormParameters(parameters: INodeProperties[], nodeName: string) {
const workflow = workflowHelpers.getCurrentWorkflow(); const workflowObject = workflowsStore.workflowObject;
const parentNodes = workflow.getParentNodes(nodeName); const parentNodes = workflowObject.getParentNodes(nodeName);
const formTriggerName = parentNodes.find( const formTriggerName = parentNodes.find(
(_node) => workflow.nodes[_node].type === FORM_TRIGGER_NODE_TYPE, (_node) => workflowObject.nodes[_node].type === FORM_TRIGGER_NODE_TYPE,
); );
if (formTriggerName) return parameters.filter((parameter) => parameter.name !== 'triggerNotice'); if (formTriggerName) return parameters.filter((parameter) => parameter.name !== 'triggerNotice');

View File

@@ -706,7 +706,7 @@ describe('RunData', () => {
node: { node: {
name: 'Test Node', name: 'Test Node',
}, },
workflow: createTestWorkflowObject({ workflowObject: createTestWorkflowObject({
id: workflowId, id: workflowId,
nodes: workflowNodes, nodes: workflowNodes,
}), }),

View File

@@ -119,7 +119,7 @@ export type EnterEditModeArgs = {
}; };
type Props = { type Props = {
workflow: Workflow; workflowObject: Workflow;
workflowExecution?: IRunExecutionData; workflowExecution?: IRunExecutionData;
runIndex: number; runIndex: number;
tooMuchDataTitle: string; tooMuchDataTitle: string;
@@ -314,7 +314,7 @@ const hasSubworkflowExecutionError = computed(() => !!workflowsStore.subWorkflow
// Sub-nodes may wish to display the parent node error as it can contain additional metadata // Sub-nodes may wish to display the parent node error as it can contain additional metadata
const parentNodeError = computed(() => { const parentNodeError = computed(() => {
const parentNode = props.workflow.getChildNodes(node.value?.name ?? '', 'ALL_NON_MAIN')[0]; const parentNode = props.workflowObject.getChildNodes(node.value?.name ?? '', 'ALL_NON_MAIN')[0];
return workflowRunData.value?.[parentNode]?.[props.runIndex]?.error as NodeError; return workflowRunData.value?.[parentNode]?.[props.runIndex]?.error as NodeError;
}); });
const workflowRunErrorAsNodeError = computed(() => { const workflowRunErrorAsNodeError = computed(() => {
@@ -508,12 +508,12 @@ const showIoSearchNoMatchContent = computed(
); );
const parentNodeOutputData = computed(() => { const parentNodeOutputData = computed(() => {
const parentNode = props.workflow.getParentNodesByDepth(node.value?.name ?? '')[0]; const parentNode = props.workflowObject.getParentNodesByDepth(node.value?.name ?? '')[0];
let parentNodeData: INodeExecutionData[] = []; let parentNodeData: INodeExecutionData[] = [];
if (parentNode?.name) { if (parentNode?.name) {
parentNodeData = nodeHelpers.getNodeInputData( parentNodeData = nodeHelpers.getNodeInputData(
props.workflow.getNode(parentNode?.name), props.workflowObject.getNode(parentNode?.name),
props.runIndex, props.runIndex,
outputIndex.value, outputIndex.value,
'input', 'input',
@@ -525,8 +525,8 @@ const parentNodeOutputData = computed(() => {
}); });
const parentNodePinnedData = computed(() => { const parentNodePinnedData = computed(() => {
const parentNode = props.workflow.getParentNodesByDepth(node.value?.name ?? '')[0]; const parentNode = props.workflowObject.getParentNodesByDepth(node.value?.name ?? '')[0];
return props.workflow.pinData?.[parentNode?.name || ''] ?? []; return props.workflowObject.pinData?.[parentNode?.name || ''] ?? [];
}); });
const showPinButton = computed(() => { const showPinButton = computed(() => {
@@ -745,10 +745,14 @@ onBeforeUnmount(() => {
function getResolvedNodeOutputs() { function getResolvedNodeOutputs() {
if (node.value && nodeType.value) { if (node.value && nodeType.value) {
const workflowNode = props.workflow.getNode(node.value.name); const workflowNode = props.workflowObject.getNode(node.value.name);
if (workflowNode) { if (workflowNode) {
const outputs = NodeHelpers.getNodeOutputs(props.workflow, workflowNode, nodeType.value); const outputs = NodeHelpers.getNodeOutputs(
props.workflowObject,
workflowNode,
nodeType.value,
);
return outputs; return outputs;
} }
} }
@@ -780,13 +784,14 @@ function shouldHintBeDisplayed(hint: NodeHint): boolean {
return true; return true;
} }
function getNodeHints(): NodeHint[] {
const nodeHints = computed<NodeHint[]>(() => {
try { try {
if (node.value && nodeType.value) { if (node.value && nodeType.value) {
const workflowNode = props.workflow.getNode(node.value.name); const workflowNode = props.workflowObject.getNode(node.value.name);
if (workflowNode) { if (workflowNode) {
const nodeHints = nodeHelpers.getNodeHints(props.workflow, workflowNode, nodeType.value, { const hints = nodeHelpers.getNodeHints(props.workflowObject, workflowNode, nodeType.value, {
runExecutionData: workflowExecution.value ?? null, runExecutionData: workflowExecution.value ?? null,
runIndex: props.runIndex, runIndex: props.runIndex,
connectionInputData: parentNodeOutputData.value, connectionInputData: parentNodeOutputData.value,
@@ -803,13 +808,13 @@ function getNodeHints(): NodeHint[] {
node: node.value, node: node.value,
nodeType: nodeType.value, nodeType: nodeType.value,
nodeOutputData, nodeOutputData,
nodes: props.workflow.nodes, nodes: props.workflowObject.nodes,
connections: props.workflow.connectionsBySourceNode, connections: props.workflowObject.connectionsBySourceNode,
hasNodeRun: hasNodeRun.value, hasNodeRun: hasNodeRun.value,
hasMultipleInputItems, hasMultipleInputItems,
}); });
return executionHints.value.concat(nodeHints, genericHints).filter(shouldHintBeDisplayed); return executionHints.value.concat(hints, genericHints).filter(shouldHintBeDisplayed);
} }
} }
} catch (error) { } catch (error) {
@@ -817,7 +822,8 @@ function getNodeHints(): NodeHint[] {
} }
return []; return [];
} });
function onItemHover(itemIndex: number | null) { function onItemHover(itemIndex: number | null) {
if (itemIndex === null) { if (itemIndex === null) {
emit('itemHover', null); emit('itemHover', null);
@@ -1548,7 +1554,7 @@ defineExpose({ enterEditMode });
:node="node" :node="node"
/> />
<N8nCallout <N8nCallout
v-for="hint in getNodeHints()" v-for="hint in nodeHints"
:key="hint.message" :key="hint.message"
:class="$style.hintCallout" :class="$style.hintCallout"
:theme="hint.type || 'info'" :theme="hint.type || 'info'"

View File

@@ -21,7 +21,7 @@ export interface Props {
node: INodeUi; node: INodeUi;
runIndex?: number; runIndex?: number;
slim?: boolean; slim?: boolean;
workflow: Workflow; workflowObject: Workflow;
} }
const props = withDefaults(defineProps<Props>(), { runIndex: 0 }); const props = withDefaults(defineProps<Props>(), { runIndex: 0 });
const workflowsStore = useWorkflowsStore(); const workflowsStore = useWorkflowsStore();
@@ -33,7 +33,7 @@ const i18n = useI18n();
const aiData = computed<AIResult[]>(() => const aiData = computed<AIResult[]>(() =>
createAiData( createAiData(
props.node.name, props.node.name,
props.workflow.connectionsBySourceNode, props.workflowObject.connectionsBySourceNode,
workflowsStore.getWorkflowResultDataByNodeName, workflowsStore.getWorkflowResultDataByNodeName,
), ),
); );
@@ -41,7 +41,7 @@ const aiData = computed<AIResult[]>(() =>
const executionTree = computed<TreeNode[]>(() => const executionTree = computed<TreeNode[]>(() =>
getTreeNodeData( getTreeNodeData(
props.node.name, props.node.name,
props.workflow.connectionsBySourceNode, props.workflowObject.connectionsBySourceNode,
aiData.value, aiData.value,
props.runIndex, props.runIndex,
), ),

View File

@@ -78,9 +78,12 @@ const hideContent = computed(() => {
} }
if (node.value) { if (node.value) {
const hideContentValue = workflowHelpers const hideContentValue = workflowsStore.workflowObject.expression.getSimpleParameterValue(
.getCurrentWorkflow() node.value,
.expression.getSimpleParameterValue(node.value, hideContent, 'internal', {}); hideContent,
'internal',
{},
);
if (typeof hideContentValue === 'boolean') { if (typeof hideContentValue === 'boolean') {
return hideContentValue; return hideContentValue;

View File

@@ -14,7 +14,7 @@ beforeEach(() => {
setActivePinia(pinia); setActivePinia(pinia);
const workflowsStore = useWorkflowsStore(); const workflowsStore = useWorkflowsStore();
const workflowObject = createTestWorkflowObject(workflowsStore.workflow); const workflowObject = createTestWorkflowObject(workflowsStore.workflow);
workflowsStore.getCurrentWorkflow = vi.fn().mockReturnValue(workflowObject); workflowsStore.workflowObject = workflowObject;
}); });
describe('CanvasNodeRenderer', () => { describe('CanvasNodeRenderer', () => {

View File

@@ -22,7 +22,7 @@ beforeEach(() => {
setActivePinia(pinia); setActivePinia(pinia);
const workflowsStore = useWorkflowsStore(); const workflowsStore = useWorkflowsStore();
const workflowObject = createTestWorkflowObject(workflowsStore.workflow); const workflowObject = createTestWorkflowObject(workflowsStore.workflow);
workflowsStore.getCurrentWorkflow = vi.fn().mockReturnValue(workflowObject); workflowsStore.workflowObject = workflowObject;
}); });
describe('CanvasNodeDefault', () => { describe('CanvasNodeDefault', () => {

View File

@@ -1,16 +1,15 @@
<script setup lang="ts"> <script setup lang="ts">
import { computed } from 'vue'; import { computed } from 'vue';
import { useCanvasNode } from '@/composables/useCanvasNode'; import { useCanvasNode } from '@/composables/useCanvasNode';
import { useWorkflowHelpers } from '@/composables/useWorkflowHelpers';
import { useI18n } from '@n8n/i18n'; import { useI18n } from '@n8n/i18n';
import { N8nIcon } from '@n8n/design-system'; import { N8nIcon } from '@n8n/design-system';
import { useWorkflowsStore } from '@/stores/workflows.store';
const { name } = useCanvasNode(); const { name } = useCanvasNode();
const i18n = useI18n(); const i18n = useI18n();
const workflowHelpers = useWorkflowHelpers(); const workflowsStore = useWorkflowsStore();
const workflow = computed(() => workflowHelpers.getCurrentWorkflow()); const node = computed(() => workflowsStore.workflowObject.getNode(name.value));
const node = computed(() => workflow.value.getNode(name.value));
const size = 'medium'; const size = 'medium';
</script> </script>

View File

@@ -78,7 +78,7 @@ watch(viewport, () => {
ref="inputPanel" ref="inputPanel"
:tabindex="-1" :tabindex="-1"
:class="$style.inputPanel" :class="$style.inputPanel"
:workflow="workflow" :workflow-object="workflow"
:run-index="0" :run-index="0"
compact compact
push-ref="" push-ref=""

View File

@@ -12,6 +12,7 @@ import { computed, provide, ref, useTemplateRef } from 'vue';
import { useExperimentalNdvStore } from '../experimentalNdv.store'; import { useExperimentalNdvStore } from '../experimentalNdv.store';
import ExperimentalCanvasNodeSettings from './ExperimentalCanvasNodeSettings.vue'; import ExperimentalCanvasNodeSettings from './ExperimentalCanvasNodeSettings.vue';
import { useI18n } from '@n8n/i18n'; import { useI18n } from '@n8n/i18n';
import type { Workflow } from 'n8n-workflow';
import NodeIcon from '@/components/NodeIcon.vue'; import NodeIcon from '@/components/NodeIcon.vue';
import { getNodeSubTitleText } from '@/components/canvas/experimental/experimentalNdv.utils'; import { getNodeSubTitleText } from '@/components/canvas/experimental/experimentalNdv.utils';
import ExperimentalEmbeddedNdvActions from '@/components/canvas/experimental/components/ExperimentalEmbeddedNdvActions.vue'; import ExperimentalEmbeddedNdvActions from '@/components/canvas/experimental/components/ExperimentalEmbeddedNdvActions.vue';
@@ -79,7 +80,7 @@ const expressionResolveCtx = computed<ExpressionLocalResolveContext | undefined>
}; };
} }
const inputs = workflow.value.getParentNodesByDepth(nodeName, 1); const inputs = workflowObject.value.getParentNodesByDepth(nodeName, 1);
if (inputs.length > 0) { if (inputs.length > 0) {
return { return {
@@ -95,7 +96,7 @@ const expressionResolveCtx = computed<ExpressionLocalResolveContext | undefined>
return { return {
localResolve: true, localResolve: true,
envVars: useEnvironmentsStore().variablesAsObject, envVars: useEnvironmentsStore().variablesAsObject,
workflow: workflow.value, workflow: workflowObject.value,
execution, execution,
nodeName, nodeName,
additionalKeys: {}, additionalKeys: {},
@@ -104,7 +105,7 @@ const expressionResolveCtx = computed<ExpressionLocalResolveContext | undefined>
}; };
}); });
const workflow = computed(() => workflowsStore.getCurrentWorkflow()); const workflowObject = computed(() => workflowsStore.workflowObject as Workflow);
function handleToggleExpand() { function handleToggleExpand() {
experimentalNdvStore.setNodeExpanded(nodeId); experimentalNdvStore.setNodeExpanded(nodeId);
@@ -142,7 +143,7 @@ watchOnce(isVisible, (visible) => {
<template v-if="!node || !isOnceVisible" /> <template v-if="!node || !isOnceVisible" />
<ExperimentalEmbeddedNdvMapper <ExperimentalEmbeddedNdvMapper
v-else-if="isExpanded" v-else-if="isExpanded"
:workflow="workflow" :workflow="workflowObject"
:node="node" :node="node"
:input-node-name="expressionResolveCtx?.inputNode?.name" :input-node-name="expressionResolveCtx?.inputNode?.name"
:container="containerRef" :container="containerRef"

View File

@@ -40,9 +40,9 @@ vi.mock('@/stores/users.store', () => ({
vi.mock('@/stores/workflows.store', () => ({ vi.mock('@/stores/workflows.store', () => ({
useWorkflowsStore: () => ({ useWorkflowsStore: () => ({
getCurrentWorkflow: vi.fn(() => ({ workflowObject: {
id: '1', id: '1',
})), },
getWorkflowById: mocks.getWorkflowById, getWorkflowById: mocks.getWorkflowById,
}), }),
})); }));

View File

@@ -45,8 +45,8 @@ export function useCalloutHelpers() {
const template = getRagStarterWorkflowJson(); const template = getRagStarterWorkflowJson();
const routeTemplateId = route.query.templateId; const routeTemplateId = route.query.templateId;
const currentWorkflow = workflowsStore.getCurrentWorkflow(); const workflowObject = workflowsStore.workflowObject;
const workflow = workflowsStore.getWorkflowById(currentWorkflow.id); const workflow = workflowsStore.getWorkflowById(workflowObject.id); // @TODO Check if we actually need workflowObject here
// Hide the RAG starter callout if we're currently on the RAG starter template // Hide the RAG starter callout if we're currently on the RAG starter template
if ((routeTemplateId ?? workflow?.meta?.templateId) === template.meta.templateId) { if ((routeTemplateId ?? workflow?.meta?.templateId) === template.meta.templateId) {

View File

@@ -582,6 +582,8 @@ export function useCanvasMapping({
const outputConnections = connectionsBySourceNode[node.name] ?? {}; const outputConnections = connectionsBySourceNode[node.name] ?? {};
const inputConnections = connectionsByDestinationNode[node.name] ?? {}; const inputConnections = connectionsByDestinationNode[node.name] ?? {};
// console.log(node.name, nodeInputsById.value[node.id]);
const data: CanvasNodeData = { const data: CanvasNodeData = {
id: node.id, id: node.id,
name: node.name, name: node.name,

View File

@@ -295,9 +295,7 @@ describe('useCanvasOperations', () => {
vi.spyOn(uiStore, 'lastInteractedWithNode', 'get').mockReturnValue(node); vi.spyOn(uiStore, 'lastInteractedWithNode', 'get').mockReturnValue(node);
nodeTypesStore.getNodeType = vi.fn().mockReturnValue(nodeTypeDescription); nodeTypesStore.getNodeType = vi.fn().mockReturnValue(nodeTypeDescription);
workflowsStore.getCurrentWorkflow.mockReturnValue( workflowsStore.workflowObject = createTestWorkflowObject(workflowsStore.workflow);
createTestWorkflowObject(workflowsStore.workflow),
);
uiStore.lastInteractedWithNodeHandle = 'inputs/main/0'; uiStore.lastInteractedWithNodeHandle = 'inputs/main/0';
uiStore.lastCancelledConnectionPosition = [200, 200]; uiStore.lastCancelledConnectionPosition = [200, 200];
@@ -324,7 +322,8 @@ describe('useCanvasOperations', () => {
typeVersion: 1, typeVersion: 1,
}); });
nodeTypesStore.getNodeType = vi.fn().mockReturnValue(nodeTypeDescription); nodeTypesStore.getNodeType = vi.fn().mockReturnValue(nodeTypeDescription);
workflowsStore.getCurrentWorkflow.mockReturnValue(workflowObject); workflowsStore.workflowObject = workflowObject;
workflowsStore.cloneWorkflowObject = vi.fn().mockReturnValue(workflowObject);
workflowObject.getNode = vi.fn().mockReturnValue(node); workflowObject.getNode = vi.fn().mockReturnValue(node);
const { resolveNodePosition } = useCanvasOperations(); const { resolveNodePosition } = useCanvasOperations();
@@ -348,7 +347,8 @@ describe('useCanvasOperations', () => {
typeVersion: 1, typeVersion: 1,
}); });
nodeTypesStore.getNodeType = vi.fn().mockReturnValue(nodeTypeDescription); nodeTypesStore.getNodeType = vi.fn().mockReturnValue(nodeTypeDescription);
workflowsStore.getCurrentWorkflow.mockReturnValue(workflowObject); workflowsStore.workflowObject = workflowObject;
workflowsStore.cloneWorkflowObject = vi.fn().mockReturnValue(workflowObject);
workflowObject.getNode = vi.fn().mockReturnValue(node); workflowObject.getNode = vi.fn().mockReturnValue(node);
vi.spyOn(NodeHelpers, 'getNodeOutputs').mockReturnValueOnce([ vi.spyOn(NodeHelpers, 'getNodeOutputs').mockReturnValueOnce([
@@ -406,7 +406,8 @@ describe('useCanvasOperations', () => {
}); });
uiStore.lastInteractedWithNodeHandle = `outputs/${NodeConnectionTypes.AiLanguageModel}/0`; uiStore.lastInteractedWithNodeHandle = `outputs/${NodeConnectionTypes.AiLanguageModel}/0`;
nodeTypesStore.getNodeType = vi.fn().mockReturnValue(nodeTypeDescription); nodeTypesStore.getNodeType = vi.fn().mockReturnValue(nodeTypeDescription);
workflowsStore.getCurrentWorkflow.mockReturnValue(workflowObject); workflowsStore.workflowObject = workflowObject;
workflowsStore.cloneWorkflowObject = vi.fn().mockReturnValue(workflowObject);
workflowObject.getNode = vi.fn().mockReturnValue(node); workflowObject.getNode = vi.fn().mockReturnValue(node);
vi.spyOn(NodeHelpers, 'getNodeOutputs').mockReturnValueOnce([ vi.spyOn(NodeHelpers, 'getNodeOutputs').mockReturnValueOnce([
@@ -442,7 +443,8 @@ describe('useCanvasOperations', () => {
}); });
uiStore.lastInteractedWithNodeHandle = `outputs/${NodeConnectionTypes.AiMemory}/0`; uiStore.lastInteractedWithNodeHandle = `outputs/${NodeConnectionTypes.AiMemory}/0`;
nodeTypesStore.getNodeType = vi.fn().mockReturnValue(nodeTypeDescription); nodeTypesStore.getNodeType = vi.fn().mockReturnValue(nodeTypeDescription);
workflowsStore.getCurrentWorkflow.mockReturnValue(workflowObject); workflowsStore.workflowObject = workflowObject;
workflowsStore.cloneWorkflowObject = vi.fn().mockReturnValue(workflowObject);
workflowObject.getNode = vi.fn().mockReturnValue(node); workflowObject.getNode = vi.fn().mockReturnValue(node);
vi.spyOn(NodeHelpers, 'getNodeOutputs').mockReturnValueOnce([ vi.spyOn(NodeHelpers, 'getNodeOutputs').mockReturnValueOnce([
@@ -476,7 +478,8 @@ describe('useCanvasOperations', () => {
}); });
uiStore.lastInteractedWithNodeHandle = `outputs/${NodeConnectionTypes.AiTool}/0`; uiStore.lastInteractedWithNodeHandle = `outputs/${NodeConnectionTypes.AiTool}/0`;
nodeTypesStore.getNodeType = vi.fn().mockReturnValue(nodeTypeDescription); nodeTypesStore.getNodeType = vi.fn().mockReturnValue(nodeTypeDescription);
workflowsStore.getCurrentWorkflow.mockReturnValue(workflowObject); workflowsStore.workflowObject = workflowObject;
workflowsStore.cloneWorkflowObject = vi.fn().mockReturnValue(workflowObject);
workflowObject.getNode = vi.fn().mockReturnValue(node); workflowObject.getNode = vi.fn().mockReturnValue(node);
vi.spyOn(NodeHelpers, 'getNodeOutputs').mockReturnValueOnce([ vi.spyOn(NodeHelpers, 'getNodeOutputs').mockReturnValueOnce([
@@ -512,7 +515,8 @@ describe('useCanvasOperations', () => {
}); });
// No lastInteractedWithNodeHandle set // No lastInteractedWithNodeHandle set
nodeTypesStore.getNodeType = vi.fn().mockReturnValue(nodeTypeDescription); nodeTypesStore.getNodeType = vi.fn().mockReturnValue(nodeTypeDescription);
workflowsStore.getCurrentWorkflow.mockReturnValue(workflowObject); workflowsStore.workflowObject = workflowObject;
workflowsStore.cloneWorkflowObject = vi.fn().mockReturnValue(workflowObject);
workflowObject.getNode = vi.fn().mockReturnValue(node); workflowObject.getNode = vi.fn().mockReturnValue(node);
vi.spyOn(NodeHelpers, 'getNodeOutputs').mockReturnValueOnce([ vi.spyOn(NodeHelpers, 'getNodeOutputs').mockReturnValueOnce([
@@ -747,9 +751,7 @@ describe('useCanvasOperations', () => {
mockNode({ name: 'Node 2', type: nodeTypeName, position: [96, 256] }), mockNode({ name: 'Node 2', type: nodeTypeName, position: [96, 256] }),
]; ];
workflowsStore.getCurrentWorkflow.mockReturnValue( workflowsStore.workflowObject = createTestWorkflowObject(workflowsStore.workflow);
createTestWorkflowObject(workflowsStore.workflow),
);
nodeTypesStore.nodeTypes = { nodeTypesStore.nodeTypes = {
[nodeTypeName]: { 1: mockNodeTypeDescription({ name: nodeTypeName }) }, [nodeTypeName]: { 1: mockNodeTypeDescription({ name: nodeTypeName }) },
@@ -784,9 +786,7 @@ describe('useCanvasOperations', () => {
mockNode({ name: 'Node 2', type: nodeTypeName, position: [192, 320] }), mockNode({ name: 'Node 2', type: nodeTypeName, position: [192, 320] }),
]; ];
workflowsStore.getCurrentWorkflow.mockReturnValue( workflowsStore.workflowObject = createTestWorkflowObject(workflowsStore.workflow);
createTestWorkflowObject(workflowsStore.workflow),
);
nodeTypesStore.nodeTypes = { nodeTypesStore.nodeTypes = {
[nodeTypeName]: { 1: mockNodeTypeDescription({ name: nodeTypeName }) }, [nodeTypeName]: { 1: mockNodeTypeDescription({ name: nodeTypeName }) },
@@ -822,16 +822,14 @@ describe('useCanvasOperations', () => {
[nodeTypeName]: { 1: mockNodeTypeDescription({ name: nodeTypeName }) }, [nodeTypeName]: { 1: mockNodeTypeDescription({ name: nodeTypeName }) },
}; };
vi.spyOn(workflowsStore, 'getCurrentWorkflow').mockImplementation(() => workflowsStore.workflowObject = mock<Workflow>({
mock<Workflow>({ getParentNodesByDepth: () =>
getParentNodesByDepth: () => nodes.map((node) => ({
nodes.map((node) => ({ name: node.name,
name: node.name, depth: 0,
depth: 0, indicies: [],
indicies: [], })),
})), });
}),
);
const { addNodes } = useCanvasOperations(); const { addNodes } = useCanvasOperations();
await addNodes(nodes, {}); await addNodes(nodes, {});
@@ -850,9 +848,7 @@ describe('useCanvasOperations', () => {
mockNode({ name: 'Node 2', type: nodeTypeName, position: [96, 240] }), mockNode({ name: 'Node 2', type: nodeTypeName, position: [96, 240] }),
]; ];
workflowsStore.getCurrentWorkflow.mockReturnValue( workflowsStore.workflowObject = createTestWorkflowObject(workflowsStore.workflow);
createTestWorkflowObject(workflowsStore.workflow),
);
nodeTypesStore.nodeTypes = { nodeTypesStore.nodeTypes = {
[nodeTypeName]: { 1: mockNodeTypeDescription({ name: nodeTypeName }) }, [nodeTypeName]: { 1: mockNodeTypeDescription({ name: nodeTypeName }) },
@@ -870,9 +866,7 @@ describe('useCanvasOperations', () => {
const nodeTypeName = 'type'; const nodeTypeName = 'type';
const nodes = [mockNode({ name: 'Node 1', type: nodeTypeName, position: [30, 40] })]; const nodes = [mockNode({ name: 'Node 1', type: nodeTypeName, position: [30, 40] })];
workflowsStore.getCurrentWorkflow.mockReturnValue( workflowsStore.workflowObject = createTestWorkflowObject(workflowsStore.workflow);
createTestWorkflowObject(workflowsStore.workflow),
);
nodeTypesStore.nodeTypes = { nodeTypesStore.nodeTypes = {
[nodeTypeName]: { 1: mockNodeTypeDescription({ name: nodeTypeName }) }, [nodeTypeName]: { 1: mockNodeTypeDescription({ name: nodeTypeName }) },
@@ -891,9 +885,7 @@ describe('useCanvasOperations', () => {
const nodeTypeName = 'type'; const nodeTypeName = 'type';
const nodes = [mockNode({ name: 'Node 1', type: nodeTypeName, position: [30, 40] })]; const nodes = [mockNode({ name: 'Node 1', type: nodeTypeName, position: [30, 40] })];
workflowsStore.getCurrentWorkflow.mockReturnValue( workflowsStore.workflowObject = createTestWorkflowObject(workflowsStore.workflow);
createTestWorkflowObject(workflowsStore.workflow),
);
nodeTypesStore.nodeTypes = { nodeTypesStore.nodeTypes = {
[nodeTypeName]: { 1: mockNodeTypeDescription({ name: nodeTypeName }) }, [nodeTypeName]: { 1: mockNodeTypeDescription({ name: nodeTypeName }) },
@@ -927,7 +919,8 @@ describe('useCanvasOperations', () => {
const historyStore = mockedStore(useHistoryStore); const historyStore = mockedStore(useHistoryStore);
const workflowObject = createTestWorkflowObject(workflowsStore.workflow); const workflowObject = createTestWorkflowObject(workflowsStore.workflow);
workflowsStore.getCurrentWorkflow.mockReturnValue(workflowObject); workflowsStore.workflowObject = workflowObject;
workflowsStore.cloneWorkflowObject = vi.fn().mockReturnValue(workflowObject);
workflowsStore.incomingConnectionsByNodeName.mockReturnValue({}); workflowsStore.incomingConnectionsByNodeName.mockReturnValue({});
const id = 'node1'; const id = 'node1';
@@ -955,7 +948,8 @@ describe('useCanvasOperations', () => {
const historyStore = mockedStore(useHistoryStore); const historyStore = mockedStore(useHistoryStore);
const workflowObject = createTestWorkflowObject(workflowsStore.workflow); const workflowObject = createTestWorkflowObject(workflowsStore.workflow);
workflowsStore.getCurrentWorkflow.mockReturnValue(workflowObject); workflowsStore.workflowObject = workflowObject;
workflowsStore.cloneWorkflowObject = vi.fn().mockReturnValue(workflowObject);
workflowsStore.incomingConnectionsByNodeName.mockReturnValue({}); workflowsStore.incomingConnectionsByNodeName.mockReturnValue({});
const id = 'node1'; const id = 'node1';
@@ -1033,7 +1027,8 @@ describe('useCanvasOperations', () => {
}; };
const workflowObject = createTestWorkflowObject(workflowsStore.workflow); const workflowObject = createTestWorkflowObject(workflowsStore.workflow);
workflowsStore.getCurrentWorkflow.mockReturnValue(workflowObject); workflowsStore.workflowObject = workflowObject;
workflowsStore.cloneWorkflowObject = vi.fn().mockReturnValue(workflowObject);
workflowsStore.incomingConnectionsByNodeName.mockReturnValue({}); workflowsStore.incomingConnectionsByNodeName.mockReturnValue({});
workflowsStore.getNodeById.mockReturnValue(nodes[1]); workflowsStore.getNodeById.mockReturnValue(nodes[1]);
@@ -1109,7 +1104,8 @@ describe('useCanvasOperations', () => {
}; };
const workflowObject = createTestWorkflowObject(workflowsStore.workflow); const workflowObject = createTestWorkflowObject(workflowsStore.workflow);
workflowsStore.getCurrentWorkflow.mockReturnValue(workflowObject); workflowsStore.workflowObject = workflowObject;
workflowsStore.cloneWorkflowObject = vi.fn().mockReturnValue(workflowObject);
workflowsStore.incomingConnectionsByNodeName.mockReturnValue({}); workflowsStore.incomingConnectionsByNodeName.mockReturnValue({});
workflowsStore.getNodeById.mockReturnValue(nodes[1]); workflowsStore.getNodeById.mockReturnValue(nodes[1]);
@@ -1151,7 +1147,8 @@ describe('useCanvasOperations', () => {
const workflowObject = createTestWorkflowObject(); const workflowObject = createTestWorkflowObject();
workflowObject.renameNode = vi.fn(); workflowObject.renameNode = vi.fn();
workflowsStore.getCurrentWorkflow.mockReturnValue(workflowObject); workflowsStore.workflowObject = workflowObject;
workflowsStore.cloneWorkflowObject = vi.fn().mockReturnValue(workflowObject);
workflowsStore.getNodeByName = vi.fn().mockReturnValue({ name: oldName }); workflowsStore.getNodeByName = vi.fn().mockReturnValue({ name: oldName });
ndvStore.activeNodeName = oldName; ndvStore.activeNodeName = oldName;
@@ -1189,7 +1186,8 @@ describe('useCanvasOperations', () => {
const error = new UserError(errorMessage, { description: errorDescription }); const error = new UserError(errorMessage, { description: errorDescription });
throw error; throw error;
}); });
workflowsStore.getCurrentWorkflow.mockReturnValue(workflowObject); workflowsStore.workflowObject = workflowObject;
workflowsStore.cloneWorkflowObject = vi.fn().mockReturnValue(workflowObject);
workflowsStore.getNodeByName = vi.fn().mockReturnValue({ name: oldName }); workflowsStore.getNodeByName = vi.fn().mockReturnValue({ name: oldName });
ndvStore.activeNodeName = oldName; ndvStore.activeNodeName = oldName;
@@ -1214,7 +1212,8 @@ describe('useCanvasOperations', () => {
const workflowObject = createTestWorkflowObject(); const workflowObject = createTestWorkflowObject();
workflowObject.renameNode = vi.fn(); workflowObject.renameNode = vi.fn();
workflowsStore.getCurrentWorkflow.mockReturnValue(workflowObject); workflowsStore.workflowObject = workflowObject;
workflowsStore.cloneWorkflowObject = vi.fn().mockReturnValue(workflowObject);
workflowsStore.getNodeByName = vi.fn().mockReturnValue({ name: currentName }); workflowsStore.getNodeByName = vi.fn().mockReturnValue({ name: currentName });
ndvStore.activeNodeName = currentName; ndvStore.activeNodeName = currentName;
@@ -1396,7 +1395,8 @@ describe('useCanvasOperations', () => {
}; };
const workflowObject = createTestWorkflowObject(workflowsStore.workflow); const workflowObject = createTestWorkflowObject(workflowsStore.workflow);
workflowsStore.getCurrentWorkflow.mockReturnValue(workflowObject); workflowsStore.workflowObject = workflowObject;
workflowsStore.cloneWorkflowObject = vi.fn().mockReturnValue(workflowObject);
workflowsStore.getNodeById.mockReturnValueOnce(nodes[0]).mockReturnValueOnce(nodes[1]); workflowsStore.getNodeById.mockReturnValueOnce(nodes[0]).mockReturnValueOnce(nodes[1]);
nodeTypesStore.getNodeType = vi.fn().mockReturnValue(nodeType); nodeTypesStore.getNodeType = vi.fn().mockReturnValue(nodeType);
@@ -1510,7 +1510,8 @@ describe('useCanvasOperations', () => {
nodeTypesStore.getNodeType = vi.fn().mockReturnValue(nodeTypeDescription); nodeTypesStore.getNodeType = vi.fn().mockReturnValue(nodeTypeDescription);
const workflowObject = createTestWorkflowObject(workflowsStore.workflow); const workflowObject = createTestWorkflowObject(workflowsStore.workflow);
workflowsStore.getCurrentWorkflow.mockReturnValue(workflowObject); workflowsStore.workflowObject = workflowObject;
workflowsStore.cloneWorkflowObject = vi.fn().mockReturnValue(workflowObject);
const { createConnection, editableWorkflowObject } = useCanvasOperations(); const { createConnection, editableWorkflowObject } = useCanvasOperations();
@@ -1567,7 +1568,8 @@ describe('useCanvasOperations', () => {
nodeTypesStore.getNodeType = vi.fn().mockReturnValue(nodeTypeDescription); nodeTypesStore.getNodeType = vi.fn().mockReturnValue(nodeTypeDescription);
const workflowObject = createTestWorkflowObject(workflowsStore.workflow); const workflowObject = createTestWorkflowObject(workflowsStore.workflow);
workflowsStore.getCurrentWorkflow.mockReturnValue(workflowObject); workflowsStore.workflowObject = workflowObject;
workflowsStore.cloneWorkflowObject = vi.fn().mockReturnValue(workflowObject);
const { createConnection, editableWorkflowObject } = useCanvasOperations(); const { createConnection, editableWorkflowObject } = useCanvasOperations();
@@ -1633,7 +1635,8 @@ describe('useCanvasOperations', () => {
}; };
const workflowObject = createTestWorkflowObject(workflowsStore.workflow); const workflowObject = createTestWorkflowObject(workflowsStore.workflow);
workflowsStore.getCurrentWorkflow.mockReturnValue(workflowObject); workflowsStore.workflowObject = workflowObject;
workflowsStore.cloneWorkflowObject = vi.fn().mockReturnValue(workflowObject);
nodeTypesStore.getNodeType = vi.fn( nodeTypesStore.getNodeType = vi.fn(
(nodeTypeName: string) => (nodeTypeName: string) =>
@@ -1681,7 +1684,8 @@ describe('useCanvasOperations', () => {
}; };
const workflowObject = createTestWorkflowObject(workflowsStore.workflow); const workflowObject = createTestWorkflowObject(workflowsStore.workflow);
workflowsStore.getCurrentWorkflow.mockReturnValue(workflowObject); workflowsStore.workflowObject = workflowObject;
workflowsStore.cloneWorkflowObject = vi.fn().mockReturnValue(workflowObject);
nodeTypesStore.getNodeType = vi.fn( nodeTypesStore.getNodeType = vi.fn(
(nodeTypeName: string) => (nodeTypeName: string) =>
({ ({
@@ -1728,7 +1732,8 @@ describe('useCanvasOperations', () => {
}; };
const workflowObject = createTestWorkflowObject(workflowsStore.workflow); const workflowObject = createTestWorkflowObject(workflowsStore.workflow);
workflowsStore.getCurrentWorkflow.mockReturnValue(workflowObject); workflowsStore.workflowObject = workflowObject;
workflowsStore.cloneWorkflowObject = vi.fn().mockReturnValue(workflowObject);
const { isConnectionAllowed, editableWorkflowObject } = useCanvasOperations(); const { isConnectionAllowed, editableWorkflowObject } = useCanvasOperations();
@@ -1779,7 +1784,8 @@ describe('useCanvasOperations', () => {
}; };
const workflowObject = createTestWorkflowObject(workflowsStore.workflow); const workflowObject = createTestWorkflowObject(workflowsStore.workflow);
workflowsStore.getCurrentWorkflow.mockReturnValue(workflowObject); workflowsStore.workflowObject = workflowObject;
workflowsStore.cloneWorkflowObject = vi.fn().mockReturnValue(workflowObject);
const { isConnectionAllowed, editableWorkflowObject } = useCanvasOperations(); const { isConnectionAllowed, editableWorkflowObject } = useCanvasOperations();
@@ -1839,7 +1845,8 @@ describe('useCanvasOperations', () => {
}; };
const workflowObject = createTestWorkflowObject(workflowsStore.workflow); const workflowObject = createTestWorkflowObject(workflowsStore.workflow);
workflowsStore.getCurrentWorkflow.mockReturnValue(workflowObject); workflowsStore.workflowObject = workflowObject;
workflowsStore.cloneWorkflowObject = vi.fn().mockReturnValue(workflowObject);
const { isConnectionAllowed, editableWorkflowObject } = useCanvasOperations(); const { isConnectionAllowed, editableWorkflowObject } = useCanvasOperations();
@@ -1899,7 +1906,8 @@ describe('useCanvasOperations', () => {
}; };
const workflowObject = createTestWorkflowObject(workflowsStore.workflow); const workflowObject = createTestWorkflowObject(workflowsStore.workflow);
workflowsStore.getCurrentWorkflow.mockReturnValue(workflowObject); workflowsStore.workflowObject = workflowObject;
workflowsStore.cloneWorkflowObject = vi.fn().mockReturnValue(workflowObject);
const { isConnectionAllowed, editableWorkflowObject } = useCanvasOperations(); const { isConnectionAllowed, editableWorkflowObject } = useCanvasOperations();
@@ -1959,7 +1967,8 @@ describe('useCanvasOperations', () => {
}; };
const workflowObject = createTestWorkflowObject(workflowsStore.workflow); const workflowObject = createTestWorkflowObject(workflowsStore.workflow);
workflowsStore.getCurrentWorkflow.mockReturnValue(workflowObject); workflowsStore.workflowObject = workflowObject;
workflowsStore.cloneWorkflowObject = vi.fn().mockReturnValue(workflowObject);
const { isConnectionAllowed, editableWorkflowObject } = useCanvasOperations(); const { isConnectionAllowed, editableWorkflowObject } = useCanvasOperations();
@@ -2020,7 +2029,8 @@ describe('useCanvasOperations', () => {
}; };
const workflowObject = createTestWorkflowObject(workflowsStore.workflow); const workflowObject = createTestWorkflowObject(workflowsStore.workflow);
workflowsStore.getCurrentWorkflow.mockReturnValue(workflowObject); workflowsStore.workflowObject = workflowObject;
workflowsStore.cloneWorkflowObject = vi.fn().mockReturnValue(workflowObject);
const { isConnectionAllowed, editableWorkflowObject } = useCanvasOperations(); const { isConnectionAllowed, editableWorkflowObject } = useCanvasOperations();
@@ -2078,7 +2088,8 @@ describe('useCanvasOperations', () => {
}; };
const workflowObject = createTestWorkflowObject(workflowsStore.workflow); const workflowObject = createTestWorkflowObject(workflowsStore.workflow);
workflowsStore.getCurrentWorkflow.mockReturnValue(workflowObject); workflowsStore.workflowObject = workflowObject;
workflowsStore.cloneWorkflowObject = vi.fn().mockReturnValue(workflowObject);
const { isConnectionAllowed, editableWorkflowObject } = useCanvasOperations(); const { isConnectionAllowed, editableWorkflowObject } = useCanvasOperations();
@@ -2121,7 +2132,7 @@ describe('useCanvasOperations', () => {
}; };
const workflowObject = createTestWorkflowObject(workflowsStore.workflow); const workflowObject = createTestWorkflowObject(workflowsStore.workflow);
workflowsStore.getCurrentWorkflow.mockReturnValue(workflowObject); workflowsStore.workflowObject = workflowObject;
const { isConnectionAllowed, editableWorkflowObject } = useCanvasOperations(); const { isConnectionAllowed, editableWorkflowObject } = useCanvasOperations();
@@ -2294,7 +2305,7 @@ describe('useCanvasOperations', () => {
.mockReturnValueOnce(sourceNodeType); .mockReturnValueOnce(sourceNodeType);
const workflowObject = createTestWorkflowObject(workflowsStore.workflow); const workflowObject = createTestWorkflowObject(workflowsStore.workflow);
workflowsStore.getCurrentWorkflow.mockReturnValue(workflowObject); workflowsStore.workflowObject = workflowObject;
const { revalidateNodeInputConnections } = useCanvasOperations(); const { revalidateNodeInputConnections } = useCanvasOperations();
revalidateNodeInputConnections(targetNodeId); revalidateNodeInputConnections(targetNodeId);
@@ -2358,7 +2369,7 @@ describe('useCanvasOperations', () => {
.mockReturnValueOnce(sourceNodeType); .mockReturnValueOnce(sourceNodeType);
const workflowObject = createTestWorkflowObject(workflowsStore.workflow); const workflowObject = createTestWorkflowObject(workflowsStore.workflow);
workflowsStore.getCurrentWorkflow.mockReturnValue(workflowObject); workflowsStore.workflowObject = workflowObject;
const { revalidateNodeInputConnections } = useCanvasOperations(); const { revalidateNodeInputConnections } = useCanvasOperations();
revalidateNodeInputConnections(targetNodeId); revalidateNodeInputConnections(targetNodeId);
@@ -2443,7 +2454,7 @@ describe('useCanvasOperations', () => {
.mockReturnValueOnce(sourceNodeType); .mockReturnValueOnce(sourceNodeType);
const workflowObject = createTestWorkflowObject(workflowsStore.workflow); const workflowObject = createTestWorkflowObject(workflowsStore.workflow);
workflowsStore.getCurrentWorkflow.mockReturnValue(workflowObject); workflowsStore.workflowObject = workflowObject;
const { revalidateNodeOutputConnections } = useCanvasOperations(); const { revalidateNodeOutputConnections } = useCanvasOperations();
revalidateNodeOutputConnections(sourceNodeId); revalidateNodeOutputConnections(sourceNodeId);
@@ -2507,7 +2518,7 @@ describe('useCanvasOperations', () => {
.mockReturnValueOnce(sourceNodeType); .mockReturnValueOnce(sourceNodeType);
const workflowObject = createTestWorkflowObject(workflowsStore.workflow); const workflowObject = createTestWorkflowObject(workflowsStore.workflow);
workflowsStore.getCurrentWorkflow.mockReturnValue(workflowObject); workflowsStore.workflowObject = workflowObject;
const { revalidateNodeOutputConnections } = useCanvasOperations(); const { revalidateNodeOutputConnections } = useCanvasOperations();
revalidateNodeOutputConnections(sourceNodeId); revalidateNodeOutputConnections(sourceNodeId);
@@ -2659,13 +2670,13 @@ describe('useCanvasOperations', () => {
}; };
const nodes = buildImportNodes(); const nodes = buildImportNodes();
workflowsStore.workflow.nodes = nodes; workflowsStore.setNodes(nodes);
workflowsStore.getNodesByIds.mockReturnValue(nodes); workflowsStore.getNodesByIds.mockReturnValue(nodes);
workflowsStore.outgoingConnectionsByNodeName.mockReturnValue({}); workflowsStore.outgoingConnectionsByNodeName.mockReturnValue({});
const workflowObject = createTestWorkflowObject(workflowsStore.workflow); const workflowObject = createTestWorkflowObject(workflowsStore.workflow);
workflowsStore.getCurrentWorkflow.mockReturnValue(workflowObject); workflowsStore.workflowObject = workflowObject;
workflowsStore.getWorkflow.mockReturnValue(workflowObject); workflowsStore.createWorkflowObject.mockReturnValue(workflowObject);
const canvasOperations = useCanvasOperations(); const canvasOperations = useCanvasOperations();
const duplicatedNodeIds = await canvasOperations.duplicateNodes(['1', '2']); const duplicatedNodeIds = await canvasOperations.duplicateNodes(['1', '2']);
@@ -3176,7 +3187,7 @@ describe('useCanvasOperations', () => {
// Mock store methods // Mock store methods
const workflowObject = createTestWorkflowObject(workflowsStore.workflow); const workflowObject = createTestWorkflowObject(workflowsStore.workflow);
workflowsStore.getCurrentWorkflow.mockReturnValue(workflowObject); workflowsStore.workflowObject = workflowObject;
workflowsStore.getNodeById.mockImplementation( workflowsStore.getNodeById.mockImplementation(
(id: string) => (id: string) =>
({ ({
@@ -3246,7 +3257,7 @@ describe('useCanvasOperations', () => {
// Mock store methods // Mock store methods
const workflowObject = createTestWorkflowObject(workflowsStore.workflow); const workflowObject = createTestWorkflowObject(workflowsStore.workflow);
workflowsStore.getCurrentWorkflow.mockReturnValue(workflowObject); workflowsStore.workflowObject = workflowObject;
workflowsStore.getNodeById.mockImplementation( workflowsStore.getNodeById.mockImplementation(
(id: string) => (id: string) =>
({ ({
@@ -3300,7 +3311,7 @@ describe('useCanvasOperations', () => {
}; };
const workflowObject = createTestWorkflowObject(workflowsStore.workflow); const workflowObject = createTestWorkflowObject(workflowsStore.workflow);
workflowsStore.getCurrentWorkflow.mockReturnValue(workflowObject); workflowsStore.workflowObject = workflowObject;
workflowsStore.getNodeById.mockReturnValue(nodeB); workflowsStore.getNodeById.mockReturnValue(nodeB);
workflowsStore.outgoingConnectionsByNodeName.mockReturnValue({ workflowsStore.outgoingConnectionsByNodeName.mockReturnValue({
main: [[{ node: nodeC.name, type: NodeConnectionTypes.Main, index: 0 }]], main: [[{ node: nodeC.name, type: NodeConnectionTypes.Main, index: 0 }]],
@@ -3328,7 +3339,7 @@ describe('useCanvasOperations', () => {
}; };
const workflowObject = createTestWorkflowObject(workflowsStore.workflow); const workflowObject = createTestWorkflowObject(workflowsStore.workflow);
workflowsStore.getCurrentWorkflow.mockReturnValue(workflowObject); workflowsStore.workflowObject = workflowObject;
workflowsStore.getNodeById.mockReturnValue(nodeB); workflowsStore.getNodeById.mockReturnValue(nodeB);
workflowsStore.outgoingConnectionsByNodeName.mockReturnValue({}); workflowsStore.outgoingConnectionsByNodeName.mockReturnValue({});
workflowsStore.incomingConnectionsByNodeName.mockReturnValue({ workflowsStore.incomingConnectionsByNodeName.mockReturnValue({
@@ -3354,7 +3365,7 @@ describe('useCanvasOperations', () => {
})); }));
const workflowObject = createTestWorkflowObject(workflowsStore.workflow); const workflowObject = createTestWorkflowObject(workflowsStore.workflow);
workflowsStore.getCurrentWorkflow.mockReturnValue(workflowObject); workflowsStore.workflowObject = workflowObject;
// Create nodes: A -> B (no outgoing from B) // Create nodes: A -> B (no outgoing from B)
const nodeA: IWorkflowTemplateNode = createTestNode({ const nodeA: IWorkflowTemplateNode = createTestNode({
@@ -3611,7 +3622,7 @@ describe('useCanvasOperations', () => {
}); });
it('should replace connections for a node and track history', () => { it('should replace connections for a node and track history', () => {
const workflowObject = createTestWorkflowObject(workflowsStore.workflow); const workflowObject = createTestWorkflowObject(workflowsStore.workflow);
workflowsStore.getCurrentWorkflow.mockReturnValue(workflowObject); workflowsStore.workflowObject = workflowObject;
const { replaceNodeConnections } = useCanvasOperations(); const { replaceNodeConnections } = useCanvasOperations();
replaceNodeConnections(targetNode.id, replacementNode.id, { trackHistory: true }); replaceNodeConnections(targetNode.id, replacementNode.id, { trackHistory: true });
@@ -3709,7 +3720,7 @@ describe('useCanvasOperations', () => {
it('should replace connections without tracking history', () => { it('should replace connections without tracking history', () => {
const workflowObject = createTestWorkflowObject(workflowsStore.workflow); const workflowObject = createTestWorkflowObject(workflowsStore.workflow);
workflowsStore.getCurrentWorkflow.mockReturnValue(workflowObject); workflowsStore.workflowObject = workflowObject;
const { replaceNodeConnections } = useCanvasOperations(); const { replaceNodeConnections } = useCanvasOperations();
replaceNodeConnections(targetNode.id, replacementNode.id, { trackHistory: false }); replaceNodeConnections(targetNode.id, replacementNode.id, { trackHistory: false });
@@ -3722,7 +3733,7 @@ describe('useCanvasOperations', () => {
it('should not replace connections if previous node does not exist', () => { it('should not replace connections if previous node does not exist', () => {
const workflowObject = createTestWorkflowObject(workflowsStore.workflow); const workflowObject = createTestWorkflowObject(workflowsStore.workflow);
workflowsStore.getCurrentWorkflow.mockReturnValue(workflowObject); workflowsStore.workflowObject = workflowObject;
const { replaceNodeConnections } = useCanvasOperations(); const { replaceNodeConnections } = useCanvasOperations();
replaceNodeConnections('nonexistent', replacementNode.id); replaceNodeConnections('nonexistent', replacementNode.id);
@@ -3733,7 +3744,7 @@ describe('useCanvasOperations', () => {
it('should not replace connections if new node does not exist', () => { it('should not replace connections if new node does not exist', () => {
const workflowObject = createTestWorkflowObject(workflowsStore.workflow); const workflowObject = createTestWorkflowObject(workflowsStore.workflow);
workflowsStore.getCurrentWorkflow.mockReturnValue(workflowObject); workflowsStore.workflowObject = workflowObject;
const { replaceNodeConnections } = useCanvasOperations(); const { replaceNodeConnections } = useCanvasOperations();
replaceNodeConnections(targetNode.id, 'nonexistent'); replaceNodeConnections(targetNode.id, 'nonexistent');
@@ -3744,7 +3755,7 @@ describe('useCanvasOperations', () => {
it('should respect replaceInputs being false', () => { it('should respect replaceInputs being false', () => {
const workflowObject = createTestWorkflowObject(workflowsStore.workflow); const workflowObject = createTestWorkflowObject(workflowsStore.workflow);
workflowsStore.getCurrentWorkflow.mockReturnValue(workflowObject); workflowsStore.workflowObject = workflowObject;
const { replaceNodeConnections } = useCanvasOperations(); const { replaceNodeConnections } = useCanvasOperations();
// nextNode only has an input connection // nextNode only has an input connection
@@ -3759,7 +3770,7 @@ describe('useCanvasOperations', () => {
it('should respect replaceOutputs being false', () => { it('should respect replaceOutputs being false', () => {
const workflowObject = createTestWorkflowObject(workflowsStore.workflow); const workflowObject = createTestWorkflowObject(workflowsStore.workflow);
workflowsStore.getCurrentWorkflow.mockReturnValue(workflowObject); workflowsStore.workflowObject = workflowObject;
const { replaceNodeConnections } = useCanvasOperations(); const { replaceNodeConnections } = useCanvasOperations();
// sourceNode only has an output connection // sourceNode only has an output connection
@@ -3833,7 +3844,7 @@ describe('useCanvasOperations', () => {
}); });
const workflowObject = createTestWorkflowObject(workflowsStore.workflow); const workflowObject = createTestWorkflowObject(workflowsStore.workflow);
workflowsStore.getCurrentWorkflow.mockReturnValue(workflowObject); workflowsStore.workflowObject = workflowObject;
const { replaceNodeConnections } = useCanvasOperations(); const { replaceNodeConnections } = useCanvasOperations();
replaceNodeConnections(previousNode1.id, newNode1.id, { replaceNodeConnections(previousNode1.id, newNode1.id, {

View File

@@ -171,8 +171,8 @@ export function useCanvasOperations() {
const preventOpeningNDV = !!localStorage.getItem('NodeView.preventOpeningNDV'); const preventOpeningNDV = !!localStorage.getItem('NodeView.preventOpeningNDV');
const editableWorkflow = computed(() => workflowsStore.workflow); const editableWorkflow = computed<IWorkflowDb>(() => workflowsStore.workflow);
const editableWorkflowObject = computed(() => workflowsStore.getCurrentWorkflow()); const editableWorkflowObject = computed(() => workflowsStore.workflowObject as Workflow);
const triggerNodes = computed<INodeUi[]>(() => { const triggerNodes = computed<INodeUi[]>(() => {
return workflowsStore.workflowTriggerNodes; return workflowsStore.workflowTriggerNodes;
@@ -298,7 +298,7 @@ export function useCanvasOperations() {
newName = uniqueNodeName(newName); newName = uniqueNodeName(newName);
// Rename the node and update the connections // Rename the node and update the connections
const workflow = workflowsStore.getCurrentWorkflow(true); const workflow = workflowsStore.cloneWorkflowObject();
try { try {
workflow.renameNode(currentName, newName); workflow.renameNode(currentName, newName);
} catch (error) { } catch (error) {
@@ -477,17 +477,17 @@ export function useCanvasOperations() {
if (!previousNode || !newNode) { if (!previousNode || !newNode) {
return; return;
} }
const wf = workflowsStore.getCurrentWorkflow(); const workflowObject = workflowsStore.workflowObject;
const inputNodeNames = replaceInputs const inputNodeNames = replaceInputs
? uniq(wf.getParentNodes(previousNode.name, 'main', 1)) ? uniq(workflowObject.getParentNodes(previousNode.name, 'main', 1))
: []; : [];
const outputNodeNames = replaceOutputs const outputNodeNames = replaceOutputs
? uniq(wf.getChildNodes(previousNode.name, 'main', 1)) ? uniq(workflowObject.getChildNodes(previousNode.name, 'main', 1))
: []; : [];
const connectionPairs = [ const connectionPairs = [
...wf.getConnectionsBetweenNodes(inputNodeNames, [previousNode.name]), ...workflowObject.getConnectionsBetweenNodes(inputNodeNames, [previousNode.name]),
...wf.getConnectionsBetweenNodes([previousNode.name], outputNodeNames), ...workflowObject.getConnectionsBetweenNodes([previousNode.name], outputNodeNames),
]; ];
if (trackHistory && trackBulk) { if (trackHistory && trackBulk) {
@@ -1768,7 +1768,7 @@ export function useCanvasOperations() {
// Create a workflow with the new nodes and connections that we can use // Create a workflow with the new nodes and connections that we can use
// the rename method // the rename method
const tempWorkflow: Workflow = workflowsStore.getWorkflow(createNodes, newConnections); const tempWorkflow: Workflow = workflowsStore.createWorkflowObject(createNodes, newConnections);
// Rename all the nodes of which the name changed // Rename all the nodes of which the name changed
for (oldName in nodeNameTable) { for (oldName in nodeNameTable) {
@@ -1875,7 +1875,7 @@ export function useCanvasOperations() {
// Generate new webhookId if workflow already contains a node with the same webhookId // Generate new webhookId if workflow already contains a node with the same webhookId
if (node.webhookId && UPDATE_WEBHOOK_ID_NODE_TYPES.includes(node.type)) { if (node.webhookId && UPDATE_WEBHOOK_ID_NODE_TYPES.includes(node.type)) {
const isDuplicate = Object.values(workflowHelpers.getCurrentWorkflow().nodes).some( const isDuplicate = Object.values(workflowsStore.workflowObject.nodes).some(
(n) => n.webhookId === node.webhookId, (n) => n.webhookId === node.webhookId,
); );
if (isDuplicate) { if (isDuplicate) {
@@ -2187,12 +2187,12 @@ export function useCanvasOperations() {
return; return;
} }
const workflow = workflowsStore.getCurrentWorkflow(); const workflowObject = workflowsStore.workflowObject; // @TODO Check if we actually need workflowObject here
logsStore.toggleOpen(true); logsStore.toggleOpen(true);
const payload = { const payload = {
workflow_id: workflow.id, workflow_id: workflowObject.id,
button_type: source, button_type: source,
}; };

View File

@@ -43,12 +43,12 @@ describe('useContextMenu', () => {
workflowsStore = useWorkflowsStore(); workflowsStore = useWorkflowsStore();
workflowsStore.workflow.nodes = nodes; workflowsStore.workflow.nodes = nodes;
workflowsStore.workflow.scopes = ['workflow:update']; workflowsStore.workflow.scopes = ['workflow:update'];
vi.spyOn(workflowsStore, 'getCurrentWorkflow').mockReturnValue({ workflowsStore.workflowObject = {
nodes, nodes,
getNode: (_: string) => { getNode: (_: string) => {
return {}; return {};
}, },
} as never); } as never;
vi.spyOn(NodeHelpers, 'getNodeInputs').mockReturnValue([]); vi.spyOn(NodeHelpers, 'getNodeInputs').mockReturnValue([]);
vi.spyOn(NodeHelpers, 'isExecutable').mockReturnValue(true); vi.spyOn(NodeHelpers, 'isExecutable').mockReturnValue(true);

View File

@@ -4,7 +4,7 @@ import { useNodeTypesStore } from '@/stores/nodeTypes.store';
import { useSourceControlStore } from '@/stores/sourceControl.store'; import { useSourceControlStore } from '@/stores/sourceControl.store';
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 type { INode, INodeTypeDescription } from 'n8n-workflow'; import type { INode, INodeTypeDescription, Workflow } from 'n8n-workflow';
import { NodeHelpers } from 'n8n-workflow'; import { NodeHelpers } from 'n8n-workflow';
import { computed, ref, watch } from 'vue'; import { computed, ref, watch } from 'vue';
import { getMousePosition } from '../utils/nodeViewUtils'; import { getMousePosition } from '../utils/nodeViewUtils';
@@ -50,6 +50,8 @@ export const useContextMenu = (onAction: ContextMenuActionCallback = () => {}) =
const sourceControlStore = useSourceControlStore(); const sourceControlStore = useSourceControlStore();
const i18n = useI18n(); const i18n = useI18n();
const workflowObject = computed(() => workflowsStore.workflowObject as Workflow);
const workflowPermissions = computed( const workflowPermissions = computed(
() => getResourcePermissions(workflowsStore.workflow.scopes).workflow, () => getResourcePermissions(workflowsStore.workflow.scopes).workflow,
); );
@@ -108,13 +110,12 @@ export const useContextMenu = (onAction: ContextMenuActionCallback = () => {}) =
}; };
const isExecutable = (node: INodeUi) => { const isExecutable = (node: INodeUi) => {
const currentWorkflow = workflowsStore.getCurrentWorkflow(); const workflowNode = workflowObject.value.getNode(node.name) as INode;
const workflowNode = currentWorkflow.getNode(node.name) as INode;
const nodeType = nodeTypesStore.getNodeType( const nodeType = nodeTypesStore.getNodeType(
workflowNode.type, workflowNode.type,
workflowNode.typeVersion, workflowNode.typeVersion,
) as INodeTypeDescription; ) as INodeTypeDescription;
return NodeHelpers.isExecutable(currentWorkflow, workflowNode, nodeType); return NodeHelpers.isExecutable(workflowObject.value, workflowNode, nodeType);
}; };
const open = (event: MouseEvent, menuTarget: ContextMenuTarget) => { const open = (event: MouseEvent, menuTarget: ContextMenuTarget) => {

View File

@@ -40,10 +40,10 @@ describe('useExecutionDebugging()', () => {
const workflowStore = mockedStore(useWorkflowsStore); const workflowStore = mockedStore(useWorkflowsStore);
workflowStore.getNodes.mockReturnValue([{ name: 'testNode' }] as INodeUi[]); workflowStore.getNodes.mockReturnValue([{ name: 'testNode' }] as INodeUi[]);
workflowStore.getExecution.mockResolvedValueOnce(mockExecution); workflowStore.getExecution.mockResolvedValueOnce(mockExecution);
workflowStore.getCurrentWorkflow.mockReturnValue({ workflowStore.workflowObject = {
pinData: {}, pinData: {},
getParentNodes: vi.fn().mockReturnValue([]), getParentNodes: vi.fn().mockReturnValue([]),
} as unknown as Workflow); } as unknown as Workflow;
await expect(executionDebugging.applyExecutionData('1')).resolves.not.toThrowError(); await expect(executionDebugging.applyExecutionData('1')).resolves.not.toThrowError();
}); });
@@ -66,10 +66,10 @@ describe('useExecutionDebugging()', () => {
const workflowStore = mockedStore(useWorkflowsStore); const workflowStore = mockedStore(useWorkflowsStore);
workflowStore.getNodes.mockReturnValue([{ name: 'testNode2' }] as INodeUi[]); workflowStore.getNodes.mockReturnValue([{ name: 'testNode2' }] as INodeUi[]);
workflowStore.getExecution.mockResolvedValueOnce(mockExecution); workflowStore.getExecution.mockResolvedValueOnce(mockExecution);
workflowStore.getCurrentWorkflow.mockReturnValue({ workflowStore.workflowObject = {
pinData: {}, pinData: {},
getParentNodes: vi.fn().mockReturnValue([]), getParentNodes: vi.fn().mockReturnValue([]),
} as unknown as Workflow); } as unknown as Workflow;
await executionDebugging.applyExecutionData('1'); await executionDebugging.applyExecutionData('1');
@@ -96,10 +96,10 @@ describe('useExecutionDebugging()', () => {
const workflowStore = mockedStore(useWorkflowsStore); const workflowStore = mockedStore(useWorkflowsStore);
workflowStore.getNodes.mockReturnValue([{ name: 'testNode' }] as INodeUi[]); workflowStore.getNodes.mockReturnValue([{ name: 'testNode' }] as INodeUi[]);
workflowStore.getExecution.mockResolvedValueOnce(mockExecution); workflowStore.getExecution.mockResolvedValueOnce(mockExecution);
workflowStore.getCurrentWorkflow.mockReturnValue({ workflowStore.workflowObject = {
pinData: {}, pinData: {},
getParentNodes: vi.fn().mockReturnValue([]), getParentNodes: vi.fn().mockReturnValue([]),
} as unknown as Workflow); } as unknown as Workflow;
await executionDebugging.applyExecutionData('1'); await executionDebugging.applyExecutionData('1');

View File

@@ -38,7 +38,7 @@ export const useExecutionDebugging = () => {
const applyExecutionData = async (executionId: string): Promise<void> => { const applyExecutionData = async (executionId: string): Promise<void> => {
const execution = await workflowsStore.getExecution(executionId); const execution = await workflowsStore.getExecution(executionId);
const workflow = workflowsStore.getCurrentWorkflow(); const workflowObject = workflowsStore.workflowObject;
const workflowNodes = workflowsStore.getNodes(); const workflowNodes = workflowsStore.getNodes();
if (!execution?.data?.resultData) { if (!execution?.data?.resultData) {
@@ -91,7 +91,7 @@ export const useExecutionDebugging = () => {
} else { } else {
await router.push({ await router.push({
name: VIEWS.EXECUTION_PREVIEW, name: VIEWS.EXECUTION_PREVIEW,
params: { name: workflow.id, executionId }, params: { name: workflowObject.id, executionId },
}); });
return; return;
} }
@@ -103,7 +103,7 @@ export const useExecutionDebugging = () => {
// Pin data of all nodes which do not have a parent node // Pin data of all nodes which do not have a parent node
const pinnableNodes = workflowNodes.filter( const pinnableNodes = workflowNodes.filter(
(node: INodeUi) => !workflow.getParentNodes(node.name).length, (node: INodeUi) => !workflowObject.getParentNodes(node.name).length,
); );
let pinnings = 0; let pinnings = 0;

View File

@@ -48,12 +48,12 @@ describe('useNodeHelpers()', () => {
parameters: {}, parameters: {},
}; };
const mockWorkflow = { const mockWorkflow = mock<Workflow>({
id: 'workflow-id', id: 'workflow-id',
getNode: () => node, getNode: () => node,
}; });
mockedStore(useWorkflowsStore).getCurrentWorkflow = vi.fn().mockReturnValue(mockWorkflow); mockedStore(useWorkflowsStore).workflowObject = mockWorkflow;
mockedStore(useNodeTypesStore).getNodeType = vi.fn().mockReturnValue({}); mockedStore(useNodeTypesStore).getNodeType = vi.fn().mockReturnValue({});
mockedStore(useNodeTypesStore).isTriggerNode = vi.fn().mockReturnValue(false); mockedStore(useNodeTypesStore).isTriggerNode = vi.fn().mockReturnValue(false);
mockedStore(useNodeTypesStore).isToolNode = vi.fn().mockReturnValue(false); mockedStore(useNodeTypesStore).isToolNode = vi.fn().mockReturnValue(false);
@@ -76,11 +76,11 @@ describe('useNodeHelpers()', () => {
parameters: {}, parameters: {},
}; };
const mockWorkflow = { const mockWorkflow = mock<Workflow>({
getNode: () => node, getNode: () => node,
}; });
mockedStore(useWorkflowsStore).getCurrentWorkflow = vi.fn().mockReturnValue(mockWorkflow); mockedStore(useWorkflowsStore).workflowObject = mockWorkflow;
mockedStore(useNodeTypesStore).getNodeType = vi.fn().mockReturnValue({}); mockedStore(useNodeTypesStore).getNodeType = vi.fn().mockReturnValue({});
mockedStore(useNodeTypesStore).isTriggerNode = vi.fn().mockReturnValue(false); mockedStore(useNodeTypesStore).isTriggerNode = vi.fn().mockReturnValue(false);
mockedStore(useNodeTypesStore).isToolNode = vi.fn().mockReturnValue(false); mockedStore(useNodeTypesStore).isToolNode = vi.fn().mockReturnValue(false);
@@ -103,11 +103,11 @@ describe('useNodeHelpers()', () => {
parameters: {}, parameters: {},
}; };
const mockWorkflow = { const mockWorkflow = mock<Workflow>({
getNode: () => node, getNode: () => node,
}; });
mockedStore(useWorkflowsStore).getCurrentWorkflow = vi.fn().mockReturnValue(mockWorkflow); mockedStore(useWorkflowsStore).workflowObject = mockWorkflow;
mockedStore(useNodeTypesStore).getNodeType = vi.fn().mockReturnValue({}); mockedStore(useNodeTypesStore).getNodeType = vi.fn().mockReturnValue({});
mockedStore(useNodeTypesStore).isTriggerNode = vi.fn().mockReturnValue(false); mockedStore(useNodeTypesStore).isTriggerNode = vi.fn().mockReturnValue(false);
mockedStore(useNodeTypesStore).isToolNode = vi.fn().mockReturnValue(false); mockedStore(useNodeTypesStore).isToolNode = vi.fn().mockReturnValue(false);
@@ -130,11 +130,11 @@ describe('useNodeHelpers()', () => {
parameters: {}, parameters: {},
}; };
const mockWorkflow = { const mockWorkflow = mock<Workflow>({
getNode: () => triggerNode, getNode: () => triggerNode,
}; });
mockedStore(useWorkflowsStore).getCurrentWorkflow = vi.fn().mockReturnValue(mockWorkflow); mockedStore(useWorkflowsStore).workflowObject = mockWorkflow;
mockedStore(useNodeTypesStore).getNodeType = vi.fn().mockReturnValue({}); mockedStore(useNodeTypesStore).getNodeType = vi.fn().mockReturnValue({});
mockedStore(useNodeTypesStore).isTriggerNode = vi.fn().mockReturnValue(true); mockedStore(useNodeTypesStore).isTriggerNode = vi.fn().mockReturnValue(true);
mockedStore(useNodeTypesStore).isToolNode = vi.fn().mockReturnValue(false); mockedStore(useNodeTypesStore).isToolNode = vi.fn().mockReturnValue(false);
@@ -157,11 +157,11 @@ describe('useNodeHelpers()', () => {
parameters: {}, parameters: {},
}; };
const mockWorkflow = { const mockWorkflow = mock<Workflow>({
getNode: () => toolNode, getNode: () => toolNode,
}; });
mockedStore(useWorkflowsStore).getCurrentWorkflow = vi.fn().mockReturnValue(mockWorkflow); mockedStore(useWorkflowsStore).workflowObject = mockWorkflow;
mockedStore(useNodeTypesStore).getNodeType = vi.fn().mockReturnValue({}); mockedStore(useNodeTypesStore).getNodeType = vi.fn().mockReturnValue({});
mockedStore(useNodeTypesStore).isTriggerNode = vi.fn().mockReturnValue(false); mockedStore(useNodeTypesStore).isTriggerNode = vi.fn().mockReturnValue(false);
mockedStore(useNodeTypesStore).isToolNode = vi.fn().mockReturnValue(true); mockedStore(useNodeTypesStore).isToolNode = vi.fn().mockReturnValue(true);
@@ -184,11 +184,11 @@ describe('useNodeHelpers()', () => {
parameters: {}, parameters: {},
}; };
const mockWorkflow = { const mockWorkflow = mock<Workflow>({
getNode: () => node, getNode: () => node,
}; });
mockedStore(useWorkflowsStore).getCurrentWorkflow = vi.fn().mockReturnValue(mockWorkflow); mockedStore(useWorkflowsStore).workflowObject = mockWorkflow;
mockedStore(useNodeTypesStore).getNodeType = vi.fn().mockReturnValue({}); mockedStore(useNodeTypesStore).getNodeType = vi.fn().mockReturnValue({});
mockedStore(useNodeTypesStore).isTriggerNode = vi.fn().mockReturnValue(false); mockedStore(useNodeTypesStore).isTriggerNode = vi.fn().mockReturnValue(false);
mockedStore(useNodeTypesStore).isToolNode = vi.fn().mockReturnValue(false); mockedStore(useNodeTypesStore).isToolNode = vi.fn().mockReturnValue(false);

View File

@@ -1,4 +1,4 @@
import { ref } from 'vue'; import { ref, computed } from 'vue';
import { useHistoryStore } from '@/stores/history.store'; import { useHistoryStore } from '@/stores/history.store';
import { import {
CUSTOM_API_CALL_KEY, CUSTOM_API_CALL_KEY,
@@ -76,6 +76,8 @@ export function useNodeHelpers() {
const isProductionExecutionPreview = ref(false); const isProductionExecutionPreview = ref(false);
const pullConnActiveNodeName = ref<string | null>(null); const pullConnActiveNodeName = ref<string | null>(null);
const workflowObject = computed(() => workflowsStore.workflowObject as Workflow);
function hasProxyAuth(node: INodeUi): boolean { function hasProxyAuth(node: INodeUi): boolean {
return Object.keys(node.parameters).includes('nodeCredentialType'); return Object.keys(node.parameters).includes('nodeCredentialType');
} }
@@ -117,14 +119,13 @@ export function useNodeHelpers() {
): boolean { ): boolean {
const nodeType = node ? nodeTypesStore.getNodeType(node.type, node.typeVersion) : null; const nodeType = node ? nodeTypesStore.getNodeType(node.type, node.typeVersion) : null;
if (node && nodeType) { if (node && nodeType) {
const currentWorkflowInstance = workflowsStore.getCurrentWorkflow(); const workflowNode = workflowObject.value.getNode(node.name);
const workflowNode = currentWorkflowInstance.getNode(node.name);
const isTriggerNode = !!node && nodeTypesStore.isTriggerNode(node.type); const isTriggerNode = !!node && nodeTypesStore.isTriggerNode(node.type);
const isToolNode = !!node && nodeTypesStore.isToolNode(node.type); const isToolNode = !!node && nodeTypesStore.isToolNode(node.type);
if (workflowNode) { if (workflowNode) {
const inputs = NodeHelpers.getNodeInputs(currentWorkflowInstance, workflowNode, nodeType); const inputs = NodeHelpers.getNodeInputs(workflowObject.value, workflowNode, nodeType);
const inputNames = NodeHelpers.getConnectionTypes(inputs); const inputNames = NodeHelpers.getConnectionTypes(inputs);
if (!inputNames.includes(NodeConnectionTypes.Main) && !isToolNode && !isTriggerNode) { if (!inputNames.includes(NodeConnectionTypes.Main) && !isToolNode && !isTriggerNode) {
@@ -279,8 +280,7 @@ export function useNodeHelpers() {
return; return;
} }
const workflow = workflowsStore.getCurrentWorkflow(); const nodeInputIssues = getNodeInputIssues(workflowObject.value, node, nodeType);
const nodeInputIssues = getNodeInputIssues(workflow, node, nodeType);
workflowsStore.setNodeIssue({ workflowsStore.setNodeIssue({
node: node.name, node: node.name,

View File

@@ -126,7 +126,7 @@ export function useNodeSettingsParameters() {
const updatedConnections = updateDynamicConnections(node, connections, parameterData); const updatedConnections = updateDynamicConnections(node, connections, parameterData);
if (updatedConnections) { if (updatedConnections) {
workflowsStore.setConnections(updatedConnections, true); workflowsStore.setConnections(updatedConnections);
} }
workflowsStore.setNodeParameters(updateInformation); workflowsStore.setNodeParameters(updateInformation);

View File

@@ -1,6 +1,6 @@
import { useToast } from '@/composables/useToast'; import { useToast } from '@/composables/useToast';
import { useI18n } from '@n8n/i18n'; import { useI18n } from '@n8n/i18n';
import type { INodeExecutionData, IPinData } from 'n8n-workflow'; import type { INodeExecutionData, IPinData, Workflow } from 'n8n-workflow';
import { jsonParse, jsonStringify, NodeConnectionTypes, NodeHelpers } from 'n8n-workflow'; import { jsonParse, jsonStringify, NodeConnectionTypes, NodeHelpers } from 'n8n-workflow';
import { import {
MAX_EXPECTED_REQUEST_SIZE, MAX_EXPECTED_REQUEST_SIZE,
@@ -51,6 +51,8 @@ export function usePinnedData(
const externalHooks = useExternalHooks(); const externalHooks = useExternalHooks();
const { getInputDataWithPinned } = useDataSchema(); const { getInputDataWithPinned } = useDataSchema();
const workflowObject = computed(() => workflowsStore.workflowObject as Workflow);
const { isSubNodeType, isMultipleOutputsNodeType } = useNodeType({ const { isSubNodeType, isMultipleOutputsNodeType } = useNodeType({
node, node,
}); });
@@ -84,9 +86,8 @@ export function usePinnedData(
if (!nodeType || (checkDataEmpty && dataToPin.length === 0)) return false; if (!nodeType || (checkDataEmpty && dataToPin.length === 0)) return false;
const workflow = workflowsStore.getCurrentWorkflow(); const outputs = NodeHelpers.getNodeOutputs(workflowObject.value, targetNode, nodeType).map(
const outputs = NodeHelpers.getNodeOutputs(workflow, targetNode, nodeType).map((output) => (output) => (typeof output === 'string' ? { type: output } : output),
typeof output === 'string' ? { type: output } : output,
); );
const mainOutputs = outputs.filter( const mainOutputs = outputs.filter(
@@ -163,8 +164,8 @@ export function usePinnedData(
if (typeof data === 'object') data = JSON.stringify(data); if (typeof data === 'object') data = JSON.stringify(data);
const { pinData: currentPinData, ...workflow } = workflowsStore.getCurrentWorkflow(); const { pinData: currentPinData, ...workflowObjectWithoutPinData } = workflowObject.value;
const workflowJson = jsonStringify(workflow, { replaceCircularRefs: true }); const workflowJson = jsonStringify(workflowObjectWithoutPinData, { replaceCircularRefs: true });
const newPinData = { ...currentPinData, [targetNode.name]: data }; const newPinData = { ...currentPinData, [targetNode.name]: data };
const newPinDataSize = workflowsStore.getPinDataSize(newPinData); const newPinDataSize = workflowsStore.getPinDataSize(newPinData);

View File

@@ -251,7 +251,7 @@ export function handleExecutionFinishedWithWaitTill(options: {
const settingsStore = useSettingsStore(); const settingsStore = useSettingsStore();
const workflowSaving = useWorkflowSaving(options); const workflowSaving = useWorkflowSaving(options);
const workflowHelpers = useWorkflowHelpers(); const workflowHelpers = useWorkflowHelpers();
const workflowObject = workflowsStore.getCurrentWorkflow(); const workflowObject = workflowsStore.workflowObject;
const workflowSettings = workflowsStore.workflowSettings; const workflowSettings = workflowsStore.workflowSettings;
const saveManualExecutions = const saveManualExecutions =
@@ -285,7 +285,7 @@ export function handleExecutionFinishedWithErrorOrCanceled(
const telemetry = useTelemetry(); const telemetry = useTelemetry();
const workflowsStore = useWorkflowsStore(); const workflowsStore = useWorkflowsStore();
const workflowHelpers = useWorkflowHelpers(); const workflowHelpers = useWorkflowHelpers();
const workflowObject = workflowsStore.getCurrentWorkflow(); const workflowObject = workflowsStore.workflowObject;
workflowHelpers.setDocumentTitle(workflowObject.name as string, 'ERROR'); workflowHelpers.setDocumentTitle(workflowObject.name as string, 'ERROR');
@@ -375,7 +375,7 @@ export function handleExecutionFinishedWithOther(successToastAlreadyShown: boole
const i18n = useI18n(); const i18n = useI18n();
const workflowHelpers = useWorkflowHelpers(); const workflowHelpers = useWorkflowHelpers();
const nodeTypesStore = useNodeTypesStore(); const nodeTypesStore = useNodeTypesStore();
const workflowObject = workflowsStore.getCurrentWorkflow(); const workflowObject = workflowsStore.workflowObject;
workflowHelpers.setDocumentTitle(workflowObject.name as string, 'IDLE'); workflowHelpers.setDocumentTitle(workflowObject.name as string, 'IDLE');

View File

@@ -43,7 +43,7 @@ vi.mock('@/stores/workflows.store', () => {
previousExecutionId: undefined, previousExecutionId: undefined,
nodesIssuesExist: false, nodesIssuesExist: false,
executionWaitingForWebhook: false, executionWaitingForWebhook: false,
getCurrentWorkflow: vi.fn().mockReturnValue({ id: '123' }), workflowObject: { id: '123' } as Workflow,
getNodeByName: vi getNodeByName: vi
.fn() .fn()
.mockImplementation((name) => .mockImplementation((name) =>
@@ -108,7 +108,6 @@ vi.mock('@/composables/useToast', () => ({
vi.mock('@/composables/useWorkflowHelpers', () => ({ vi.mock('@/composables/useWorkflowHelpers', () => ({
useWorkflowHelpers: vi.fn().mockReturnValue({ useWorkflowHelpers: vi.fn().mockReturnValue({
getCurrentWorkflow: vi.fn(),
saveCurrentWorkflow: vi.fn(), saveCurrentWorkflow: vi.fn(),
getWorkflowDataToSave: vi.fn(), getWorkflowDataToSave: vi.fn(),
setDocumentTitle: vi.fn(), setDocumentTitle: vi.fn(),
@@ -286,9 +285,9 @@ describe('useRunWorkflow({ router })', () => {
const mockExecutionResponse = { executionId: '123' }; const mockExecutionResponse = { executionId: '123' };
vi.mocked(uiStore).activeActions = ['']; vi.mocked(uiStore).activeActions = [''];
vi.mocked(workflowHelpers).getCurrentWorkflow.mockReturnValue({ vi.mocked(workflowsStore).workflowObject = {
name: 'Test Workflow', name: 'Test Workflow',
} as unknown as Workflow); } as unknown as Workflow;
vi.mocked(workflowsStore).runWorkflow.mockResolvedValue(mockExecutionResponse); vi.mocked(workflowsStore).runWorkflow.mockResolvedValue(mockExecutionResponse);
vi.mocked(workflowsStore).nodesIssuesExist = true; vi.mocked(workflowsStore).nodesIssuesExist = true;
vi.mocked(workflowsStore).getWorkflowRunData = { vi.mocked(workflowsStore).getWorkflowRunData = {
@@ -333,9 +332,9 @@ describe('useRunWorkflow({ router })', () => {
], ],
} as unknown as WorkflowData); } as unknown as WorkflowData);
vi.mocked(uiStore).activeActions = ['']; vi.mocked(uiStore).activeActions = [''];
vi.mocked(workflowHelpers).getCurrentWorkflow.mockReturnValue({ vi.mocked(workflowsStore).workflowObject = {
name: 'Test Workflow', name: 'Test Workflow',
} as unknown as Workflow); } as unknown as Workflow;
vi.mocked(workflowsStore).runWorkflow.mockResolvedValue(mockExecutionResponse); vi.mocked(workflowsStore).runWorkflow.mockResolvedValue(mockExecutionResponse);
vi.mocked(workflowsStore).nodesIssuesExist = true; vi.mocked(workflowsStore).nodesIssuesExist = true;
vi.mocked(workflowsStore).getWorkflowRunData = { NodeName: [] }; vi.mocked(workflowsStore).getWorkflowRunData = { NodeName: [] };
@@ -380,9 +379,9 @@ describe('useRunWorkflow({ router })', () => {
], ],
} as unknown as WorkflowData); } as unknown as WorkflowData);
vi.mocked(uiStore).activeActions = ['']; vi.mocked(uiStore).activeActions = [''];
vi.mocked(workflowHelpers).getCurrentWorkflow.mockReturnValue({ vi.mocked(workflowsStore).workflowObject = {
name: 'Test Workflow', name: 'Test Workflow',
} as unknown as Workflow); } as unknown as Workflow;
vi.mocked(workflowsStore).runWorkflow.mockResolvedValue(mockExecutionResponse); vi.mocked(workflowsStore).runWorkflow.mockResolvedValue(mockExecutionResponse);
vi.mocked(workflowsStore).nodesIssuesExist = true; vi.mocked(workflowsStore).nodesIssuesExist = true;
vi.mocked(workflowsStore).getWorkflowRunData = { NodeName: [] }; vi.mocked(workflowsStore).getWorkflowRunData = { NodeName: [] };
@@ -413,9 +412,9 @@ describe('useRunWorkflow({ router })', () => {
const { runWorkflow } = useRunWorkflow({ router }); const { runWorkflow } = useRunWorkflow({ router });
vi.mocked(uiStore).activeActions = ['']; vi.mocked(uiStore).activeActions = [''];
vi.mocked(workflowHelpers).getCurrentWorkflow.mockReturnValue({ vi.mocked(workflowsStore).workflowObject = {
name: 'Test Workflow', name: 'Test Workflow',
} as unknown as Workflow); } as unknown as Workflow;
vi.mocked(workflowsStore).runWorkflow.mockResolvedValue(mockExecutionResponse); vi.mocked(workflowsStore).runWorkflow.mockResolvedValue(mockExecutionResponse);
vi.mocked(workflowsStore).nodesIssuesExist = true; vi.mocked(workflowsStore).nodesIssuesExist = true;
vi.mocked(workflowHelpers).getWorkflowDataToSave.mockResolvedValue({ vi.mocked(workflowHelpers).getWorkflowDataToSave.mockResolvedValue({
@@ -437,9 +436,9 @@ describe('useRunWorkflow({ router })', () => {
vi.mocked(pushConnectionStore).isConnected = true; vi.mocked(pushConnectionStore).isConnected = true;
vi.mocked(workflowsStore).runWorkflow.mockResolvedValue(mockExecutionResponse); vi.mocked(workflowsStore).runWorkflow.mockResolvedValue(mockExecutionResponse);
vi.mocked(workflowsStore).nodesIssuesExist = false; vi.mocked(workflowsStore).nodesIssuesExist = false;
vi.mocked(workflowHelpers).getCurrentWorkflow.mockReturnValue({ vi.mocked(workflowsStore).workflowObject = {
name: 'Test Workflow', name: 'Test Workflow',
} as Workflow); } as Workflow;
vi.mocked(workflowHelpers).getWorkflowDataToSave.mockResolvedValue({ vi.mocked(workflowHelpers).getWorkflowDataToSave.mockResolvedValue({
id: 'workflowId', id: 'workflowId',
nodes: [], nodes: [],
@@ -480,7 +479,7 @@ describe('useRunWorkflow({ router })', () => {
vi.mocked(pushConnectionStore).isConnected = true; vi.mocked(pushConnectionStore).isConnected = true;
vi.mocked(workflowsStore).runWorkflow.mockResolvedValue(mockExecutionResponse); vi.mocked(workflowsStore).runWorkflow.mockResolvedValue(mockExecutionResponse);
vi.mocked(workflowsStore).nodesIssuesExist = false; vi.mocked(workflowsStore).nodesIssuesExist = false;
vi.mocked(workflowHelpers).getCurrentWorkflow.mockReturnValue(workflow); vi.mocked(workflowsStore).workflowObject = workflow;
vi.mocked(workflowHelpers).getWorkflowDataToSave.mockResolvedValue({ vi.mocked(workflowHelpers).getWorkflowDataToSave.mockResolvedValue({
id: 'workflowId', id: 'workflowId',
nodes: [], nodes: [],
@@ -551,11 +550,11 @@ describe('useRunWorkflow({ router })', () => {
}, },
], ],
}; };
vi.mocked(workflowHelpers).getCurrentWorkflow.mockReturnValue({ vi.mocked(workflowsStore).workflowObject = {
name: 'Test Workflow', name: 'Test Workflow',
getParentNodes: () => [parentName], getParentNodes: () => [parentName],
nodes: { [parentName]: {} }, nodes: { [parentName]: {} },
} as unknown as Workflow); } as unknown as Workflow;
vi.mocked(workflowHelpers).getWorkflowDataToSave.mockResolvedValue({ vi.mocked(workflowHelpers).getWorkflowDataToSave.mockResolvedValue({
nodes: [], nodes: [],
} as unknown as WorkflowData); } as unknown as WorkflowData);
@@ -585,9 +584,9 @@ describe('useRunWorkflow({ router })', () => {
const composable = useRunWorkflow({ router }); const composable = useRunWorkflow({ router });
const triggerNode = 'Chat Trigger'; const triggerNode = 'Chat Trigger';
const nodeData = mock<ITaskData>(); const nodeData = mock<ITaskData>();
vi.mocked(workflowHelpers).getCurrentWorkflow.mockReturnValue( vi.mocked(workflowsStore).workflowObject = mock<Workflow>({
mock<Workflow>({ getChildNodes: vi.fn().mockReturnValue([]) }), getChildNodes: vi.fn().mockReturnValue([]),
); });
vi.mocked(workflowHelpers).getWorkflowDataToSave.mockResolvedValue( vi.mocked(workflowHelpers).getWorkflowDataToSave.mockResolvedValue(
mock<WorkflowData>({ nodes: [] }), mock<WorkflowData>({ nodes: [] }),
); );
@@ -614,11 +613,9 @@ describe('useRunWorkflow({ router })', () => {
const composable = useRunWorkflow({ router }); const composable = useRunWorkflow({ router });
const triggerNode = 'Chat Trigger'; const triggerNode = 'Chat Trigger';
const nodeData = mock<ITaskData>(); const nodeData = mock<ITaskData>();
vi.mocked(workflowHelpers).getCurrentWorkflow.mockReturnValue( vi.mocked(workflowsStore).workflowObject = mock<Workflow>({
mock<Workflow>({ getChildNodes: vi.fn().mockReturnValue([{ name: 'Child node', type: 'nodes.child' }]),
getChildNodes: vi.fn().mockReturnValue([{ name: 'Child node', type: 'nodes.child' }]), });
}),
);
vi.mocked(workflowHelpers).getWorkflowDataToSave.mockResolvedValue( vi.mocked(workflowHelpers).getWorkflowDataToSave.mockResolvedValue(
mock<WorkflowData>({ nodes: [] }), mock<WorkflowData>({ nodes: [] }),
); );
@@ -653,11 +650,9 @@ describe('useRunWorkflow({ router })', () => {
const composable = useRunWorkflow({ router }); const composable = useRunWorkflow({ router });
const triggerNode = 'Chat Trigger'; const triggerNode = 'Chat Trigger';
const nodeData = mock<ITaskData>(); const nodeData = mock<ITaskData>();
vi.mocked(workflowHelpers).getCurrentWorkflow.mockReturnValue( vi.mocked(workflowsStore).workflowObject = mock<Workflow>({
mock<Workflow>({ getChildNodes: vi.fn().mockReturnValue([{ name: 'Child node', type: 'nodes.child' }]),
getChildNodes: vi.fn().mockReturnValue([{ name: 'Child node', type: 'nodes.child' }]), });
}),
);
vi.mocked(workflowHelpers).getWorkflowDataToSave.mockResolvedValue( vi.mocked(workflowHelpers).getWorkflowDataToSave.mockResolvedValue(
mock<WorkflowData>({ nodes: [] }), mock<WorkflowData>({ nodes: [] }),
); );
@@ -683,9 +678,9 @@ describe('useRunWorkflow({ router })', () => {
// ARRANGE // ARRANGE
const { runWorkflow } = useRunWorkflow({ router }); const { runWorkflow } = useRunWorkflow({ router });
const triggerNode = 'Chat Trigger'; const triggerNode = 'Chat Trigger';
vi.mocked(workflowHelpers).getCurrentWorkflow.mockReturnValue( vi.mocked(workflowsStore).workflowObject = mock<Workflow>({
mock<Workflow>({ getChildNodes: vi.fn().mockReturnValue([]) }), getChildNodes: vi.fn().mockReturnValue([]),
); });
vi.mocked(workflowHelpers).getWorkflowDataToSave.mockResolvedValue( vi.mocked(workflowHelpers).getWorkflowDataToSave.mockResolvedValue(
mock<WorkflowData>({ nodes: [] }), mock<WorkflowData>({ nodes: [] }),
); );
@@ -716,7 +711,7 @@ describe('useRunWorkflow({ router })', () => {
vi.mocked(pushConnectionStore).isConnected = true; vi.mocked(pushConnectionStore).isConnected = true;
vi.mocked(workflowsStore).runWorkflow.mockResolvedValue(mockExecutionResponse); vi.mocked(workflowsStore).runWorkflow.mockResolvedValue(mockExecutionResponse);
vi.mocked(workflowsStore).nodesIssuesExist = false; vi.mocked(workflowsStore).nodesIssuesExist = false;
vi.mocked(workflowHelpers).getCurrentWorkflow.mockReturnValue(workflow); vi.mocked(workflowsStore).workflowObject = workflow;
vi.mocked(workflowHelpers).getWorkflowDataToSave.mockResolvedValue( vi.mocked(workflowHelpers).getWorkflowDataToSave.mockResolvedValue(
mock<WorkflowData>({ id: 'workflowId', nodes: [] }), mock<WorkflowData>({ id: 'workflowId', nodes: [] }),
); );
@@ -782,7 +777,7 @@ describe('useRunWorkflow({ router })', () => {
vi.mocked(pushConnectionStore).isConnected = true; vi.mocked(pushConnectionStore).isConnected = true;
vi.mocked(workflowsStore).runWorkflow.mockResolvedValue(mockExecutionResponse); vi.mocked(workflowsStore).runWorkflow.mockResolvedValue(mockExecutionResponse);
vi.mocked(workflowsStore).nodesIssuesExist = false; vi.mocked(workflowsStore).nodesIssuesExist = false;
vi.mocked(workflowHelpers).getCurrentWorkflow.mockReturnValue(workflow); vi.mocked(workflowsStore).workflowObject = workflow;
vi.mocked(workflowHelpers).getWorkflowDataToSave.mockResolvedValue(workflowData); vi.mocked(workflowHelpers).getWorkflowDataToSave.mockResolvedValue(workflowData);
vi.mocked(workflowsStore).getWorkflowRunData = mockRunData; vi.mocked(workflowsStore).getWorkflowRunData = mockRunData;
vi.mocked(agentRequestStore).getAgentRequest.mockReturnValue(agentRequest); vi.mocked(agentRequestStore).getAgentRequest.mockReturnValue(agentRequest);
@@ -829,7 +824,7 @@ describe('useRunWorkflow({ router })', () => {
vi.mocked(pushConnectionStore).isConnected = true; vi.mocked(pushConnectionStore).isConnected = true;
vi.mocked(workflowsStore).runWorkflow.mockResolvedValue(mockExecutionResponse); vi.mocked(workflowsStore).runWorkflow.mockResolvedValue(mockExecutionResponse);
vi.mocked(workflowsStore).nodesIssuesExist = false; vi.mocked(workflowsStore).nodesIssuesExist = false;
vi.mocked(workflowHelpers).getCurrentWorkflow.mockReturnValue(workflow); vi.mocked(workflowsStore).workflowObject = workflow;
vi.mocked(workflowHelpers).getWorkflowDataToSave.mockResolvedValue( vi.mocked(workflowHelpers).getWorkflowDataToSave.mockResolvedValue(
mock<WorkflowData>({ id: 'workflowId', nodes: [] }), mock<WorkflowData>({ id: 'workflowId', nodes: [] }),
); );
@@ -858,7 +853,7 @@ describe('useRunWorkflow({ router })', () => {
vi.mocked(pushConnectionStore).isConnected = true; vi.mocked(pushConnectionStore).isConnected = true;
vi.mocked(workflowsStore).runWorkflow.mockResolvedValue(mockExecutionResponse); vi.mocked(workflowsStore).runWorkflow.mockResolvedValue(mockExecutionResponse);
vi.mocked(workflowsStore).nodesIssuesExist = false; vi.mocked(workflowsStore).nodesIssuesExist = false;
vi.mocked(workflowHelpers).getCurrentWorkflow.mockReturnValue(workflow); vi.mocked(workflowsStore).workflowObject = workflow;
vi.mocked(workflowHelpers).getWorkflowDataToSave.mockResolvedValue( vi.mocked(workflowHelpers).getWorkflowDataToSave.mockResolvedValue(
mock<WorkflowData>({ id: 'workflowId', nodes: [] }), mock<WorkflowData>({ id: 'workflowId', nodes: [] }),
); );
@@ -878,7 +873,7 @@ describe('useRunWorkflow({ router })', () => {
const { runWorkflow } = useRunWorkflow({ router }); const { runWorkflow } = useRunWorkflow({ router });
const workflow = mock<Workflow>({ name: 'Test Workflow' }); const workflow = mock<Workflow>({ name: 'Test Workflow' });
vi.mocked(workflowHelpers).getCurrentWorkflow.mockReturnValue(workflow); vi.mocked(workflowsStore).workflowObject = workflow;
vi.mocked(workflowHelpers).getWorkflowDataToSave.mockResolvedValue({ vi.mocked(workflowHelpers).getWorkflowDataToSave.mockResolvedValue({
id: workflow.id, id: workflow.id,
nodes: [], nodes: [],
@@ -1002,9 +997,9 @@ describe('useRunWorkflow({ router })', () => {
it('should invoke runWorkflow with expected arguments', async () => { it('should invoke runWorkflow with expected arguments', async () => {
const runWorkflowComposable = useRunWorkflow({ router }); const runWorkflowComposable = useRunWorkflow({ router });
vi.mocked(workflowHelpers).getCurrentWorkflow.mockReturnValue({ vi.mocked(workflowsStore).workflowObject = {
id: 'workflowId', id: 'workflowId',
} as unknown as Workflow); } as unknown as Workflow;
vi.mocked(workflowHelpers).getWorkflowDataToSave.mockResolvedValue({ vi.mocked(workflowHelpers).getWorkflowDataToSave.mockResolvedValue({
id: 'workflowId', id: 'workflowId',
nodes: [], nodes: [],

View File

@@ -45,6 +45,7 @@ import { useNodeDirtiness } from '@/composables/useNodeDirtiness';
import { useCanvasOperations } from './useCanvasOperations'; import { useCanvasOperations } from './useCanvasOperations';
import { useAgentRequestStore } from '@n8n/stores/useAgentRequestStore'; import { useAgentRequestStore } from '@n8n/stores/useAgentRequestStore';
import { useWorkflowSaving } from './useWorkflowSaving'; import { useWorkflowSaving } from './useWorkflowSaving';
import { computed } from 'vue';
export function useRunWorkflow(useRunWorkflowOpts: { router: ReturnType<typeof useRouter> }) { export function useRunWorkflow(useRunWorkflowOpts: { router: ReturnType<typeof useRouter> }) {
const nodeHelpers = useNodeHelpers(); const nodeHelpers = useNodeHelpers();
@@ -64,6 +65,8 @@ export function useRunWorkflow(useRunWorkflowOpts: { router: ReturnType<typeof u
const { dirtinessByName } = useNodeDirtiness(); const { dirtinessByName } = useNodeDirtiness();
const { startChat } = useCanvasOperations(); const { startChat } = useCanvasOperations();
const workflowObject = computed(() => workflowsStore.workflowObject as Workflow);
function sortNodesByYPosition(nodes: string[]) { function sortNodesByYPosition(nodes: string[]) {
return [...nodes].sort((a, b) => { return [...nodes].sort((a, b) => {
const nodeA = workflowsStore.getNodeByName(a)?.position ?? [0, 0]; const nodeA = workflowsStore.getNodeByName(a)?.position ?? [0, 0];
@@ -128,15 +131,13 @@ export function useRunWorkflow(useRunWorkflowOpts: { router: ReturnType<typeof u
return; return;
} }
const workflow = workflowHelpers.getCurrentWorkflow();
toast.clearAllStickyNotifications(); toast.clearAllStickyNotifications();
try { try {
// Get the direct parents of the node // Get the direct parents of the node
let directParentNodes: string[] = []; let directParentNodes: string[] = [];
if (options.destinationNode !== undefined) { if (options.destinationNode !== undefined) {
directParentNodes = workflow.getParentNodes( directParentNodes = workflowObject.value.getParentNodes(
options.destinationNode, options.destinationNode,
NodeConnectionTypes.Main, NodeConnectionTypes.Main,
-1, -1,
@@ -155,7 +156,7 @@ export function useRunWorkflow(useRunWorkflowOpts: { router: ReturnType<typeof u
directParentNodes, directParentNodes,
runData, runData,
workflowData.pinData, workflowData.pinData,
workflow, workflowObject.value,
); );
const { startNodeNames } = consolidatedData; const { startNodeNames } = consolidatedData;
@@ -177,7 +178,7 @@ export function useRunWorkflow(useRunWorkflowOpts: { router: ReturnType<typeof u
} else if (options.triggerNode && options.nodeData && !options.rerunTriggerNode) { } else if (options.triggerNode && options.nodeData && !options.rerunTriggerNode) {
// starts execution from downstream nodes of trigger node // starts execution from downstream nodes of trigger node
startNodeNames.push( startNodeNames.push(
...workflow.getChildNodes(options.triggerNode, NodeConnectionTypes.Main, 1), ...workflowObject.value.getChildNodes(options.triggerNode, NodeConnectionTypes.Main, 1),
); );
newRunData = { [options.triggerNode]: [options.nodeData] }; newRunData = { [options.triggerNode]: [options.nodeData] };
executedNode = options.triggerNode; executedNode = options.triggerNode;
@@ -199,7 +200,7 @@ export function useRunWorkflow(useRunWorkflowOpts: { router: ReturnType<typeof u
destinationNodeType === CHAT_TRIGGER_NODE_TYPE) && destinationNodeType === CHAT_TRIGGER_NODE_TYPE) &&
options.source !== 'RunData.ManualChatMessage' options.source !== 'RunData.ManualChatMessage'
) { ) {
const startNode = workflow.getStartNode(options.destinationNode); const startNode = workflowObject.value.getStartNode(options.destinationNode);
if (startNode && startNode.type === CHAT_TRIGGER_NODE_TYPE) { if (startNode && startNode.type === CHAT_TRIGGER_NODE_TYPE) {
// Check if the chat node has input data or pin data // Check if the chat node has input data or pin data
const chatHasInputData = const chatHasInputData =
@@ -251,9 +252,13 @@ export function useRunWorkflow(useRunWorkflowOpts: { router: ReturnType<typeof u
// Find for each start node the source data // Find for each start node the source data
let sourceData = get(runData, [name, 0, 'source', 0], null); let sourceData = get(runData, [name, 0, 'source', 0], null);
if (sourceData === null) { if (sourceData === null) {
const parentNodes = workflow.getParentNodes(name, NodeConnectionTypes.Main, 1); const parentNodes = workflowObject.value.getParentNodes(
name,
NodeConnectionTypes.Main,
1,
);
const executeData = workflowHelpers.executeData( const executeData = workflowHelpers.executeData(
workflow.connectionsBySourceNode, workflowObject.value.connectionsBySourceNode,
parentNodes, parentNodes,
name, name,
NodeConnectionTypes.Main, NodeConnectionTypes.Main,
@@ -322,8 +327,8 @@ export function useRunWorkflow(useRunWorkflowOpts: { router: ReturnType<typeof u
if ('destinationNode' in options) { if ('destinationNode' in options) {
startRunData.destinationNode = options.destinationNode; startRunData.destinationNode = options.destinationNode;
const nodeId = workflowsStore.getNodeByName(options.destinationNode as string)?.id; const nodeId = workflowsStore.getNodeByName(options.destinationNode as string)?.id;
if (workflow.id && nodeId && version === 2) { if (workflowObject.value.id && nodeId && version === 2) {
const agentRequest = agentRequestStore.getAgentRequest(workflow.id, nodeId); const agentRequest = agentRequestStore.getAgentRequest(workflowObject.value.id, nodeId);
if (agentRequest) { if (agentRequest) {
startRunData.agentRequest = { startRunData.agentRequest = {
@@ -355,7 +360,7 @@ export function useRunWorkflow(useRunWorkflowOpts: { router: ReturnType<typeof u
createdAt: new Date(), createdAt: new Date(),
startedAt: new Date(), startedAt: new Date(),
stoppedAt: undefined, stoppedAt: undefined,
workflowId: workflow.id, workflowId: workflowObject.value.id,
executedNode, executedNode,
triggerNode: triggerToStartFrom?.name, triggerNode: triggerToStartFrom?.name,
data: { data: {
@@ -377,7 +382,7 @@ export function useRunWorkflow(useRunWorkflowOpts: { router: ReturnType<typeof u
workflowsStore.setWorkflowExecutionData(executionData); workflowsStore.setWorkflowExecutionData(executionData);
nodeHelpers.updateNodesExecutionIssues(); nodeHelpers.updateNodesExecutionIssues();
workflowHelpers.setDocumentTitle(workflow.name as string, 'EXECUTING'); workflowHelpers.setDocumentTitle(workflowObject.value.name as string, 'EXECUTING');
const runWorkflowApiResponse = await runWorkflowApi(startRunData); const runWorkflowApiResponse = await runWorkflowApi(startRunData);
const pinData = workflowData.pinData ?? {}; const pinData = workflowData.pinData ?? {};
@@ -412,7 +417,7 @@ export function useRunWorkflow(useRunWorkflowOpts: { router: ReturnType<typeof u
return runWorkflowApiResponse; return runWorkflowApiResponse;
} catch (error) { } catch (error) {
workflowsStore.setWorkflowExecutionData(null); workflowsStore.setWorkflowExecutionData(null);
workflowHelpers.setDocumentTitle(workflow.name as string, 'ERROR'); workflowHelpers.setDocumentTitle(workflowObject.value.name as string, 'ERROR');
toast.showError(error, i18n.baseText('workflowRun.showError.title')); toast.showError(error, i18n.baseText('workflowRun.showError.title'));
return undefined; return undefined;
} }
@@ -539,11 +544,9 @@ export function useRunWorkflow(useRunWorkflowOpts: { router: ReturnType<typeof u
} }
async function runEntireWorkflow(source: 'node' | 'main', triggerNode?: string) { async function runEntireWorkflow(source: 'node' | 'main', triggerNode?: string) {
const workflow = workflowHelpers.getCurrentWorkflow();
void workflowHelpers.getWorkflowDataToSave().then((workflowData) => { void workflowHelpers.getWorkflowDataToSave().then((workflowData) => {
const telemetryPayload = { const telemetryPayload = {
workflow_id: workflow.id, workflow_id: workflowObject.value.id,
node_graph_string: JSON.stringify( node_graph_string: JSON.stringify(
TelemetryHelpers.generateNodesGraph( TelemetryHelpers.generateNodesGraph(
workflowData as IWorkflowBase, workflowData as IWorkflowBase,

View File

@@ -2,14 +2,17 @@ import { useWorkflowsStore } from '@/stores/workflows.store';
import { import {
buildAdjacencyList, buildAdjacencyList,
parseExtractableSubgraphSelection, parseExtractableSubgraphSelection,
type ExtractableSubgraphData,
type ExtractableErrorResult,
extractReferencesInNodeExpressions, extractReferencesInNodeExpressions,
type IConnections,
type INode,
EXECUTE_WORKFLOW_TRIGGER_NODE_TYPE, EXECUTE_WORKFLOW_TRIGGER_NODE_TYPE,
NodeHelpers, NodeHelpers,
} from 'n8n-workflow'; } from 'n8n-workflow';
import type {
ExtractableSubgraphData,
ExtractableErrorResult,
IConnections,
INode,
Workflow,
} from 'n8n-workflow';
import { computed } from 'vue'; import { computed } from 'vue';
import { useToast } from './useToast'; import { useToast } from './useToast';
import { useRouter } from 'vue-router'; import { useRouter } from 'vue-router';
@@ -44,6 +47,8 @@ export function useWorkflowExtraction() {
const adjacencyList = computed(() => buildAdjacencyList(workflowsStore.workflow.connections)); const adjacencyList = computed(() => buildAdjacencyList(workflowsStore.workflow.connections));
const workflowObject = computed(() => workflowsStore.workflowObject as Workflow);
function showError(message: string) { function showError(message: string) {
toast.showMessage({ toast.showMessage({
type: 'error', type: 'error',
@@ -309,7 +314,7 @@ export function useWorkflowExtraction() {
const nodeType = useNodeTypesStore().getNodeType(node.type, node.typeVersion); const nodeType = useNodeTypesStore().getNodeType(node.type, node.typeVersion);
if (!nodeType) return true; // invariant broken -> abort onto error path if (!nodeType) return true; // invariant broken -> abort onto error path
const ios = getIOs(workflowsStore.getCurrentWorkflow(), node, nodeType); const ios = getIOs(workflowObject.value, node, nodeType);
return ( return (
ios.filter((x) => (typeof x === 'string' ? x === 'main' : x.type === 'main')).length <= 1 ios.filter((x) => (typeof x === 'string' ? x === 'main' : x.type === 'main')).length <= 1
); );
@@ -427,7 +432,6 @@ export function useWorkflowExtraction() {
) { ) {
const { start, end } = selection; const { start, end } = selection;
const currentWorkflow = workflowsStore.getCurrentWorkflow();
const allNodeNames = workflowsStore.workflow.nodes.map((x) => x.name); const allNodeNames = workflowsStore.workflow.nodes.map((x) => x.name);
let startNodeName = 'Start'; let startNodeName = 'Start';
@@ -438,16 +442,16 @@ export function useWorkflowExtraction() {
while (subGraphNames.includes(returnNodeName)) returnNodeName += '_1'; while (subGraphNames.includes(returnNodeName)) returnNodeName += '_1';
const directAfterEndNodeNames = end const directAfterEndNodeNames = end
? currentWorkflow ? workflowObject.value
.getChildNodes(end, 'main', 1) .getChildNodes(end, 'main', 1)
.map((x) => currentWorkflow.getNode(x)?.name) .map((x) => workflowObject.value.getNode(x)?.name)
.filter((x) => x !== undefined) .filter((x) => x !== undefined)
: []; : [];
const allAfterEndNodes = end const allAfterEndNodes = end
? currentWorkflow ? workflowObject.value
.getChildNodes(end, 'ALL') .getChildNodes(end, 'ALL')
.map((x) => currentWorkflow.getNode(x)) .map((x) => workflowObject.value.getNode(x))
.filter((x) => x !== null) .filter((x) => x !== null)
: []; : [];

View File

@@ -82,7 +82,7 @@ export function resolveParameter<T = IDataObject>(
if ('localResolve' in opts && opts.localResolve) { if ('localResolve' in opts && opts.localResolve) {
return resolveParameterImpl( return resolveParameterImpl(
parameter, parameter,
() => opts.workflow, opts.workflow,
opts.connections, opts.connections,
opts.envVars, opts.envVars,
opts.workflow.getNode(opts.nodeName), opts.workflow.getNode(opts.nodeName),
@@ -102,7 +102,7 @@ export function resolveParameter<T = IDataObject>(
return resolveParameterImpl( return resolveParameterImpl(
parameter, parameter,
workflowsStore.getCurrentWorkflow, workflowsStore.workflowObject as Workflow,
workflowsStore.connectionsBySourceNode, workflowsStore.connectionsBySourceNode,
useEnvironmentsStore().variablesAsObject, useEnvironmentsStore().variablesAsObject,
useNDVStore().activeNode, useNDVStore().activeNode,
@@ -116,7 +116,7 @@ export function resolveParameter<T = IDataObject>(
// TODO: move to separate file // TODO: move to separate file
function resolveParameterImpl<T = IDataObject>( function resolveParameterImpl<T = IDataObject>(
parameter: NodeParameterValue | INodeParameters | NodeParameterValue[] | INodeParameters[], parameter: NodeParameterValue | INodeParameters | NodeParameterValue[] | INodeParameters[],
getContextWorkflow: () => Workflow, workflowObject: Workflow,
connections: IConnections, connections: IConnections,
envVars: Record<string, string | boolean | number>, envVars: Record<string, string | boolean | number>,
ndvActiveNode: INodeUi | null, ndvActiveNode: INodeUi | null,
@@ -127,8 +127,6 @@ function resolveParameterImpl<T = IDataObject>(
): T | null { ): T | null {
let itemIndex = opts?.targetItem?.itemIndex || 0; let itemIndex = opts?.targetItem?.itemIndex || 0;
const workflow = getContextWorkflow();
const additionalKeys: IWorkflowDataProxyAdditionalKeys = { const additionalKeys: IWorkflowDataProxyAdditionalKeys = {
$execution: { $execution: {
id: PLACEHOLDER_FILLED_AT_EXECUTION_TIME, id: PLACEHOLDER_FILLED_AT_EXECUTION_TIME,
@@ -147,7 +145,7 @@ function resolveParameterImpl<T = IDataObject>(
if (opts.isForCredential) { if (opts.isForCredential) {
// node-less expression resolution // node-less expression resolution
return workflow.expression.getParameterValue( return workflowObject.expression.getParameterValue(
parameter, parameter,
null, null,
0, 0,
@@ -165,18 +163,18 @@ function resolveParameterImpl<T = IDataObject>(
const inputName = NodeConnectionTypes.Main; const inputName = NodeConnectionTypes.Main;
const activeNode = ndvActiveNode ?? workflow.getNode(opts.contextNodeName || ''); const activeNode = ndvActiveNode ?? workflowObject.getNode(opts.contextNodeName || '');
let contextNode = activeNode; let contextNode = activeNode;
if (activeNode) { if (activeNode) {
contextNode = workflow.getParentMainInputNode(activeNode) ?? null; contextNode = workflowObject.getParentMainInputNode(activeNode) ?? null;
} }
const workflowRunData = executionData?.data?.resultData.runData ?? null; const workflowRunData = executionData?.data?.resultData.runData ?? null;
let parentNode = workflow.getParentNodes(contextNode!.name, inputName, 1); let parentNode = workflowObject.getParentNodes(contextNode!.name, inputName, 1);
let runIndexParent = opts?.inputRunIndex ?? 0; let runIndexParent = opts?.inputRunIndex ?? 0;
const nodeConnection = workflow.getNodeConnectionIndexes(contextNode!.name, parentNode[0]); const nodeConnection = workflowObject.getNodeConnectionIndexes(contextNode!.name, parentNode[0]);
if (opts.targetItem && opts?.targetItem?.nodeName === contextNode!.name && executionData) { if (opts.targetItem && opts?.targetItem?.nodeName === contextNode!.name && executionData) {
const sourceItems = getSourceItems(executionData, opts.targetItem); const sourceItems = getSourceItems(executionData, opts.targetItem);
if (!sourceItems.length) { if (!sourceItems.length) {
@@ -295,7 +293,7 @@ function resolveParameterImpl<T = IDataObject>(
); );
} }
return workflow.expression.getParameterValue( return workflowObject.expression.getParameterValue(
parameter, parameter,
runExecutionData, runExecutionData,
runIndexCurrent, runIndexCurrent,
@@ -344,10 +342,6 @@ export function resolveRequiredParameters(
return resolvedParameters; return resolvedParameters;
} }
function getCurrentWorkflow(copyData?: boolean): Workflow {
return useWorkflowsStore().getCurrentWorkflow(copyData);
}
function getConnectedNodes( function getConnectedNodes(
direction: 'upstream' | 'downstream', direction: 'upstream' | 'downstream',
workflow: Workflow, workflow: Workflow,
@@ -376,11 +370,6 @@ function getConnectedNodes(
return [...new Set(connectedNodes)]; return [...new Set(connectedNodes)];
} }
// Returns a workflow instance.
function getWorkflow(nodes: INodeUi[], connections: IConnections, copyData?: boolean): Workflow {
return useWorkflowsStore().getWorkflow(nodes, connections, copyData);
}
function getNodeTypes(): INodeTypes { function getNodeTypes(): INodeTypes {
return useWorkflowsStore().getNodeTypes(); return useWorkflowsStore().getNodeTypes();
} }
@@ -870,8 +859,7 @@ export function useWorkflowHelpers() {
if (typeof obj === 'object' && stringifyObject) { if (typeof obj === 'object' && stringifyObject) {
const proxy = obj as { isProxy: boolean; toJSON?: () => unknown } | null; const proxy = obj as { isProxy: boolean; toJSON?: () => unknown } | null;
if (proxy?.isProxy && proxy.toJSON) return JSON.stringify(proxy.toJSON()); if (proxy?.isProxy && proxy.toJSON) return JSON.stringify(proxy.toJSON());
const workflow = getCurrentWorkflow(); return workflowsStore.workflowObject.expression.convertObjectValueToString(obj as object);
return workflow.expression.convertObjectValueToString(obj as object);
} }
return obj; return obj;
} }
@@ -1080,9 +1068,7 @@ export function useWorkflowHelpers() {
setDocumentTitle, setDocumentTitle,
resolveParameter, resolveParameter,
resolveRequiredParameters, resolveRequiredParameters,
getCurrentWorkflow,
getConnectedNodes, getConnectedNodes,
getWorkflow,
getNodeTypes, getNodeTypes,
connectionInputData, connectionInputData,
executeData, executeData,

View File

@@ -41,12 +41,8 @@ export function useWorkflowSaving({ router }: { router: ReturnType<typeof useRou
const nodeHelpers = useNodeHelpers(); const nodeHelpers = useNodeHelpers();
const templatesStore = useTemplatesStore(); const templatesStore = useTemplatesStore();
const { const { getWorkflowDataToSave, checkConflictingWebhooks, getWorkflowProjectRole } =
getWorkflowDataToSave, useWorkflowHelpers();
checkConflictingWebhooks,
getWorkflowProjectRole,
getCurrentWorkflow,
} = useWorkflowHelpers();
async function promptSaveUnsavedWorkflowChanges( async function promptSaveUnsavedWorkflowChanges(
next: NavigationGuardNext, next: NavigationGuardNext,
@@ -414,7 +410,6 @@ export function useWorkflowSaving({ router }: { router: ReturnType<typeof useRou
uiStore.stateIsDirty = false; uiStore.stateIsDirty = false;
void useExternalHooks().run('workflow.afterUpdate', { workflowData }); void useExternalHooks().run('workflow.afterUpdate', { workflowData });
getCurrentWorkflow(true); // refresh cache
return workflowData.id; return workflowData.id;
} catch (e) { } catch (e) {
uiStore.removeActiveAction('workflowSaving'); uiStore.removeActiveAction('workflowSaving');

View File

@@ -341,8 +341,8 @@ describe('LogsPanel', () => {
it('should still show logs for a removed node', async () => { it('should still show logs for a removed node', async () => {
const operations = useCanvasOperations(); const operations = useCanvasOperations();
logsStore.toggleOpen(true);
workflowsStore.setWorkflow(deepCopy(aiChatWorkflow)); workflowsStore.setWorkflow(deepCopy(aiChatWorkflow));
logsStore.toggleOpen(true);
workflowsStore.setWorkflowExecutionData({ workflowsStore.setWorkflowExecutionData({
...aiChatExecutionResponse, ...aiChatExecutionResponse,
id: '2345', id: '2345',
@@ -711,15 +711,15 @@ describe('LogsPanel', () => {
const { getByTestId, queryByTestId } = render(); const { getByTestId, queryByTestId } = render();
expect(getByTestId('canvas-chat')).toBeInTheDocument(); expect(getByTestId('canvas-chat')).toBeInTheDocument();
expect(getByTestId('chat-attach-file-button')).toBeInTheDocument(); expect(queryByTestId('chat-attach-file-button')).toBeInTheDocument();
workflowsStore.setNodeParameters({ // workflowsStore.setNodeParameters({
name: chatTriggerNode.name, // name: chatTriggerNode.name,
value: { options: { allowFileUploads: false } }, // value: { options: { allowFileUploads: false } },
}); // });
await waitFor(() => // await waitFor(() =>
expect(queryByTestId('chat-attach-file-button')).not.toBeInTheDocument(), // expect(queryByTestId('chat-attach-file-button')).not.toBeInTheDocument(),
); // );
}); });
}); });

View File

@@ -76,7 +76,7 @@ function handleChangeDisplayMode(value: IRunDataDisplayMode) {
v-bind="runDataProps" v-bind="runDataProps"
:key="`run-data${pipWindow ? '-pip' : ''}`" :key="`run-data${pipWindow ? '-pip' : ''}`"
:class="$style.component" :class="$style.component"
:workflow="logEntry.workflow" :workflow-object="logEntry.workflow"
:workflow-execution="logEntry.execution" :workflow-execution="logEntry.execution"
:too-much-data-title="locale.baseText('ndv.output.tooMuchData.title')" :too-much-data-title="locale.baseText('ndv.output.tooMuchData.title')"
:no-data-in-branch-message="locale.baseText('ndv.output.noOutputDataInBranch')" :no-data-in-branch-message="locale.baseText('ndv.output.noOutputDataInBranch')"

View File

@@ -48,9 +48,7 @@ export function useChatState(isReadOnly: boolean): ChatState {
const currentSessionId = ref<string>(uuid().replace(/-/g, '')); const currentSessionId = ref<string>(uuid().replace(/-/g, ''));
const previousChatMessages = computed(() => workflowsStore.getPastChatMessages); const previousChatMessages = computed(() => workflowsStore.getPastChatMessages);
const chatTriggerNode = computed( const chatTriggerNode = computed(() => workflowsStore.allNodes.find(isChatNode) ?? null);
() => Object.values(workflowsStore.allNodes).find(isChatNode) ?? null,
);
const allowFileUploads = computed( const allowFileUploads = computed(
() => () =>
(chatTriggerNode.value?.parameters?.options as INodeParameters)?.allowFileUploads === true, (chatTriggerNode.value?.parameters?.options as INodeParameters)?.allowFileUploads === true,

View File

@@ -15,7 +15,7 @@ import { useCanvasMapping } from '@/composables/useCanvasMapping';
// Mock modules at top level // Mock modules at top level
vi.mock('@/stores/workflows.store', () => ({ vi.mock('@/stores/workflows.store', () => ({
useWorkflowsStore: () => ({ useWorkflowsStore: () => ({
getWorkflow: vi.fn().mockReturnValue({ createWorkflowObject: vi.fn().mockReturnValue({
id: 'test-workflow', id: 'test-workflow',
nodes: [], nodes: [],
connections: {}, connections: {},

View File

@@ -84,17 +84,20 @@ export function mapConnections(connections: CanvasConnection[]) {
function createWorkflowRefs( function createWorkflowRefs(
workflow: MaybeRefOrGetter<IWorkflowDb | undefined>, workflow: MaybeRefOrGetter<IWorkflowDb | undefined>,
getWorkflow: (nodes: INodeUi[], connections: IConnections) => Workflow, createWorkflowObject: (nodes: INodeUi[], connections: IConnections) => Workflow,
) { ) {
const workflowRef = computed(() => toValue(workflow)); const workflowRef = computed(() => toValue(workflow));
const workflowNodes = ref<INodeUi[]>([]); const workflowNodes = ref<INodeUi[]>([]);
const workflowConnections = ref<IConnections>({}); const workflowConnections = ref<IConnections>({});
const workflowObjectRef = shallowRef<Workflow>(getWorkflow([], {})); const workflowObjectRef = shallowRef<Workflow>(createWorkflowObject([], {}));
watchEffect(() => { watchEffect(() => {
const workflowValue = workflowRef.value; const workflowValue = workflowRef.value;
if (workflowValue) { if (workflowValue) {
workflowObjectRef.value = getWorkflow(workflowValue.nodes, workflowValue.connections); workflowObjectRef.value = createWorkflowObject(
workflowValue.nodes,
workflowValue.connections,
);
workflowNodes.value = workflowValue.nodes; workflowNodes.value = workflowValue.nodes;
workflowConnections.value = workflowValue.connections; workflowConnections.value = workflowValue.connections;
} }
@@ -153,8 +156,8 @@ export const useWorkflowDiff = (
const workflowsStore = useWorkflowsStore(); const workflowsStore = useWorkflowsStore();
const nodeTypesStore = useNodeTypesStore(); const nodeTypesStore = useNodeTypesStore();
const sourceRefs = createWorkflowRefs(sourceWorkflow, workflowsStore.getWorkflow); const sourceRefs = createWorkflowRefs(sourceWorkflow, workflowsStore.createWorkflowObject);
const targetRefs = createWorkflowRefs(targetWorkflow, workflowsStore.getWorkflow); const targetRefs = createWorkflowRefs(targetWorkflow, workflowsStore.createWorkflowObject);
const source = createWorkflowDiff( const source = createWorkflowDiff(
sourceRefs.workflowRef, sourceRefs.workflowRef,

View File

@@ -1,5 +1,4 @@
import { createTestNode, createTestWorkflowObject } from '@/__tests__/mocks'; import { createTestNode, createTestWorkflowObject } from '@/__tests__/mocks';
import * as workflowHelpers from '@/composables/useWorkflowHelpers';
import * as ndvStore from '@/stores/ndv.store'; import * as ndvStore from '@/stores/ndv.store';
import { CompletionContext, insertCompletionText } from '@codemirror/autocomplete'; import { CompletionContext, insertCompletionText } from '@codemirror/autocomplete';
import { javascriptLanguage } from '@codemirror/lang-javascript'; import { javascriptLanguage } from '@codemirror/lang-javascript';
@@ -8,6 +7,10 @@ import { EditorView } from '@codemirror/view';
import { NodeConnectionTypes, type IConnections } from 'n8n-workflow'; import { NodeConnectionTypes, type IConnections } from 'n8n-workflow';
import type { MockInstance } from 'vitest'; import type { MockInstance } from 'vitest';
import { autocompletableNodeNames, expressionWithFirstItem, stripExcessParens } from './utils'; import { autocompletableNodeNames, expressionWithFirstItem, stripExcessParens } from './utils';
import { useWorkflowsStore } from '@/stores/workflows.store';
import { mockedStore } from '@/__tests__/utils';
import { createTestingPinia } from '@pinia/testing';
import { setActivePinia } from 'pinia';
vi.mock('@/composables/useWorkflowHelpers', () => ({ vi.mock('@/composables/useWorkflowHelpers', () => ({
useWorkflowHelpers: vi.fn().mockReturnValue({ useWorkflowHelpers: vi.fn().mockReturnValue({
@@ -75,6 +78,11 @@ describe('completion utils', () => {
}); });
describe('autocompletableNodeNames', () => { describe('autocompletableNodeNames', () => {
beforeEach(() => {
const pinia = createTestingPinia();
setActivePinia(pinia);
});
it('should work for normal nodes', () => { it('should work for normal nodes', () => {
const nodes = [ const nodes = [
createTestNode({ name: 'Node 1' }), createTestNode({ name: 'Node 1' }),
@@ -98,10 +106,8 @@ describe('completion utils', () => {
connections, connections,
}); });
const workflowHelpersMock: MockInstance = vi.spyOn(workflowHelpers, 'useWorkflowHelpers'); const workflowsStore = mockedStore(useWorkflowsStore);
workflowHelpersMock.mockReturnValue({ workflowsStore.workflowObject = workflowObject;
getCurrentWorkflow: vi.fn(() => workflowObject),
});
const ndvStoreMock: MockInstance = vi.spyOn(ndvStore, 'useNDVStore'); const ndvStoreMock: MockInstance = vi.spyOn(ndvStore, 'useNDVStore');
ndvStoreMock.mockReturnValue({ activeNode: nodes[2] }); ndvStoreMock.mockReturnValue({ activeNode: nodes[2] });
@@ -131,10 +137,9 @@ describe('completion utils', () => {
connections, connections,
}); });
const workflowHelpersMock: MockInstance = vi.spyOn(workflowHelpers, 'useWorkflowHelpers'); const workflowsStore = mockedStore(useWorkflowsStore);
workflowHelpersMock.mockReturnValue({ workflowsStore.workflowObject = workflowObject;
getCurrentWorkflow: vi.fn(() => workflowObject),
});
const ndvStoreMock: MockInstance = vi.spyOn(ndvStore, 'useNDVStore'); const ndvStoreMock: MockInstance = vi.spyOn(ndvStore, 'useNDVStore');
ndvStoreMock.mockReturnValue({ activeNode: nodes[2] }); ndvStoreMock.mockReturnValue({ activeNode: nodes[2] });

View File

@@ -4,7 +4,7 @@ import {
SPLIT_IN_BATCHES_NODE_TYPE, SPLIT_IN_BATCHES_NODE_TYPE,
} from '@/constants'; } from '@/constants';
import { useWorkflowsStore } from '@/stores/workflows.store'; import { useWorkflowsStore } from '@/stores/workflows.store';
import { resolveParameter, useWorkflowHelpers } from '@/composables/useWorkflowHelpers'; import { resolveParameter } from '@/composables/useWorkflowHelpers';
import { useNDVStore } from '@/stores/ndv.store'; import { useNDVStore } from '@/stores/ndv.store';
import { useUIStore } from '@/stores/ui.store'; import { useUIStore } from '@/stores/ui.store';
import { import {
@@ -225,8 +225,8 @@ export function autocompletableNodeNames(targetNodeParameterContext?: TargetNode
const activeNodeName = activeNode.name; const activeNodeName = activeNode.name;
const workflow = useWorkflowHelpers().getCurrentWorkflow(); const workflowObject = useWorkflowsStore().workflowObject;
const nonMainChildren = workflow.getChildNodes(activeNodeName, 'ALL_NON_MAIN'); const nonMainChildren = workflowObject.getChildNodes(activeNodeName, 'ALL_NON_MAIN');
// This is a tool node, look for the nearest node with main connections // This is a tool node, look for the nearest node with main connections
if (nonMainChildren.length > 0) { if (nonMainChildren.length > 0) {
@@ -237,8 +237,8 @@ export function autocompletableNodeNames(targetNodeParameterContext?: TargetNode
} }
export function getPreviousNodes(nodeName: string) { export function getPreviousNodes(nodeName: string) {
const workflow = useWorkflowHelpers().getCurrentWorkflow(); const workflowObject = useWorkflowsStore().workflowObject;
return workflow return workflowObject
.getParentNodesByDepth(nodeName) .getParentNodesByDepth(nodeName)
.map((node) => node.name) .map((node) => node.name)
.filter((name) => name !== nodeName); .filter((name) => name !== nodeName);

View File

@@ -50,9 +50,11 @@ export function useTypescript(
allNodeNames: autocompletableNodeNames(toValue(targetNodeParameterContext)), allNodeNames: autocompletableNodeNames(toValue(targetNodeParameterContext)),
variables: useEnvironmentsStore().variables.map((v) => v.key), variables: useEnvironmentsStore().variables.map((v) => v.key),
inputNodeNames: activeNodeName inputNodeNames: activeNodeName
? workflowsStore ? workflowsStore.workflowObject.getParentNodes(
.getCurrentWorkflow() activeNodeName,
.getParentNodes(activeNodeName, NodeConnectionTypes.Main, 1) NodeConnectionTypes.Main,
1,
)
: [], : [],
mode: toValue(mode), mode: toValue(mode),
}, },

View File

@@ -728,8 +728,8 @@ export const useAssistantStore = defineStore(STORES.ASSISTANT, () => {
codeDiffMessage.replacing = true; codeDiffMessage.replacing = true;
const suggestionId = codeDiffMessage.suggestionId; const suggestionId = codeDiffMessage.suggestionId;
const currentWorkflow = workflowsStore.getCurrentWorkflow(); const workflowObject = workflowsStore.workflowObject;
const activeNode = currentWorkflow.getNode(chatSessionError.value.node.name); const activeNode = workflowObject.getNode(chatSessionError.value.node.name);
assert(activeNode); assert(activeNode);
const cached = suggestions.value[suggestionId]; const cached = suggestions.value[suggestionId];
@@ -774,8 +774,8 @@ export const useAssistantStore = defineStore(STORES.ASSISTANT, () => {
const suggestion = suggestions.value[suggestionId]; const suggestion = suggestions.value[suggestionId];
assert(suggestion); assert(suggestion);
const currentWorkflow = workflowsStore.getCurrentWorkflow(); const workflowObject = workflowsStore.workflowObject;
const activeNode = currentWorkflow.getNode(chatSessionError.value.node.name); const activeNode = workflowObject.getNode(chatSessionError.value.node.name);
assert(activeNode); assert(activeNode);
const suggested = suggestion.previous; const suggested = suggestion.previous;

View File

@@ -180,8 +180,11 @@ export const useNDVStore = defineStore(STORES.NDV, () => {
if (!activeNode.value || !inputNodeName) { if (!activeNode.value || !inputNodeName) {
return false; return false;
} }
const workflow = workflowsStore.getCurrentWorkflow(); const parentNodes = workflowsStore.workflowObject.getParentNodes(
const parentNodes = workflow.getParentNodes(activeNode.value.name, NodeConnectionTypes.Main, 1); activeNode.value.name,
NodeConnectionTypes.Main,
1,
);
return parentNodes.includes(inputNodeName); return parentNodes.includes(inputNodeName);
}); });

View File

@@ -46,12 +46,12 @@ vi.mock('@/stores/workflows.store', () => {
}), }),
workflowTriggerNodes: [], workflowTriggerNodes: [],
workflowId: 'dummy-workflow-id', workflowId: 'dummy-workflow-id',
getCurrentWorkflow: vi.fn(() => ({ workflowObject: {
getNode: vi.fn(() => ({ getNode: vi.fn(() => ({
type: 'n8n-node.example', type: 'n8n-node.example',
typeVersion: 1, typeVersion: 1,
})), })),
})), },
}), }),
}; };
}); });

View File

@@ -25,6 +25,7 @@ import type {
INodeInputConfiguration, INodeInputConfiguration,
NodeParameterValueType, NodeParameterValueType,
NodeConnectionType, NodeConnectionType,
Workflow,
} from 'n8n-workflow'; } from 'n8n-workflow';
import { NodeConnectionTypes, NodeHelpers } from 'n8n-workflow'; import { NodeConnectionTypes, NodeHelpers } from 'n8n-workflow';
import { useWorkflowsStore } from '@/stores/workflows.store'; import { useWorkflowsStore } from '@/stores/workflows.store';
@@ -66,6 +67,8 @@ export const useNodeCreatorStore = defineStore(STORES.NODE_CREATOR, () => {
Object.values(mergedNodes.value).map((i) => transformNodeType(i)), Object.values(mergedNodes.value).map((i) => transformNodeType(i)),
); );
const workflowObject = computed(() => workflowsStore.workflowObject as Workflow);
function setMergeNodes(nodes: SimplifiedNodeType[]) { function setMergeNodes(nodes: SimplifiedNodeType[]) {
mergedNodes.value = nodes; mergedNodes.value = nodes;
} }
@@ -263,13 +266,12 @@ export const useNodeCreatorStore = defineStore(STORES.NODE_CREATOR, () => {
function getNodeCreatorFilter(nodeName: string, outputType?: NodeConnectionType) { function getNodeCreatorFilter(nodeName: string, outputType?: NodeConnectionType) {
let filter; let filter;
const workflow = workflowsStore.getCurrentWorkflow(); const workflowNode = workflowObject.value.getNode(nodeName);
const workflowNode = workflow.getNode(nodeName);
if (!workflowNode) return { nodes: [] }; if (!workflowNode) return { nodes: [] };
const nodeType = nodeTypesStore.getNodeType(workflowNode?.type, workflowNode.typeVersion); const nodeType = nodeTypesStore.getNodeType(workflowNode?.type, workflowNode.typeVersion);
if (nodeType) { if (nodeType) {
const inputs = NodeHelpers.getNodeInputs(workflow, workflowNode, nodeType); const inputs = NodeHelpers.getNodeInputs(workflowObject.value, workflowNode, nodeType);
const filterFound = inputs.filter((input) => { const filterFound = inputs.filter((input) => {
if (typeof input === 'string' || input.type !== outputType || !input.filter) { if (typeof input === 'string' || input.type !== outputType || !input.filter) {

View File

@@ -18,6 +18,7 @@ import type {
IConnection, IConnection,
INodeExecutionData, INodeExecutionData,
INode, INode,
INodeTypeDescription,
} from 'n8n-workflow'; } from 'n8n-workflow';
import { stringSizeInBytes } from '@/utils/typesUtils'; import { stringSizeInBytes } from '@/utils/typesUtils';
import { dataPinningEventBus } from '@/event-bus'; import { dataPinningEventBus } from '@/event-bus';
@@ -46,7 +47,12 @@ vi.mock('@/api/workflows', () => ({
getNewWorkflow: vi.fn(), getNewWorkflow: vi.fn(),
})); }));
const getNodeType = vi.fn(); const getNodeType = vi.fn((_nodeTypeName: string): Partial<INodeTypeDescription> | null => ({
inputs: [],
group: [],
webhooks: [],
properties: [],
}));
vi.mock('@/stores/nodeTypes.store', () => ({ vi.mock('@/stores/nodeTypes.store', () => ({
useNodeTypesStore: vi.fn(() => ({ useNodeTypesStore: vi.fn(() => ({
getNodeType, getNodeType,
@@ -86,50 +92,50 @@ describe('useWorkflowsStore', () => {
describe('isWaitingExecution', () => { describe('isWaitingExecution', () => {
it('should return false if no activeNode and no waiting nodes in workflow', () => { it('should return false if no activeNode and no waiting nodes in workflow', () => {
workflowsStore.workflow.nodes = [ workflowsStore.setNodes([
{ type: 'type1' }, { type: 'type1' },
{ type: 'type2' }, { type: 'type2' },
] as unknown as IWorkflowDb['nodes']; ] as unknown as IWorkflowDb['nodes']);
const isWaiting = workflowsStore.isWaitingExecution; const isWaiting = workflowsStore.isWaitingExecution;
expect(isWaiting).toEqual(false); expect(isWaiting).toEqual(false);
}); });
it('should return false if no activeNode and waiting node in workflow and waiting node is disabled', () => { it('should return false if no activeNode and waiting node in workflow and waiting node is disabled', () => {
workflowsStore.workflow.nodes = [ workflowsStore.setNodes([
{ type: FORM_NODE_TYPE, disabled: true }, { type: FORM_NODE_TYPE, disabled: true },
{ type: 'type2' }, { type: 'type2' },
] as unknown as IWorkflowDb['nodes']; ] as unknown as IWorkflowDb['nodes']);
const isWaiting = workflowsStore.isWaitingExecution; const isWaiting = workflowsStore.isWaitingExecution;
expect(isWaiting).toEqual(false); expect(isWaiting).toEqual(false);
}); });
it('should return true if no activeNode and wait node in workflow', () => { it('should return true if no activeNode and wait node in workflow', () => {
workflowsStore.workflow.nodes = [ workflowsStore.setNodes([
{ type: WAIT_NODE_TYPE }, { type: WAIT_NODE_TYPE },
{ type: 'type2' }, { type: 'type2' },
] as unknown as IWorkflowDb['nodes']; ] as unknown as IWorkflowDb['nodes']);
const isWaiting = workflowsStore.isWaitingExecution; const isWaiting = workflowsStore.isWaitingExecution;
expect(isWaiting).toEqual(true); expect(isWaiting).toEqual(true);
}); });
it('should return true if no activeNode and form node in workflow', () => { it('should return true if no activeNode and form node in workflow', () => {
workflowsStore.workflow.nodes = [ workflowsStore.setNodes([
{ type: FORM_NODE_TYPE }, { type: FORM_NODE_TYPE },
{ type: 'type2' }, { type: 'type2' },
] as unknown as IWorkflowDb['nodes']; ] as unknown as IWorkflowDb['nodes']);
const isWaiting = workflowsStore.isWaitingExecution; const isWaiting = workflowsStore.isWaitingExecution;
expect(isWaiting).toEqual(true); expect(isWaiting).toEqual(true);
}); });
it('should return true if no activeNode and sendAndWait node in workflow', () => { it('should return true if no activeNode and sendAndWait node in workflow', () => {
workflowsStore.workflow.nodes = [ workflowsStore.setNodes([
{ type: 'type1', parameters: { operation: SEND_AND_WAIT_OPERATION } }, { type: 'type1', parameters: { operation: SEND_AND_WAIT_OPERATION } },
{ type: 'type2' }, { type: 'type2' },
] as unknown as IWorkflowDb['nodes']; ] as unknown as IWorkflowDb['nodes']);
const isWaiting = workflowsStore.isWaitingExecution; const isWaiting = workflowsStore.isWaitingExecution;
expect(isWaiting).toEqual(true); expect(isWaiting).toEqual(true);
@@ -180,22 +186,30 @@ describe('useWorkflowsStore', () => {
describe('workflowTriggerNodes', () => { describe('workflowTriggerNodes', () => {
it('should return only nodes that are triggers', () => { it('should return only nodes that are triggers', () => {
getNodeType.mockReturnValueOnce({ group: ['trigger'] }); getNodeType.mockImplementation(
(nodeTypeName: string) =>
({
group: nodeTypeName === 'triggerNode' ? ['trigger'] : [],
inputs: [],
webhooks: [],
properties: [],
}) as Partial<INodeTypeDescription> | null,
);
workflowsStore.workflow.nodes = [ workflowsStore.setNodes([
{ type: 'triggerNode', typeVersion: '1' }, { type: 'triggerNode', typeVersion: '1' },
{ type: 'nonTriggerNode', typeVersion: '1' }, { type: 'nonTriggerNode', typeVersion: '1' },
] as unknown as IWorkflowDb['nodes']; ] as unknown as IWorkflowDb['nodes']);
expect(workflowsStore.workflowTriggerNodes).toHaveLength(1); expect(workflowsStore.workflowTriggerNodes).toHaveLength(1);
expect(workflowsStore.workflowTriggerNodes[0].type).toBe('triggerNode'); expect(workflowsStore.workflowTriggerNodes[0].type).toBe('triggerNode');
}); });
it('should return empty array when no nodes are triggers', () => { it('should return empty array when no nodes are triggers', () => {
workflowsStore.workflow.nodes = [ workflowsStore.setNodes([
{ type: 'nonTriggerNode1', typeVersion: '1' }, { type: 'nonTriggerNode1', typeVersion: '1' },
{ type: 'nonTriggerNode2', typeVersion: '1' }, { type: 'nonTriggerNode2', typeVersion: '1' },
] as unknown as IWorkflowDb['nodes']; ] as unknown as IWorkflowDb['nodes']);
expect(workflowsStore.workflowTriggerNodes).toHaveLength(0); expect(workflowsStore.workflowTriggerNodes).toHaveLength(0);
}); });
@@ -203,20 +217,20 @@ describe('useWorkflowsStore', () => {
describe('currentWorkflowHasWebhookNode', () => { describe('currentWorkflowHasWebhookNode', () => {
it('should return true when a node has a webhookId', () => { it('should return true when a node has a webhookId', () => {
workflowsStore.workflow.nodes = [ workflowsStore.setNodes([
{ name: 'Node1', webhookId: 'webhook1' }, { name: 'Node1', webhookId: 'webhook1' },
{ name: 'Node2' }, { name: 'Node2' },
] as unknown as IWorkflowDb['nodes']; ] as unknown as IWorkflowDb['nodes']);
const hasWebhookNode = workflowsStore.currentWorkflowHasWebhookNode; const hasWebhookNode = workflowsStore.currentWorkflowHasWebhookNode;
expect(hasWebhookNode).toBe(true); expect(hasWebhookNode).toBe(true);
}); });
it('should return false when no nodes have a webhookId', () => { it('should return false when no nodes have a webhookId', () => {
workflowsStore.workflow.nodes = [ workflowsStore.setNodes([
{ name: 'Node1' }, { name: 'Node1' },
{ name: 'Node2' }, { name: 'Node2' },
] as unknown as IWorkflowDb['nodes']; ] as unknown as IWorkflowDb['nodes']);
const hasWebhookNode = workflowsStore.currentWorkflowHasWebhookNode; const hasWebhookNode = workflowsStore.currentWorkflowHasWebhookNode;
expect(hasWebhookNode).toBe(false); expect(hasWebhookNode).toBe(false);
@@ -258,38 +272,38 @@ describe('useWorkflowsStore', () => {
describe('nodesIssuesExist', () => { describe('nodesIssuesExist', () => {
it('should return true when a node has issues and connected', () => { it('should return true when a node has issues and connected', () => {
workflowsStore.workflow.nodes = [ workflowsStore.setNodes([
{ name: 'Node1', issues: { error: ['Error message'] } }, { name: 'Node1', issues: { error: ['Error message'] } },
{ name: 'Node2' }, { name: 'Node2' },
] as unknown as IWorkflowDb['nodes']; ] as unknown as IWorkflowDb['nodes']);
workflowsStore.workflow.connections = { workflowsStore.setConnections({
Node1: { main: [[{ node: 'Node2' } as IConnection]] }, Node1: { main: [[{ node: 'Node2' } as IConnection]] },
}; });
const hasIssues = workflowsStore.nodesIssuesExist; const hasIssues = workflowsStore.nodesIssuesExist;
expect(hasIssues).toBe(true); expect(hasIssues).toBe(true);
}); });
it('should return false when node has issues but it is not connected', () => { it('should return false when node has issues but it is not connected', () => {
workflowsStore.workflow.nodes = [ workflowsStore.setNodes([
{ name: 'Node1', issues: { error: ['Error message'] } }, { name: 'Node1', issues: { error: ['Error message'] } },
{ name: 'Node2' }, { name: 'Node2' },
] as unknown as IWorkflowDb['nodes']; ] as unknown as IWorkflowDb['nodes']);
const hasIssues = workflowsStore.nodesIssuesExist; const hasIssues = workflowsStore.nodesIssuesExist;
expect(hasIssues).toBe(false); expect(hasIssues).toBe(false);
}); });
it('should return false when no nodes have issues', () => { it('should return false when no nodes have issues', () => {
workflowsStore.workflow.nodes = [ workflowsStore.setNodes([
{ name: 'Node1' }, { name: 'Node1' },
{ name: 'Node2' }, { name: 'Node2' },
] as unknown as IWorkflowDb['nodes']; ] as unknown as IWorkflowDb['nodes']);
workflowsStore.workflow.connections = { workflowsStore.setConnections({
Node1: { main: [[{ node: 'Node2' } as IConnection]] }, Node1: { main: [[{ node: 'Node2' } as IConnection]] },
}; });
const hasIssues = workflowsStore.nodesIssuesExist; const hasIssues = workflowsStore.nodesIssuesExist;
expect(hasIssues).toBe(false); expect(hasIssues).toBe(false);
@@ -353,65 +367,65 @@ describe('useWorkflowsStore', () => {
describe('isNodeInOutgoingNodeConnections()', () => { describe('isNodeInOutgoingNodeConnections()', () => {
it('should return false when no outgoing connections from root node', () => { it('should return false when no outgoing connections from root node', () => {
workflowsStore.workflow.connections = {}; workflowsStore.setConnections({});
const result = workflowsStore.isNodeInOutgoingNodeConnections('RootNode', 'SearchNode'); const result = workflowsStore.isNodeInOutgoingNodeConnections('RootNode', 'SearchNode');
expect(result).toBe(false); expect(result).toBe(false);
}); });
it('should return true when search node is directly connected to root node', () => { it('should return true when search node is directly connected to root node', () => {
workflowsStore.workflow.connections = { workflowsStore.setConnections({
RootNode: { main: [[{ node: 'SearchNode' } as IConnection]] }, RootNode: { main: [[{ node: 'SearchNode' } as IConnection]] },
}; });
const result = workflowsStore.isNodeInOutgoingNodeConnections('RootNode', 'SearchNode'); const result = workflowsStore.isNodeInOutgoingNodeConnections('RootNode', 'SearchNode');
expect(result).toBe(true); expect(result).toBe(true);
}); });
it('should return true when search node is indirectly connected to root node', () => { it('should return true when search node is indirectly connected to root node', () => {
workflowsStore.workflow.connections = { workflowsStore.setConnections({
RootNode: { main: [[{ node: 'IntermediateNode' } as IConnection]] }, RootNode: { main: [[{ node: 'IntermediateNode' } as IConnection]] },
IntermediateNode: { main: [[{ node: 'SearchNode' } as IConnection]] }, IntermediateNode: { main: [[{ node: 'SearchNode' } as IConnection]] },
}; });
const result = workflowsStore.isNodeInOutgoingNodeConnections('RootNode', 'SearchNode'); const result = workflowsStore.isNodeInOutgoingNodeConnections('RootNode', 'SearchNode');
expect(result).toBe(true); expect(result).toBe(true);
}); });
it('should return false when search node is not connected to root node', () => { it('should return false when search node is not connected to root node', () => {
workflowsStore.workflow.connections = { workflowsStore.setConnections({
RootNode: { main: [[{ node: 'IntermediateNode' } as IConnection]] }, RootNode: { main: [[{ node: 'IntermediateNode' } as IConnection]] },
IntermediateNode: { main: [[{ node: 'AnotherNode' } as IConnection]] }, IntermediateNode: { main: [[{ node: 'AnotherNode' } as IConnection]] },
}; });
const result = workflowsStore.isNodeInOutgoingNodeConnections('RootNode', 'SearchNode'); const result = workflowsStore.isNodeInOutgoingNodeConnections('RootNode', 'SearchNode');
expect(result).toBe(false); expect(result).toBe(false);
}); });
it('should return true if connection is indirect within `depth`', () => { it('should return true if connection is indirect within `depth`', () => {
workflowsStore.workflow.connections = { workflowsStore.setConnections({
RootNode: { main: [[{ node: 'IntermediateNode' } as IConnection]] }, RootNode: { main: [[{ node: 'IntermediateNode' } as IConnection]] },
IntermediateNode: { main: [[{ node: 'SearchNode' } as IConnection]] }, IntermediateNode: { main: [[{ node: 'SearchNode' } as IConnection]] },
}; });
const result = workflowsStore.isNodeInOutgoingNodeConnections('RootNode', 'SearchNode', 2); const result = workflowsStore.isNodeInOutgoingNodeConnections('RootNode', 'SearchNode', 2);
expect(result).toBe(true); expect(result).toBe(true);
}); });
it('should return false if connection is indirect beyond `depth`', () => { it('should return false if connection is indirect beyond `depth`', () => {
workflowsStore.workflow.connections = { workflowsStore.setConnections({
RootNode: { main: [[{ node: 'IntermediateNode' } as IConnection]] }, RootNode: { main: [[{ node: 'IntermediateNode' } as IConnection]] },
IntermediateNode: { main: [[{ node: 'SearchNode' } as IConnection]] }, IntermediateNode: { main: [[{ node: 'SearchNode' } as IConnection]] },
}; });
const result = workflowsStore.isNodeInOutgoingNodeConnections('RootNode', 'SearchNode', 1); const result = workflowsStore.isNodeInOutgoingNodeConnections('RootNode', 'SearchNode', 1);
expect(result).toBe(false); expect(result).toBe(false);
}); });
it('should return false if depth is 0', () => { it('should return false if depth is 0', () => {
workflowsStore.workflow.connections = { workflowsStore.setConnections({
RootNode: { main: [[{ node: 'SearchNode' } as IConnection]] }, RootNode: { main: [[{ node: 'SearchNode' } as IConnection]] },
}; });
const result = workflowsStore.isNodeInOutgoingNodeConnections('RootNode', 'SearchNode', 0); const result = workflowsStore.isNodeInOutgoingNodeConnections('RootNode', 'SearchNode', 0);
expect(result).toBe(false); expect(result).toBe(false);
@@ -632,6 +646,7 @@ describe('useWorkflowsStore', () => {
}); });
it('should add node error event and track errored executions', async () => { it('should add node error event and track errored executions', async () => {
workflowsStore.workflow.pinData = {};
workflowsStore.setWorkflowExecutionData(executionResponse); workflowsStore.setWorkflowExecutionData(executionResponse);
workflowsStore.addNode({ workflowsStore.addNode({
parameters: {}, parameters: {},
@@ -1092,6 +1107,8 @@ describe('useWorkflowsStore', () => {
it('should not update last parameter update time if parameters are set to the same value', () => { it('should not update last parameter update time if parameters are set to the same value', () => {
expect(workflowsStore.getParametersLastUpdate('a')).toEqual(undefined); expect(workflowsStore.getParametersLastUpdate('a')).toEqual(undefined);
console.log(workflowsStore.workflow.nodes, workflowsStore.workflowObject.nodes);
workflowsStore.setNodeParameters({ name: 'a', value: { p: 1, q: true } }); workflowsStore.setNodeParameters({ name: 'a', value: { p: 1, q: true } });
expect(workflowsStore.getParametersLastUpdate('a')).toEqual(undefined); expect(workflowsStore.getParametersLastUpdate('a')).toEqual(undefined);
@@ -1294,7 +1311,7 @@ describe('useWorkflowsStore', () => {
}); });
}); });
function getMockEditFieldsNode() { function getMockEditFieldsNode(): Partial<INodeTypeDescription> {
return { return {
displayName: 'Edit Fields (Set)', displayName: 'Edit Fields (Set)',
name: 'n8n-nodes-base.set', name: 'n8n-nodes-base.set',

View File

@@ -127,9 +127,6 @@ const createEmptyWorkflow = (): IWorkflowDb => ({
...defaults, ...defaults,
}); });
let cachedWorkflowKey: string | null = '';
let cachedWorkflow: Workflow | null = null;
export const useWorkflowsStore = defineStore(STORES.WORKFLOWS, () => { export const useWorkflowsStore = defineStore(STORES.WORKFLOWS, () => {
const uiStore = useUIStore(); const uiStore = useUIStore();
const telemetry = useTelemetry(); const telemetry = useTelemetry();
@@ -142,6 +139,11 @@ export const useWorkflowsStore = defineStore(STORES.WORKFLOWS, () => {
const version = computed(() => settingsStore.partialExecutionVersion); const version = computed(() => settingsStore.partialExecutionVersion);
const workflow = ref<IWorkflowDb>(createEmptyWorkflow()); const workflow = ref<IWorkflowDb>(createEmptyWorkflow());
const workflowObject = ref<Workflow>(
// eslint-disable-next-line @typescript-eslint/no-use-before-define
createWorkflowObject(workflow.value.nodes, workflow.value.connections),
);
// For paginated workflow lists // For paginated workflow lists
const totalWorkflowCount = ref(0); const totalWorkflowCount = ref(0);
const usedCredentials = ref<Record<string, IUsedCredential>>({}); const usedCredentials = ref<Record<string, IUsedCredential>>({});
@@ -228,11 +230,10 @@ export const useWorkflowsStore = defineStore(STORES.WORKFLOWS, () => {
if (activeNode) { if (activeNode) {
if (willNodeWait(activeNode)) return true; if (willNodeWait(activeNode)) return true;
const workflow = getCurrentWorkflow(); const parentNodes = workflowObject.value.getParentNodes(activeNode.name);
const parentNodes = workflow.getParentNodes(activeNode.name);
for (const parentNode of parentNodes) { for (const parentNode of parentNodes) {
if (willNodeWait(workflow.nodes[parentNode])) { if (willNodeWait(workflowObject.value.nodes[parentNode])) {
return true; return true;
} }
} }
@@ -509,55 +510,35 @@ export const useWorkflowsStore = defineStore(STORES.WORKFLOWS, () => {
}; };
} }
function updateCachedWorkflow() { function createWorkflowObject(
nodes: INodeUi[],
connections: IConnections,
copyData?: boolean,
): Workflow {
const nodeTypes = getNodeTypes(); const nodeTypes = getNodeTypes();
const nodes = getNodes();
const connections = allConnections.value;
cachedWorkflow = new Workflow({ let id: string | undefined = workflow.value.id;
id: workflowId.value, if (id && id === PLACEHOLDER_EMPTY_WORKFLOW_ID) {
name: workflowName.value, id = undefined;
nodes,
connections,
active: false,
nodeTypes,
settings: workflowSettings.value,
pinData: pinnedWorkflowData.value,
});
}
function getWorkflow(nodes: INodeUi[], connections: IConnections, copyData?: boolean): Workflow {
const nodeTypes = getNodeTypes();
let cachedWorkflowId: string | undefined = workflowId.value;
if (cachedWorkflowId && cachedWorkflowId === PLACEHOLDER_EMPTY_WORKFLOW_ID) {
cachedWorkflowId = undefined;
} }
cachedWorkflow = new Workflow({ return new Workflow({
id: cachedWorkflowId, id,
name: workflowName.value, name: workflow.value.name,
nodes: copyData ? deepCopy(nodes) : nodes, nodes: copyData ? deepCopy(nodes) : nodes,
connections: copyData ? deepCopy(connections) : connections, connections: copyData ? deepCopy(connections) : connections,
active: false, active: false,
nodeTypes, nodeTypes,
settings: workflowSettings.value, settings: workflow.value.settings ?? { ...defaults.settings },
pinData: pinnedWorkflowData.value, pinData: workflow.value.pinData,
}); });
return cachedWorkflow;
} }
function getCurrentWorkflow(copyData?: boolean): Workflow { function cloneWorkflowObject(): Workflow {
const nodes = getNodes(); const nodes = getNodes();
const connections = allConnections.value; const connections = allConnections.value;
const cacheKey = JSON.stringify({ nodes, connections });
if (!copyData && cachedWorkflow && cacheKey === cachedWorkflowKey) {
return cachedWorkflow;
}
cachedWorkflowKey = cacheKey;
return getWorkflow(nodes, connections, copyData); return createWorkflowObject(nodes, connections);
} }
async function getWorkflowFromUrl(url: string): Promise<IWorkflowDb> { async function getWorkflowFromUrl(url: string): Promise<IWorkflowDb> {
@@ -623,7 +604,10 @@ export const useWorkflowsStore = defineStore(STORES.WORKFLOWS, () => {
async function searchWorkflows({ async function searchWorkflows({
projectId, projectId,
name, name,
}: { projectId?: string; name?: string }): Promise<IWorkflowDb[]> { }: {
projectId?: string;
name?: string;
}): Promise<IWorkflowDb[]> {
const filter = { const filter = {
projectId, projectId,
name, name,
@@ -725,6 +709,7 @@ export const useWorkflowsStore = defineStore(STORES.WORKFLOWS, () => {
function setWorkflowId(id?: string) { function setWorkflowId(id?: string) {
workflow.value.id = !id || id === 'new' ? PLACEHOLDER_EMPTY_WORKFLOW_ID : id; workflow.value.id = !id || id === 'new' ? PLACEHOLDER_EMPTY_WORKFLOW_ID : id;
workflowObject.value.id = workflow.value.id;
} }
function setUsedCredentials(data: IUsedCredential[]) { function setUsedCredentials(data: IUsedCredential[]) {
@@ -932,10 +917,8 @@ export const useWorkflowsStore = defineStore(STORES.WORKFLOWS, () => {
} }
function setWorkflowSettings(workflowSettings: IWorkflowSettings) { function setWorkflowSettings(workflowSettings: IWorkflowSettings) {
workflow.value = { workflow.value.settings = workflowSettings as IWorkflowDb['settings'];
...workflow.value, workflowObject.value.setSettings(workflowSettings);
settings: workflowSettings as IWorkflowDb['settings'],
};
} }
function setWorkflowPinData(data: IPinData = {}) { function setWorkflowPinData(data: IPinData = {}) {
@@ -952,7 +935,7 @@ export const useWorkflowsStore = defineStore(STORES.WORKFLOWS, () => {
}, {} as IPinData); }, {} as IPinData);
workflow.value.pinData = validPinData; workflow.value.pinData = validPinData;
updateCachedWorkflow(); workflowObject.value.setPinData(validPinData);
dataPinningEventBus.emit('pin-data', validPinData); dataPinningEventBus.emit('pin-data', validPinData);
} }
@@ -962,19 +945,15 @@ export const useWorkflowsStore = defineStore(STORES.WORKFLOWS, () => {
} }
function addWorkflowTagIds(tags: string[]) { function addWorkflowTagIds(tags: string[]) {
workflow.value = { workflow.value.tags = [
...workflow.value, ...new Set([...(workflow.value.tags ?? []), ...tags]),
tags: [...new Set([...(workflow.value.tags ?? []), ...tags])] as IWorkflowDb['tags'], ] as IWorkflowDb['tags'];
};
} }
function removeWorkflowTagId(tagId: string) { function removeWorkflowTagId(tagId: string) {
const tags = workflow.value.tags as string[]; const tags = workflow.value.tags as string[];
const updated = tags.filter((id: string) => id !== tagId); const updated = tags.filter((id: string) => id !== tagId);
workflow.value = { workflow.value.tags = updated as IWorkflowDb['tags'];
...workflow.value,
tags: updated as IWorkflowDb['tags'],
};
} }
function setWorkflowScopes(scopes: IWorkflowDb['scopes']): void { function setWorkflowScopes(scopes: IWorkflowDb['scopes']): void {
@@ -1003,13 +982,18 @@ export const useWorkflowsStore = defineStore(STORES.WORKFLOWS, () => {
...(!value.hasOwnProperty('nodes') ? { nodes: [] } : {}), ...(!value.hasOwnProperty('nodes') ? { nodes: [] } : {}),
...(!value.hasOwnProperty('settings') ? { settings: { ...defaults.settings } } : {}), ...(!value.hasOwnProperty('settings') ? { settings: { ...defaults.settings } } : {}),
}; };
workflowObject.value = createWorkflowObject(
workflow.value.nodes,
workflow.value.connections,
true,
);
} }
function pinData(payload: { node: INodeUi; data: INodeExecutionData[] }): void { function pinData(payload: { node: INodeUi; data: INodeExecutionData[] }): void {
const nodeName = payload.node.name; const nodeName = payload.node.name;
if (!workflow.value.pinData) { if (!workflow.value.pinData) {
workflow.value = { ...workflow.value, pinData: {} }; workflow.value.pinData = {};
} }
if (!Array.isArray(payload.data)) { if (!Array.isArray(payload.data)) {
@@ -1025,16 +1009,10 @@ export const useWorkflowsStore = defineStore(STORES.WORKFLOWS, () => {
isJsonKeyObject(item) ? { json: item.json } : { json: item }, isJsonKeyObject(item) ? { json: item.json } : { json: item },
); );
workflow.value = { workflow.value.pinData[nodeName] = storedPinData;
...workflow.value, workflowObject.value.setPinData(workflow.value.pinData);
pinData: {
...workflow.value.pinData,
[nodeName]: storedPinData,
},
};
uiStore.stateIsDirty = true; uiStore.stateIsDirty = true;
updateCachedWorkflow();
dataPinningEventBus.emit('pin-data', { [payload.node.name]: storedPinData }); dataPinningEventBus.emit('pin-data', { [payload.node.name]: storedPinData });
} }
@@ -1043,21 +1021,18 @@ export const useWorkflowsStore = defineStore(STORES.WORKFLOWS, () => {
const nodeName = payload.node.name; const nodeName = payload.node.name;
if (!workflow.value.pinData) { if (!workflow.value.pinData) {
workflow.value = { ...workflow.value, pinData: {} }; workflow.value.pinData = {};
} }
const { [nodeName]: _, ...pinData } = workflow.value.pinData as IPinData; const { [nodeName]: _, ...pinData } = workflow.value.pinData as IPinData;
workflow.value = { workflow.value.pinData = pinData;
...workflow.value, workflowObject.value.setPinData(pinData);
pinData,
};
if (nodeMetadata.value[nodeName]) { if (nodeMetadata.value[nodeName]) {
nodeMetadata.value[nodeName].pinnedDataLastRemovedAt = Date.now(); nodeMetadata.value[nodeName].pinnedDataLastRemovedAt = Date.now();
} }
uiStore.stateIsDirty = true; uiStore.stateIsDirty = true;
updateCachedWorkflow();
dataPinningEventBus.emit('unpin-data', { dataPinningEventBus.emit('unpin-data', {
nodeNames: [nodeName], nodeNames: [nodeName],
@@ -1076,25 +1051,13 @@ export const useWorkflowsStore = defineStore(STORES.WORKFLOWS, () => {
// Check if source node and type exist already and if not add them // Check if source node and type exist already and if not add them
if (!workflow.value.connections.hasOwnProperty(sourceData.node)) { if (!workflow.value.connections.hasOwnProperty(sourceData.node)) {
workflow.value = { workflow.value.connections[sourceData.node] = {};
...workflow.value,
connections: {
...workflow.value.connections,
[sourceData.node]: {},
},
};
} }
if (!workflow.value.connections[sourceData.node].hasOwnProperty(sourceData.type)) { if (!workflow.value.connections[sourceData.node].hasOwnProperty(sourceData.type)) {
workflow.value = { workflow.value.connections[sourceData.node] = {
...workflow.value, ...workflow.value.connections[sourceData.node],
connections: { [sourceData.type]: [],
...workflow.value.connections,
[sourceData.node]: {
...workflow.value.connections[sourceData.node],
[sourceData.type]: [],
},
},
}; };
} }
@@ -1139,6 +1102,8 @@ export const useWorkflowsStore = defineStore(STORES.WORKFLOWS, () => {
connections.push(destinationData); connections.push(destinationData);
} }
} }
workflowObject.value.setConnections(workflow.value.connections);
} }
function removeConnection(data: { connection: IConnection[] }): void { function removeConnection(data: { connection: IConnection[] }): void {
@@ -1178,6 +1143,8 @@ export const useWorkflowsStore = defineStore(STORES.WORKFLOWS, () => {
connections.splice(parseInt(index, 10), 1); connections.splice(parseInt(index, 10), 1);
} }
} }
workflowObject.value.setConnections(workflow.value.connections);
} }
function removeAllConnections(data: { setStateDirty: boolean }): void { function removeAllConnections(data: { setStateDirty: boolean }): void {
@@ -1186,6 +1153,7 @@ export const useWorkflowsStore = defineStore(STORES.WORKFLOWS, () => {
} }
workflow.value.connections = {}; workflow.value.connections = {};
workflowObject.value.setConnections({});
} }
function removeAllNodeConnection( function removeAllNodeConnection(
@@ -1229,6 +1197,8 @@ export const useWorkflowsStore = defineStore(STORES.WORKFLOWS, () => {
} }
} }
} }
workflowObject.value.setConnections(workflow.value.connections);
} }
function renameNodeSelectedAndExecution(nameData: { old: string; new: string }): void { function renameNodeSelectedAndExecution(nameData: { old: string; new: string }): void {
@@ -1252,13 +1222,12 @@ export const useWorkflowsStore = defineStore(STORES.WORKFLOWS, () => {
if (workflow.value.pinData?.[nameData.old]) { if (workflow.value.pinData?.[nameData.old]) {
const { [nameData.old]: renamed, ...restPinData } = workflow.value.pinData; const { [nameData.old]: renamed, ...restPinData } = workflow.value.pinData;
workflow.value = { workflow.value.pinData = {
...workflow.value, ...restPinData,
pinData: { [nameData.new]: renamed,
...restPinData,
[nameData.new]: renamed,
},
}; };
workflowObject.value.setPinData(workflow.value.pinData);
} }
const resultData = workflowExecutionData.value?.data?.resultData; const resultData = workflowExecutionData.value?.data?.resultData;
@@ -1295,14 +1264,10 @@ export const useWorkflowsStore = defineStore(STORES.WORKFLOWS, () => {
} }
function setParentFolder(folder: IWorkflowDb['parentFolder']) { function setParentFolder(folder: IWorkflowDb['parentFolder']) {
workflow.value = { workflow.value.parentFolder = folder;
...workflow.value,
parentFolder: folder,
};
} }
function setNodes(nodes: INodeUi[]): void { function setNodes(nodes: INodeUi[]): void {
workflow.value.nodes = nodes;
nodes.forEach((node) => { nodes.forEach((node) => {
if (!node.id) { if (!node.id) {
nodeHelpers.assignNodeId(node); nodeHelpers.assignNodeId(node);
@@ -1320,14 +1285,14 @@ export const useWorkflowsStore = defineStore(STORES.WORKFLOWS, () => {
nodeMetadata.value[node.name] = { pristine: true }; nodeMetadata.value[node.name] = { pristine: true };
} }
}); });
workflow.value.nodes = nodes;
workflowObject.value.setNodes(nodes);
} }
function setConnections(connections: IConnections, updateWorkflow = false): void { function setConnections(value: IConnections): void {
workflow.value.connections = connections; workflow.value.connections = value;
workflowObject.value.setConnections(value);
if (updateWorkflow) {
updateCachedWorkflow();
}
} }
function resetAllNodesIssues(): boolean { function resetAllNodesIssues(): boolean {
@@ -1343,8 +1308,15 @@ export const useWorkflowsStore = defineStore(STORES.WORKFLOWS, () => {
function updateNodeAtIndex(nodeIndex: number, nodeData: Partial<INodeUi>): boolean { function updateNodeAtIndex(nodeIndex: number, nodeData: Partial<INodeUi>): boolean {
if (nodeIndex !== -1) { if (nodeIndex !== -1) {
const node = workflow.value.nodes[nodeIndex]; const node = workflow.value.nodes[nodeIndex];
const changed = !isEqual(pick(node, Object.keys(nodeData)), nodeData); const existingData = pick<Partial<INodeUi>>(node, Object.keys(nodeData));
Object.assign(node, nodeData); const changed = !isEqual(existingData, nodeData);
if (changed) {
Object.assign(node, nodeData);
workflow.value.nodes[nodeIndex] = node;
workflowObject.value.setNodes(workflow.value.nodes);
}
return changed; return changed;
} }
return false; return false;
@@ -1389,6 +1361,8 @@ export const useWorkflowsStore = defineStore(STORES.WORKFLOWS, () => {
} }
workflow.value.nodes.push(nodeData); workflow.value.nodes.push(nodeData);
workflowObject.value.setNodes(workflow.value.nodes);
// Init node metadata // Init node metadata
if (!nodeMetadata.value[nodeData.name]) { if (!nodeMetadata.value[nodeData.name]) {
nodeMetadata.value[nodeData.name] = {} as INodeMetadata; nodeMetadata.value[nodeData.name] = {} as INodeMetadata;
@@ -1401,19 +1375,16 @@ export const useWorkflowsStore = defineStore(STORES.WORKFLOWS, () => {
if (workflow.value.pinData?.hasOwnProperty(node.name)) { if (workflow.value.pinData?.hasOwnProperty(node.name)) {
const { [node.name]: removedPinData, ...remainingPinData } = workflow.value.pinData; const { [node.name]: removedPinData, ...remainingPinData } = workflow.value.pinData;
workflow.value = { workflow.value.pinData = remainingPinData;
...workflow.value,
pinData: remainingPinData,
};
} }
for (let i = 0; i < workflow.value.nodes.length; i++) { for (let i = 0; i < workflow.value.nodes.length; i++) {
if (workflow.value.nodes[i].name === node.name) { if (workflow.value.nodes[i].name === node.name) {
workflow.value = { workflow.value.nodes = [
...workflow.value, ...workflow.value.nodes.slice(0, i),
nodes: [...workflow.value.nodes.slice(0, i), ...workflow.value.nodes.slice(i + 1)], ...workflow.value.nodes.slice(i + 1),
}; ];
workflowObject.value.setNodes(workflow.value.nodes);
uiStore.stateIsDirty = true; uiStore.stateIsDirty = true;
return; return;
} }
@@ -1426,13 +1397,11 @@ export const useWorkflowsStore = defineStore(STORES.WORKFLOWS, () => {
} }
if (data.removePinData) { if (data.removePinData) {
workflow.value = { workflow.value.pinData = {};
...workflow.value,
pinData: {},
};
} }
workflow.value.nodes.splice(0, workflow.value.nodes.length); workflow.value.nodes.splice(0, workflow.value.nodes.length);
workflowObject.value.setNodes(workflow.value.nodes);
nodeMetadata.value = {}; nodeMetadata.value = {};
} }
@@ -1842,8 +1811,7 @@ export const useWorkflowsStore = defineStore(STORES.WORKFLOWS, () => {
} }
function checkIfNodeHasChatParent(nodeName: string): boolean { function checkIfNodeHasChatParent(nodeName: string): boolean {
const workflow = getCurrentWorkflow(); const parents = workflowObject.value.getParentNodes(nodeName, NodeConnectionTypes.Main);
const parents = workflow.getParentNodes(nodeName, NodeConnectionTypes.Main);
const matchedChatNode = parents.find((parent) => { const matchedChatNode = parents.find((parent) => {
const parentNodeType = getNodeByName(parent)?.type; const parentNodeType = getNodeByName(parent)?.type;
@@ -2009,8 +1977,9 @@ export const useWorkflowsStore = defineStore(STORES.WORKFLOWS, () => {
getNodeTypes, getNodeTypes,
getNodes, getNodes,
convertTemplateNodeToNodeUi, convertTemplateNodeToNodeUi,
getWorkflow, workflowObject,
getCurrentWorkflow, createWorkflowObject,
cloneWorkflowObject,
getWorkflowFromUrl, getWorkflowFromUrl,
getActivationError, getActivationError,
searchWorkflows, searchWorkflows,

View File

@@ -67,9 +67,9 @@ export class Workflow {
nodes: INodes = {}; nodes: INodes = {};
connectionsBySourceNode: IConnections; connectionsBySourceNode: IConnections = {};
connectionsByDestinationNode: IConnections; connectionsByDestinationNode: IConnections = {};
nodeTypes: INodeTypes; nodeTypes: INodeTypes;
@@ -77,7 +77,7 @@ export class Workflow {
active: boolean; active: boolean;
settings: IWorkflowSettings; settings: IWorkflowSettings = {};
readonly timezone: string; readonly timezone: string;
@@ -93,15 +93,9 @@ export class Workflow {
this.id = parameters.id as string; // @tech_debt Ensure this is not optional this.id = parameters.id as string; // @tech_debt Ensure this is not optional
this.name = parameters.name; this.name = parameters.name;
this.nodeTypes = parameters.nodeTypes; this.nodeTypes = parameters.nodeTypes;
this.pinData = parameters.pinData;
// Save nodes in workflow as object to be able to get the
// nodes easily by its name.
// Also directly add the default values of the node type.
let nodeType: INodeType | undefined; let nodeType: INodeType | undefined;
for (const node of parameters.nodes) { for (const node of parameters.nodes) {
this.nodes[node.name] = node;
nodeType = this.nodeTypes.getByNameAndVersion(node.type, node.typeVersion); nodeType = this.nodeTypes.getByNameAndVersion(node.type, node.typeVersion);
if (nodeType === undefined) { if (nodeType === undefined) {
@@ -127,10 +121,11 @@ export class Workflow {
); );
node.parameters = nodeParameters !== null ? nodeParameters : {}; node.parameters = nodeParameters !== null ? nodeParameters : {};
} }
this.connectionsBySourceNode = parameters.connections;
// Save also the connections by the destination nodes this.setNodes(parameters.nodes);
this.connectionsByDestinationNode = mapConnectionsByDestination(parameters.connections); this.setConnections(parameters.connections);
this.setPinData(parameters.pinData);
this.setSettings(parameters.settings ?? {});
this.active = parameters.active || false; this.active = parameters.active || false;
@@ -138,12 +133,32 @@ export class Workflow {
ignoreEmptyOnFirstChild: true, ignoreEmptyOnFirstChild: true,
}); });
this.settings = parameters.settings || {};
this.timezone = this.settings.timezone ?? getGlobalState().defaultTimezone; this.timezone = this.settings.timezone ?? getGlobalState().defaultTimezone;
this.expression = new Expression(this); this.expression = new Expression(this);
} }
// Save nodes in workflow as object to be able to get the nodes easily by their name.
setNodes(nodes: INode[]) {
this.nodes = {};
for (const node of nodes) {
this.nodes[node.name] = node;
}
}
setConnections(connections: IConnections) {
this.connectionsBySourceNode = connections;
this.connectionsByDestinationNode = mapConnectionsByDestination(this.connectionsBySourceNode);
}
setPinData(pinData: IPinData | undefined) {
this.pinData = pinData;
}
setSettings(settings: IWorkflowSettings) {
this.settings = settings;
}
overrideStaticData(staticData?: IDataObject) { overrideStaticData(staticData?: IDataObject) {
this.staticData = ObservableObject.create(staticData || {}, undefined, { this.staticData = ObservableObject.create(staticData || {}, undefined, {
ignoreEmptyOnFirstChild: true, ignoreEmptyOnFirstChild: true,
@@ -471,9 +486,6 @@ export class Workflow {
} }
} }
} }
// Use the updated connections to create updated connections by destination nodes
this.connectionsByDestinationNode = mapConnectionsByDestination(this.connectionsBySourceNode);
} }
/** /**