refactor(editor): Delete workflow ref from workflows.store.ts (#29531)

This commit is contained in:
Suguru Inoue 2026-05-08 10:54:35 +02:00 committed by GitHub
parent 33c3598e66
commit 149bdebf37
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
68 changed files with 1006 additions and 1345 deletions

View File

@ -1,5 +1,5 @@
import { createCanvasGraphNode } from '@/features/workflows/canvas/__tests__/utils';
import { createTestNode, createTestWorkflow, mockNodeTypeDescription } from '@/__tests__/mocks';
import { createTestNode, mockNodeTypeDescription } from '@/__tests__/mocks';
import { createComponentRenderer } from '@/__tests__/render';
import { mockedStore } from '@/__tests__/utils';
import { SET_NODE_TYPE } from '@/app/constants';
@ -83,7 +83,7 @@ describe('FocusPanel', () => {
}),
]);
workflowsStore = useWorkflowsStore(pinia);
workflowsStore.workflow = createTestWorkflow({ id: 'w0' });
workflowsStore.setWorkflowId('w0');
workflowDocumentStore = useWorkflowDocumentStore(createWorkflowDocumentId('w0'));
workflowDocumentStore.setNodes(testNodes);

View File

@ -2,7 +2,6 @@ import { createCanvasGraphNode } from '@/features/workflows/canvas/__tests__/uti
import {
createTestNode,
createTestNodeProperties,
createTestWorkflow,
mockNodeTypeDescription,
} from '@/__tests__/mocks';
import { createComponentRenderer } from '@/__tests__/render';
@ -90,7 +89,7 @@ describe('FocusSidebar', () => {
}),
]);
workflowsStore = useWorkflowsStore(pinia);
workflowsStore.workflow = createTestWorkflow({ id: 'w0' });
workflowsStore.setWorkflowId('w0');
workflowDocumentStore = useWorkflowDocumentStore(createWorkflowDocumentId('w0'));
workflowDocumentStore.setNodes(testNodes);

View File

@ -128,6 +128,7 @@ describe('FromAiParametersModal', () => {
},
[STORES.WORKFLOWS]: {
workflow: mockWorkflow,
workflowId: 'test-workflow',
workflowExecutionData: mockRunData,
},
},

View File

@ -102,7 +102,8 @@ describe('MainHeader', () => {
sourceControlStore = mockedStore(useSourceControlStore);
collaborationStore = mockedStore(useCollaborationStore);
workflowsStore.workflow = {
workflowsStore.setWorkflowId('1');
workflowDocumentStore.hydrate({
id: '1',
name: 'Test Workflow',
active: false,
@ -117,7 +118,7 @@ describe('MainHeader', () => {
connections: {},
tags: [],
meta: {},
};
});
workflowDocumentStore.setName('Test Workflow');

View File

@ -199,7 +199,7 @@ describe('WorkflowDetails', () => {
'123': workflow,
};
workflowsStore.isWorkflowSaved = { '1': true, '123': true };
workflowsStore.workflowId = workflow.id;
workflowsStore.setWorkflowId(workflow.id);
workflowDocumentStoreRef.value?.setChecksum('test-checksum');
projectsStore.currentProject = null;
projectsStore.personalProject = { id: 'personal', name: 'Personal' } as Project;

View File

@ -131,11 +131,8 @@ describe('WorkflowHeaderDraftPublishActions', () => {
let projectsStore: MockedStore<typeof useProjectsStore>;
let workflowDocumentStore: ReturnType<typeof useWorkflowDocumentStore>;
const setupEnabledPublishButton = (overrides: Record<string, unknown> = {}) => {
Object.assign(workflowsStore, overrides);
if (!workflowsStore.workflow.nodes.length) {
workflowsStore.workflow.nodes = [triggerNode];
}
const setupEnabledPublishButton = () => {
workflowDocumentStore.setNodes([triggerNode]);
};
beforeEach(() => {
@ -144,19 +141,6 @@ describe('WorkflowHeaderDraftPublishActions', () => {
collaborationStore = mockedStore(useCollaborationStore);
projectsStore = mockedStore(useProjectsStore);
workflowsStore.workflow = {
id: '1',
name: 'Test Workflow',
active: false,
activeVersionId: null,
activeVersion: null,
versionId: 'version-1',
isArchived: false,
createdAt: Date.now(),
updatedAt: Date.now(),
nodes: [],
connections: {},
};
const nodeTypesStore = useNodeTypesStore();
nodeTypesStore.setNodeTypes([
mockNodeTypeDescription({
@ -164,6 +148,8 @@ describe('WorkflowHeaderDraftPublishActions', () => {
group: ['trigger'],
}),
]);
workflowsStore.setWorkflowId('1');
workflowDocumentStore = useWorkflowDocumentStore(createWorkflowDocumentId('1'));
workflowDocumentStore.setVersionData({ versionId: 'version-1', name: null, description: null });
workflowDocumentStore.setActiveState({ activeVersionId: null, activeVersion: null });
@ -347,12 +333,7 @@ describe('WorkflowHeaderDraftPublishActions', () => {
it('should open publish modal when clicked and workflow is saved', async () => {
const openModalSpy = vi.spyOn(uiStore, 'openModalWithData');
uiStore.markStateClean();
setupEnabledPublishButton({
workflow: {
...workflowsStore.workflow,
versionId: 'version-1',
},
});
setupEnabledPublishButton();
workflowDocumentStore.setActiveState({
activeVersionId: 'version-2',
activeVersion: createMockActiveVersion('version-2'),
@ -433,8 +414,8 @@ describe('WorkflowHeaderDraftPublishActions', () => {
describe('Publish button state', () => {
it('should show publish button disabled when there are no trigger nodes', () => {
workflowsStore.workflow.nodes = [];
workflowsStore.workflow.versionId = 'version-1';
workflowDocumentStore.setNodes([]);
workflowDocumentStore.setActiveState({
activeVersionId: 'version-2',
activeVersion: createMockActiveVersion('version-2'),
@ -447,8 +428,8 @@ describe('WorkflowHeaderDraftPublishActions', () => {
});
it('should show publish button disabled when trigger node is disabled', () => {
workflowsStore.workflow.nodes = [{ ...triggerNode, disabled: true }];
workflowsStore.workflow.versionId = 'version-1';
workflowDocumentStore.setNodes([{ ...triggerNode, disabled: true }]);
workflowDocumentStore.setActiveState({
activeVersionId: 'version-2',
activeVersion: createMockActiveVersion('version-2'),
@ -461,8 +442,8 @@ describe('WorkflowHeaderDraftPublishActions', () => {
});
it('should show publish button enabled when there are unpublished changes (versionId mismatch)', () => {
workflowsStore.workflow.nodes = [triggerNode];
workflowsStore.workflow.versionId = 'version-1';
workflowDocumentStore.setNodes([triggerNode]);
workflowDocumentStore.setActiveState({
activeVersionId: 'version-2',
activeVersion: createMockActiveVersion('version-2'),
@ -475,8 +456,8 @@ describe('WorkflowHeaderDraftPublishActions', () => {
});
it('should show publish button enabled when state is dirty', () => {
workflowsStore.workflow.nodes = [triggerNode];
workflowsStore.workflow.versionId = 'version-1';
workflowDocumentStore.setNodes([triggerNode]);
workflowDocumentStore.setActiveState({
activeVersionId: 'version-1',
activeVersion: createMockActiveVersion('version-1'),
@ -489,8 +470,8 @@ describe('WorkflowHeaderDraftPublishActions', () => {
});
it('should show publish button disabled when versions match and state is not dirty', () => {
workflowsStore.workflow.nodes = [triggerNode];
workflowsStore.workflow.versionId = 'version-1';
workflowDocumentStore.setNodes([triggerNode]);
workflowDocumentStore.setActiveState({
activeVersionId: 'version-1',
activeVersion: createMockActiveVersion('version-1'),
@ -503,8 +484,8 @@ describe('WorkflowHeaderDraftPublishActions', () => {
});
it('should keep the version menu enabled when workflow is published with no changes', () => {
workflowsStore.workflow.nodes = [triggerNode];
workflowsStore.workflow.versionId = 'version-1';
workflowDocumentStore.setNodes([triggerNode]);
workflowDocumentStore.setActiveState({
activeVersionId: 'version-1',
activeVersion: createMockActiveVersion('version-1'),
@ -518,8 +499,8 @@ describe('WorkflowHeaderDraftPublishActions', () => {
});
it('should keep the version menu enabled when workflow is published with no changes and unpublish is unavailable', () => {
workflowsStore.workflow.nodes = [triggerNode];
workflowsStore.workflow.versionId = 'version-1';
workflowDocumentStore.setNodes([triggerNode]);
workflowDocumentStore.setActiveState({
activeVersionId: 'version-1',
activeVersion: createMockActiveVersion('version-1'),
@ -541,8 +522,8 @@ describe('WorkflowHeaderDraftPublishActions', () => {
});
it('should show publish button enabled when workflow has never been published (no active version)', () => {
workflowsStore.workflow.nodes = [triggerNode];
workflowsStore.workflow.versionId = 'version-1';
workflowDocumentStore.setNodes([triggerNode]);
workflowDocumentStore.setActiveState({ activeVersionId: null, activeVersion: null });
uiStore.markStateClean();
@ -634,11 +615,6 @@ describe('WorkflowHeaderDraftPublishActions', () => {
settingsStore.isEnterpriseFeatureEnabled = createMockEnterpriseSettings({
[EnterpriseEditionFeature.NamedVersions]: true,
});
workflowsStore.workflow = {
...workflowsStore.workflow,
versionId: 'version-1',
updatedAt: Date.now(),
};
workflowDocumentStore.setVersionData({
versionId: 'version-1',
name: 'Test Version',
@ -724,7 +700,6 @@ describe('WorkflowHeaderDraftPublishActions', () => {
settingsStore.isEnterpriseFeatureEnabled = createMockEnterpriseSettings({
[EnterpriseEditionFeature.NamedVersions]: true,
});
workflowsStore.workflow.versionId = 'version-1';
const { container } = renderComponent();
expect(container).toBeInTheDocument();

View File

@ -5,16 +5,16 @@ import WorkflowPublishModal from '@/app/components/MainHeader/WorkflowPublishMod
import { useWorkflowsStore } from '@/app/stores/workflows.store';
import { useWorkflowsListStore } from '@/app/stores/workflowsList.store';
import { useSettingsStore } from '@/app/stores/settings.store';
import { useNodeTypesStore } from '@/app/stores/nodeTypes.store';
import { WORKFLOW_PUBLISH_MODAL_KEY } from '@/app/constants';
import { STORES } from '@n8n/stores';
import { waitFor } from '@testing-library/vue';
import userEvent from '@testing-library/user-event';
import { NodeConnectionTypes, WEBHOOK_NODE_TYPE, type INodeTypeDescription } from 'n8n-workflow';
import { WEBHOOK_NODE_TYPE, NodeConnectionTypes, type INodeTypeDescription } from 'n8n-workflow';
import {
useWorkflowDocumentStore,
createWorkflowDocumentId,
} from '@/app/stores/workflowDocument.store';
import { useNodeTypesStore } from '@/app/stores/nodeTypes.store';
const mockPublishWorkflow = vi.fn();
const mockShowMessage = vi.fn();
@ -105,6 +105,7 @@ const AI_GATEWAY_NODE = {
describe('WorkflowPublishModal', () => {
let workflowsStore: MockedStore<typeof useWorkflowsStore>;
let workflowsListStore: MockedStore<typeof useWorkflowsListStore>;
let workflowDocumentStore: ReturnType<typeof useWorkflowDocumentStore>;
beforeEach(() => {
workflowsStore = mockedStore(useWorkflowsStore);
@ -114,7 +115,9 @@ describe('WorkflowPublishModal', () => {
const nodeTypesStore = useNodeTypesStore();
nodeTypesStore.setNodeTypes([WEBHOOK_NODE_TYPE_DESCRIPTION]);
const workflowDocumentStore = useWorkflowDocumentStore(createWorkflowDocumentId('workflow-1'));
workflowsStore.setWorkflowId('workflow-1');
workflowDocumentStore = useWorkflowDocumentStore(createWorkflowDocumentId('workflow-1'));
workflowDocumentStore.setActiveState({
activeVersionId: 'old-version',
activeVersion: {
@ -128,37 +131,25 @@ describe('WorkflowPublishModal', () => {
},
});
workflowsStore.workflow = {
id: 'workflow-1',
name: 'Test Workflow',
active: false,
activeVersionId: null,
activeVersion: {
versionId: 'old-version',
authors: 'Test Author',
createdAt: new Date().toISOString(),
updatedAt: new Date().toISOString(),
workflowPublishHistory: [],
name: 'Published Version',
description: null,
},
// Set versionId different from activeVersion.versionId so wfHasAnyChanges is true
workflowDocumentStore.setVersionData({
versionId: 'new-version',
isArchived: false,
createdAt: Date.now(),
updatedAt: Date.now(),
nodes: [
{
id: 'trigger-1',
name: 'Webhook Trigger',
type: WEBHOOK_NODE_TYPE,
typeVersion: 1,
position: [0, 0],
parameters: {},
disabled: false,
},
],
connections: {},
};
name: null,
description: null,
});
// Add a trigger node to the document store so containsTrigger is true
workflowDocumentStore.setNodes([
{
id: 'trigger-1',
name: 'Webhook Trigger',
type: WEBHOOK_NODE_TYPE,
typeVersion: 1,
position: [0, 0],
parameters: {},
disabled: false,
},
]);
mockPublishWorkflow.mockReset().mockResolvedValue({
success: true,
@ -238,7 +229,7 @@ describe('WorkflowPublishModal', () => {
it('should not show warning when AI gateway is disabled', () => {
Object.assign(settingsStore.settings, { aiGateway: { enabled: false } });
workflowsStore.workflow = { ...workflowsStore.workflow, nodes: [AI_GATEWAY_NODE] };
workflowDocumentStore.setNodes([AI_GATEWAY_NODE]);
const { queryByTestId } = renderComponent();
@ -246,20 +237,17 @@ describe('WorkflowPublishModal', () => {
});
it('should not show warning when no nodes have AI gateway credentials', () => {
workflowsStore.workflow = {
...workflowsStore.workflow,
nodes: [
{
id: 'regular-node',
name: 'Regular Node',
type: 'n8n-nodes-base.set',
typeVersion: 1,
position: [100, 100],
parameters: {},
disabled: false,
},
],
};
workflowDocumentStore.setNodes([
{
id: 'regular-node',
name: 'Regular Node',
type: 'n8n-nodes-base.set',
typeVersion: 1,
position: [100, 100],
parameters: {},
disabled: false,
},
]);
const { queryByTestId } = renderComponent();
@ -267,10 +255,7 @@ describe('WorkflowPublishModal', () => {
});
it('should not show warning when the only AI gateway node is disabled', () => {
workflowsStore.workflow = {
...workflowsStore.workflow,
nodes: [{ ...AI_GATEWAY_NODE, disabled: true }],
};
workflowDocumentStore.setNodes([{ ...AI_GATEWAY_NODE, disabled: true }]);
const { queryByTestId } = renderComponent();
@ -278,7 +263,7 @@ describe('WorkflowPublishModal', () => {
});
it('should show warning with node name for a single active AI gateway node', () => {
workflowsStore.workflow = { ...workflowsStore.workflow, nodes: [AI_GATEWAY_NODE] };
workflowDocumentStore.setNodes([AI_GATEWAY_NODE]);
const { getByTestId } = renderComponent();
@ -288,7 +273,7 @@ describe('WorkflowPublishModal', () => {
});
it('should show singular copy for a single active AI gateway node', () => {
workflowsStore.workflow = { ...workflowsStore.workflow, nodes: [AI_GATEWAY_NODE] };
workflowDocumentStore.setNodes([AI_GATEWAY_NODE]);
const { getByTestId } = renderComponent();
@ -302,10 +287,10 @@ describe('WorkflowPublishModal', () => {
});
it('should show warning with all node names for multiple active AI gateway nodes', () => {
workflowsStore.workflow = {
...workflowsStore.workflow,
nodes: [AI_GATEWAY_NODE, { ...AI_GATEWAY_NODE, id: 'ai-node-2', name: 'Generate Image' }],
};
workflowDocumentStore.setNodes([
AI_GATEWAY_NODE,
{ ...AI_GATEWAY_NODE, id: 'ai-node-2', name: 'Generate Image' },
]);
const { getByTestId } = renderComponent();
@ -316,10 +301,10 @@ describe('WorkflowPublishModal', () => {
});
it('should show plural copy for multiple active AI gateway nodes', () => {
workflowsStore.workflow = {
...workflowsStore.workflow,
nodes: [AI_GATEWAY_NODE, { ...AI_GATEWAY_NODE, id: 'ai-node-2', name: 'Generate Image' }],
};
workflowDocumentStore.setNodes([
AI_GATEWAY_NODE,
{ ...AI_GATEWAY_NODE, id: 'ai-node-2', name: 'Generate Image' },
]);
const { getByTestId } = renderComponent();

View File

@ -85,18 +85,7 @@ describe('WorkflowDescriptionModal', () => {
description: '',
versionId: '2',
} as IWorkflowDb);
workflowsStore.workflow = {
id: 'test-workflow-id',
name: 'Test Workflow',
active: false,
activeVersionId: null,
isArchived: false,
createdAt: Date.now(),
updatedAt: Date.now(),
versionId: '1',
nodes: [],
connections: {},
};
workflowsStore.workflowId = 'test-workflow-id';
uiStore.markStateClean();
});

View File

@ -108,7 +108,7 @@ describe('WorkflowSettingsVue', () => {
releaseChannel: 'stable',
});
vi.spyOn(settingsStore, 'isModuleActive').mockReturnValue(true);
workflowsStore.workflowId = '1';
workflowsStore.setWorkflowId('1');
workflowDocumentStore.setName('Test Workflow');
// Populate workflowsById to mark workflow as existing (not new)
const testWorkflow = createTestWorkflow({

View File

@ -148,21 +148,7 @@ describe('WorkflowShareModal.ee.vue', () => {
updatedAt: new Date().toISOString(),
};
workflowsStore.workflow = {
id: '',
name: 'My workflow',
active: false,
activeVersionId: null,
isArchived: false,
createdAt: new Date().toISOString(),
updatedAt: new Date().toISOString(),
versionId: '',
scopes: [],
nodes: [],
connections: {},
homeProject,
};
workflowsStore.workflowId = '';
mockWorkflowDocumentState.homeProject = homeProject;
const saveWorkflowSharedWithSpy = vi.spyOn(workflowsEEStore, 'saveWorkflowSharedWith');
@ -213,21 +199,7 @@ describe('WorkflowShareModal.ee.vue', () => {
updatedAt: new Date().toISOString(),
};
workflowsStore.workflow = {
id: 'workflow-1',
name: 'My workflow',
active: false,
activeVersionId: null,
isArchived: false,
createdAt: new Date().toISOString(),
updatedAt: new Date().toISOString(),
versionId: '',
scopes: [],
nodes: [],
connections: {},
homeProject,
};
workflowsStore.workflowId = 'workflow-1';
mockWorkflowDocumentState.homeProject = homeProject;
const props = {
@ -257,19 +229,7 @@ describe('WorkflowShareModal.ee.vue', () => {
type: ProjectTypes.Team,
});
workflowsStore.workflow = {
id: 'workflow-1',
name: 'My workflow',
active: false,
activeVersionId: null,
isArchived: false,
createdAt: new Date().toISOString(),
updatedAt: new Date().toISOString(),
versionId: '',
scopes: [],
nodes: [],
connections: {},
};
workflowsStore.workflowId = 'workflow-1';
const props = {
data: { id: 'workflow-1' },

View File

@ -26,7 +26,7 @@ describe('useActivationError', () => {
beforeEach(() => {
vi.clearAllMocks();
setActivePinia(createTestingPinia());
useWorkflowsStore().workflow.id = TEST_WF_ID;
useWorkflowsStore().workflowId = TEST_WF_ID;
workflowDocumentStore = useWorkflowDocumentStore(createWorkflowDocumentId(TEST_WF_ID));
});

View File

@ -197,8 +197,11 @@ describe('useCanvasOperations', () => {
};
}
type Writable<T> = { -readonly [K in keyof T]: T[K] };
type WritableDocumentStore = Writable<ReturnType<typeof useWorkflowDocumentStore>>;
let workflowState: WorkflowState;
let workflowDocumentStoreInstance: ReturnType<typeof useWorkflowDocumentStore>;
let workflowDocumentStoreInstance: WritableDocumentStore;
beforeEach(() => {
vi.restoreAllMocks();
@ -211,10 +214,9 @@ describe('useCanvasOperations', () => {
vi.mocked(injectWorkflowState).mockReturnValue(workflowState);
const workflowsStore = useWorkflowsStore();
workflowsStore.workflow.id = workflowId;
workflowDocumentStoreInstance = useWorkflowDocumentStore(
createWorkflowDocumentId(workflowsStore.workflowId),
);
) as WritableDocumentStore;
// These actions are stubbed by createTestingPinia, so provide safe defaults.
// Tests that need custom behavior can override via vi.spyOn.
@ -506,12 +508,12 @@ describe('useCanvasOperations', () => {
});
it('should place the node at the last clicked position if no other position is set', () => {
const workflowsStore = mockedStore(useWorkflowsStore);
const node = createTestNode({ id: '0' });
const nodeTypeDescription = mockNodeTypeDescription();
workflowsStore.workflowTriggerNodes = [createTestNode({ id: 'trigger', position: [96, 96] })];
workflowDocumentStoreInstance.workflowTriggerNodes = [
createTestNode({ id: 'trigger', position: [96, 96] }),
];
const { resolveNodePosition, lastClickPosition } = useCanvasOperations();
lastClickPosition.value = [300, 300];
@ -1454,8 +1456,8 @@ describe('useCanvasOperations', () => {
}),
];
workflowsStore.workflow.nodes = nodes;
workflowsStore.workflow.connections = {
workflowDocumentStoreInstance.allNodes = nodes;
workflowDocumentStoreInstance.connectionsBySourceNode = {
[nodes[0].name]: {
main: [
[
@ -1525,8 +1527,8 @@ describe('useCanvasOperations', () => {
(name: string) => nodes.find((node) => node.name === name) ?? null,
);
workflowsStore.workflow.nodes = nodes;
workflowsStore.workflow.connections = {
workflowDocumentStoreInstance.allNodes = nodes;
workflowDocumentStoreInstance.connectionsBySourceNode = {
[nodes[0].name]: {
main: [
null,
@ -1920,7 +1922,6 @@ describe('useCanvasOperations', () => {
describe('addConnections', () => {
it('should create connections between nodes', async () => {
const workflowsStore = mockedStore(useWorkflowsStore);
const nodeTypesStore = mockedStore(useNodeTypesStore);
const nodeTypeName = SET_NODE_TYPE;
const nodeType = mockNodeTypeDescription({
@ -1972,7 +1973,7 @@ describe('useCanvasOperations', () => {
},
];
workflowsStore.workflow.nodes = nodes;
workflowDocumentStoreInstance.allNodes = nodes;
nodeTypesStore.nodeTypes = {
[nodeTypeName]: { 1: nodeType },
};
@ -2066,7 +2067,6 @@ describe('useCanvasOperations', () => {
});
it('should create a connection if source and target nodes exist and connection is allowed', () => {
const workflowsStore = mockedStore(useWorkflowsStore);
const uiStore = mockedStore(useUIStore);
const nodeTypesStore = mockedStore(useNodeTypesStore);
@ -2099,7 +2099,7 @@ describe('useCanvasOperations', () => {
node: { 1: nodeTypeDescription },
};
workflowsStore.workflow.nodes = [nodeA, nodeB];
workflowDocumentStoreInstance.allNodes = [nodeA, nodeB];
vi.spyOn(workflowDocumentStoreInstance, 'getNodeById')
.mockReturnValueOnce(nodeA)
.mockReturnValueOnce(nodeB);
@ -2126,7 +2126,6 @@ describe('useCanvasOperations', () => {
});
it('should not set UI state as dirty if keepPristine is true', () => {
const workflowsStore = mockedStore(useWorkflowsStore);
const uiStore = mockedStore(useUIStore);
const nodeTypesStore = mockedStore(useNodeTypesStore);
@ -2159,7 +2158,7 @@ describe('useCanvasOperations', () => {
node: { 1: nodeTypeDescription },
};
workflowsStore.workflow.nodes = [nodeA, nodeB];
workflowDocumentStoreInstance.allNodes = [nodeA, nodeB];
vi.spyOn(workflowDocumentStoreInstance, 'getNodeById')
.mockReturnValueOnce(nodeA)
.mockReturnValueOnce(nodeB);
@ -3124,19 +3123,14 @@ describe('useCanvasOperations', () => {
outputs: [NodeConnectionTypes.AiTool],
});
(
workflowDocumentStoreInstance as unknown as Record<string, unknown>
).connectionsBySourceNode = {
workflowDocumentStoreInstance.connectionsBySourceNode = {
[sourceNode.name]: {
[NodeConnectionTypes.AiTool]: [
[{ node: targetNode.name, type: NodeConnectionTypes.Main, index: 0 }],
],
},
};
(workflowDocumentStoreInstance as unknown as Record<string, unknown>).allNodes = [
sourceNode,
targetNode,
];
workflowDocumentStoreInstance.allNodes = [sourceNode, targetNode];
vi.spyOn(workflowDocumentStoreInstance, 'getNodeById').mockImplementation((id) => {
if (id === sourceNodeId) return sourceNode;
@ -3163,7 +3157,6 @@ describe('useCanvasOperations', () => {
});
it('should keep valid connections that match input type', () => {
const workflowsStore = mockedStore(useWorkflowsStore);
const nodeTypesStore = mockedStore(useNodeTypesStore);
vi.mocked(workflowDocumentStoreInstance.removeConnection).mockClear();
@ -3190,8 +3183,8 @@ describe('useCanvasOperations', () => {
outputs: [NodeConnectionTypes.Main],
});
workflowsStore.workflow.nodes = [sourceNode, targetNode];
workflowsStore.workflow.connections = {
workflowDocumentStoreInstance.allNodes = [sourceNode, targetNode];
workflowDocumentStoreInstance.connectionsBySourceNode = {
[sourceNode.name]: {
[NodeConnectionTypes.Main]: [
[{ node: targetNode.name, type: NodeConnectionTypes.Main, index: 0 }],
@ -3243,19 +3236,14 @@ describe('useCanvasOperations', () => {
outputs: [NodeConnectionTypes.AiLanguageModel],
});
(
workflowDocumentStoreInstance as unknown as Record<string, unknown>
).connectionsBySourceNode = {
workflowDocumentStoreInstance.connectionsBySourceNode = {
[sourceNode.name]: {
[NodeConnectionTypes.AiLanguageModel]: [
[{ node: targetNode.name, type: NodeConnectionTypes.AiLanguageModel, index: 1 }],
],
},
};
(workflowDocumentStoreInstance as unknown as Record<string, unknown>).allNodes = [
sourceNode,
targetNode,
];
workflowDocumentStoreInstance.allNodes = [sourceNode, targetNode];
vi.spyOn(workflowDocumentStoreInstance, 'getNodeById').mockImplementation((id) => {
if (id === sourceNodeId) return sourceNode;
@ -3282,7 +3270,6 @@ describe('useCanvasOperations', () => {
});
it('should keep connections if the input port index is still valid for the type', async () => {
const workflowsStore = mockedStore(useWorkflowsStore);
const nodeTypesStore = mockedStore(useNodeTypesStore);
vi.mocked(workflowDocumentStoreInstance.removeConnection).mockClear();
@ -3313,8 +3300,8 @@ describe('useCanvasOperations', () => {
outputs: [NodeConnectionTypes.AiLanguageModel],
});
workflowsStore.workflow.nodes = [sourceNode, targetNode];
workflowsStore.workflow.connections = {
workflowDocumentStoreInstance.allNodes = [sourceNode, targetNode];
workflowDocumentStoreInstance.connectionsBySourceNode = {
[sourceNode.name]: {
[NodeConnectionTypes.AiLanguageModel]: [
[{ node: targetNode.name, type: NodeConnectionTypes.AiLanguageModel, index: 1 }],
@ -3403,19 +3390,14 @@ describe('useCanvasOperations', () => {
outputs: [NodeConnectionTypes.AiTool],
});
(
workflowDocumentStoreInstance as unknown as Record<string, unknown>
).connectionsBySourceNode = {
workflowDocumentStoreInstance.connectionsBySourceNode = {
[sourceNode.name]: {
[NodeConnectionTypes.AiTool]: [
[{ node: targetNode.name, type: NodeConnectionTypes.Main, index: 0 }],
],
},
};
(workflowDocumentStoreInstance as unknown as Record<string, unknown>).allNodes = [
sourceNode,
targetNode,
];
workflowDocumentStoreInstance.allNodes = [sourceNode, targetNode];
vi.spyOn(workflowDocumentStoreInstance, 'getNodeById').mockImplementation((id) => {
if (id === sourceNodeId) return sourceNode;
@ -3442,7 +3424,6 @@ describe('useCanvasOperations', () => {
});
it('should keep valid connections that match output type', () => {
const workflowsStore = mockedStore(useWorkflowsStore);
const nodeTypesStore = mockedStore(useNodeTypesStore);
vi.mocked(workflowDocumentStoreInstance.removeConnection).mockClear();
@ -3469,8 +3450,8 @@ describe('useCanvasOperations', () => {
outputs: [NodeConnectionTypes.Main],
});
workflowsStore.workflow.nodes = [sourceNode, targetNode];
workflowsStore.workflow.connections = {
workflowDocumentStoreInstance.allNodes = [sourceNode, targetNode];
workflowDocumentStoreInstance.connectionsBySourceNode = {
[sourceNode.name]: {
[NodeConnectionTypes.AiTool]: [
[{ node: targetNode.name, type: NodeConnectionTypes.Main, index: 0 }],
@ -3503,9 +3484,7 @@ describe('useCanvasOperations', () => {
const node1 = createTestNode({ id: 'node1', name: 'Node 1' });
const node2 = createTestNode({ id: 'node2', name: 'Node 1' });
(
workflowDocumentStoreInstance as unknown as Record<string, unknown>
).connectionsBySourceNode = {
workflowDocumentStoreInstance.connectionsBySourceNode = {
[node1.name]: {
[NodeConnectionTypes.Main]: [
[{ node: node2.name, type: NodeConnectionTypes.Main, index: 0 }],
@ -3572,9 +3551,7 @@ describe('useCanvasOperations', () => {
[NodeConnectionTypes.Main]: [],
},
};
(
workflowDocumentStoreInstance as unknown as Record<string, unknown>
).connectionsBySourceNode = connections;
workflowDocumentStoreInstance.connectionsBySourceNode = connections;
vi.spyOn(workflowDocumentStoreInstance, 'getNodeById').mockImplementation((id) => {
if (id === sourceNode.id) return sourceNode;
@ -3630,7 +3607,6 @@ describe('useCanvasOperations', () => {
describe('copyNodes', () => {
it('should copy nodes', async () => {
const workflowsStore = mockedStore(useWorkflowsStore);
const nodeTypesStore = useNodeTypesStore();
const nodeTypeDescription = mockNodeTypeDescription({ name: SET_NODE_TYPE });
@ -3639,7 +3615,7 @@ describe('useCanvasOperations', () => {
};
const nodes = buildImportNodes();
workflowsStore.workflow.nodes = nodes;
workflowDocumentStoreInstance.allNodes = nodes;
vi.spyOn(workflowDocumentStoreInstance, 'getNodesByIds').mockReturnValue(nodes);
vi.mocked(workflowDocumentStoreInstance.outgoingConnectionsByNodeName).mockReturnValue({});
@ -3653,7 +3629,6 @@ describe('useCanvasOperations', () => {
describe('cutNodes', () => {
it('should copy and delete nodes', async () => {
const workflowsStore = mockedStore(useWorkflowsStore);
const nodeTypesStore = useNodeTypesStore();
const nodeTypeDescription = mockNodeTypeDescription({ name: SET_NODE_TYPE });
@ -3662,7 +3637,7 @@ describe('useCanvasOperations', () => {
};
const nodes = buildImportNodes();
workflowsStore.workflow.nodes = nodes;
workflowDocumentStoreInstance.allNodes = nodes;
vi.spyOn(workflowDocumentStoreInstance, 'getNodesByIds').mockReturnValue(nodes);
vi.mocked(workflowDocumentStoreInstance.outgoingConnectionsByNodeName).mockReturnValue({});
@ -3742,7 +3717,7 @@ describe('useCanvasOperations', () => {
it('should set connections even when workflowId is initially empty', async () => {
const workflowsStore = useWorkflowsStore();
// Simulate the state after resetWorkspace() — workflowId is cleared
workflowsStore.workflow.id = '';
workflowsStore.workflowId = '';
const testConnections = {
'Node 1': { main: [[{ node: 'Node 2', type: 'main' as const, index: 0 }]] },
@ -3854,7 +3829,6 @@ describe('useCanvasOperations', () => {
describe('initializeUnknownNodes', () => {
it('should initialize nodes', () => {
const updateNodeByIdSpy = vi.spyOn(workflowDocumentStoreInstance, 'updateNodeById');
const workflowsStore = mockedStore(useWorkflowsStore);
const nodes = [
createTestNode({ type: 'n8n-nodes-community.testNode1', name: 'testNode1' }),
createTestNode({ type: 'n8n-nodes-community.testNode2', name: 'testNode2' }),
@ -3863,7 +3837,7 @@ describe('useCanvasOperations', () => {
nodes,
connections: {},
});
workflowsStore.workflow.nodes = nodes;
workflowDocumentStoreInstance.allNodes = nodes;
vi.spyOn(workflowDocumentStoreInstance, 'getNodeByName').mockImplementation(
(name: string) => nodes.find((n) => n.name === name) ?? null,
);
@ -3877,7 +3851,6 @@ describe('useCanvasOperations', () => {
it('should remove preview token from node type when initializing', () => {
const updateNodeByIdSpy = vi.spyOn(workflowDocumentStoreInstance, 'updateNodeById');
const workflowsStore = mockedStore(useWorkflowsStore);
const nodeWithPreview = createTestNode({
type: 'n8n-nodes-community.testNode-preview',
name: 'testNode',
@ -3886,7 +3859,7 @@ describe('useCanvasOperations', () => {
nodes: [nodeWithPreview],
connections: {},
});
workflowsStore.workflow.nodes = [nodeWithPreview];
workflowDocumentStoreInstance.allNodes = [nodeWithPreview];
vi.spyOn(workflowDocumentStoreInstance, 'getNodeByName').mockReturnValue(nodeWithPreview);
const { initializeUnknownNodes } = useCanvasOperations();
initializeUnknownNodes(workflow.nodes);
@ -4292,7 +4265,6 @@ describe('useCanvasOperations', () => {
describe('connectAdjacentNodes', () => {
it('should connect nodes that were connected through the removed node', () => {
const workflowsStore = mockedStore(useWorkflowsStore);
const nodeTypesStore = mockedStore(useNodeTypesStore);
const historyStore = mockedStore(useHistoryStore);
@ -4310,8 +4282,8 @@ describe('useCanvasOperations', () => {
nodeTypesStore.getNodeType = vi.fn(() => nodeTypeDescription);
// Set up the workflow connections A -> B -> C
workflowsStore.workflow.nodes = [nodeA, nodeB, nodeC];
workflowsStore.workflow.connections = {
workflowDocumentStoreInstance.allNodes = [nodeA, nodeB, nodeC];
workflowDocumentStoreInstance.connectionsBySourceNode = {
[nodeA.name]: {
main: [[{ node: nodeB.name, type: NodeConnectionTypes.Main, index: 0 }]],
},
@ -4360,7 +4332,6 @@ describe('useCanvasOperations', () => {
});
it('should connect nodes that were connected through the removed node at different indices', () => {
const workflowsStore = mockedStore(useWorkflowsStore);
const nodeTypesStore = mockedStore(useNodeTypesStore);
const historyStore = mockedStore(useHistoryStore);
@ -4378,8 +4349,8 @@ describe('useCanvasOperations', () => {
nodeTypesStore.getNodeType = vi.fn(() => nodeTypeDescription);
// Set up the workflow connections A -> B -> C
workflowsStore.workflow.nodes = [nodeA, nodeB, nodeC];
workflowsStore.workflow.connections = {
workflowDocumentStoreInstance.allNodes = [nodeA, nodeB, nodeC];
workflowDocumentStoreInstance.connectionsBySourceNode = {
[nodeA.name]: {
main: [[{ node: nodeB.name, type: NodeConnectionTypes.Main, index: 1 }]],
},
@ -4428,14 +4399,12 @@ describe('useCanvasOperations', () => {
});
it('should not create connections if middle node has no incoming connections', () => {
const workflowsStore = mockedStore(useWorkflowsStore);
// Create nodes: B -> C (no incoming to B)
const nodeB = createTestNode({ id: 'B', name: 'Node B', position: [96, 0] });
const nodeC = createTestNode({ id: 'C', name: 'Node C', position: [208, 0] });
workflowsStore.workflow.nodes = [nodeB, nodeC];
workflowsStore.workflow.connections = {
workflowDocumentStoreInstance.allNodes = [nodeB, nodeC];
workflowDocumentStoreInstance.connectionsBySourceNode = {
[nodeB.name]: {
main: [[{ node: nodeC.name, type: NodeConnectionTypes.Main, index: 0 }]],
},
@ -4454,14 +4423,12 @@ describe('useCanvasOperations', () => {
});
it('should not create connections if middle node has no outgoing connections', () => {
const workflowsStore = mockedStore(useWorkflowsStore);
// Create nodes: A -> B (no outgoing from B)
const nodeA = createTestNode({ id: 'A', name: 'Node A', position: [0, 0] });
const nodeB = createTestNode({ id: 'B', name: 'Node B', position: [96, 0] });
workflowsStore.workflow.nodes = [nodeA, nodeB];
workflowsStore.workflow.connections = {
workflowDocumentStoreInstance.allNodes = [nodeA, nodeB];
workflowDocumentStoreInstance.connectionsBySourceNode = {
[nodeA.name]: {
main: [[{ node: nodeB.name, type: NodeConnectionTypes.Main, index: 0 }]],
},
@ -4837,7 +4804,6 @@ describe('useCanvasOperations', () => {
describe('duplicateNodes', () => {
it('should duplicate nodes', async () => {
const workflowsStore = mockedStore(useWorkflowsStore);
const nodeTypesStore = useNodeTypesStore();
const nodeTypeDescription = mockNodeTypeDescription({ name: SET_NODE_TYPE });
@ -4846,11 +4812,14 @@ describe('useCanvasOperations', () => {
};
const nodes = buildImportNodes();
workflowsStore.workflow.nodes = nodes;
workflowDocumentStoreInstance.allNodes = nodes;
vi.spyOn(workflowDocumentStoreInstance, 'getNodesByIds').mockReturnValue(nodes);
vi.mocked(workflowDocumentStoreInstance.outgoingConnectionsByNodeName).mockReturnValue({});
const workflowObject = createTestWorkflowObject(workflowsStore.workflow);
const workflowObject = createTestWorkflowObject({
nodes: workflowDocumentStoreInstance.allNodes,
connections: workflowDocumentStoreInstance.connectionsBySourceNode,
});
vi.mocked(workflowDocumentStoreInstance.createWorkflowObject).mockReturnValue(workflowObject);
const canvasOperations = useCanvasOperations();
@ -4862,7 +4831,6 @@ describe('useCanvasOperations', () => {
});
it('should not crash when TelemetryHelpers.generateNodesGraph throws error', async () => {
const workflowsStore = mockedStore(useWorkflowsStore);
const telemetry = useTelemetry();
const nodeTypesStore = useNodeTypesStore();
const nodeTypeDescription = mockNodeTypeDescription({ name: SET_NODE_TYPE });
@ -4872,11 +4840,14 @@ describe('useCanvasOperations', () => {
};
const nodes = buildImportNodes();
workflowsStore.workflow.nodes = nodes;
workflowDocumentStoreInstance.allNodes = nodes;
vi.spyOn(workflowDocumentStoreInstance, 'getNodesByIds').mockReturnValue(nodes);
vi.mocked(workflowDocumentStoreInstance.outgoingConnectionsByNodeName).mockReturnValue({});
const workflowObject = createTestWorkflowObject(workflowsStore.workflow);
const workflowObject = createTestWorkflowObject({
nodes: workflowDocumentStoreInstance.allNodes,
connections: workflowDocumentStoreInstance.connectionsBySourceNode,
});
vi.mocked(workflowDocumentStoreInstance.createWorkflowObject).mockReturnValue(workflowObject);
// Mock TelemetryHelpers.generateNodesGraph to throw an error for this test
@ -5106,12 +5077,10 @@ describe('useCanvasOperations', () => {
let historyStore: ReturnType<typeof mockedStore<typeof useHistoryStore>>;
let nodeTypesStore: ReturnType<typeof mockedStore<typeof useNodeTypesStore>>;
let workflowsStore: ReturnType<typeof mockedStore<typeof useWorkflowsStore>>;
beforeEach(() => {
historyStore = mockedStore(useHistoryStore);
nodeTypesStore = mockedStore(useNodeTypesStore);
workflowsStore = mockedStore(useWorkflowsStore);
const nodeTypeDescription = mockNodeTypeDescription({
inputs: [NodeConnectionTypes.Main, NodeConnectionTypes.Main],
@ -5125,8 +5094,13 @@ describe('useCanvasOperations', () => {
describe('common cases', () => {
beforeEach(() => {
workflowsStore.workflow.nodes = [sourceNode, targetNode, replacementNode, nextNode];
workflowsStore.workflow.connections = {
workflowDocumentStoreInstance.allNodes = [
sourceNode,
targetNode,
replacementNode,
nextNode,
];
workflowDocumentStoreInstance.connectionsBySourceNode = {
[sourceNode.name]: {
[NodeConnectionTypes.Main]: [
[
@ -5158,7 +5132,10 @@ describe('useCanvasOperations', () => {
});
});
it('should replace connections for a node and track history', () => {
const workflowObject = createTestWorkflowObject(workflowsStore.workflow);
const workflowObject = createTestWorkflowObject({
nodes: workflowDocumentStoreInstance.allNodes,
connections: workflowDocumentStoreInstance.connectionsBySourceNode,
});
vi.mocked(workflowDocumentStoreInstance.getParentNodes).mockImplementation((...args) =>
workflowObject.getParentNodes(...args),
);
@ -5264,7 +5241,10 @@ describe('useCanvasOperations', () => {
});
it('should replace connections without tracking history', () => {
const workflowObject = createTestWorkflowObject(workflowsStore.workflow);
const workflowObject = createTestWorkflowObject({
nodes: workflowDocumentStoreInstance.allNodes,
connections: workflowDocumentStoreInstance.connectionsBySourceNode,
});
vi.mocked(workflowDocumentStoreInstance.getParentNodes).mockImplementation((...args) =>
workflowObject.getParentNodes(...args),
);
@ -5346,14 +5326,14 @@ describe('useCanvasOperations', () => {
name: 'Target Node',
});
workflowsStore.workflow.nodes = [
workflowDocumentStoreInstance.allNodes = [
previousNode1,
previousNode2,
newNode1,
newNode2,
targetNode,
];
workflowsStore.workflow.connections = {
workflowDocumentStoreInstance.connectionsBySourceNode = {
[previousNode1.name]: {
[NodeConnectionTypes.Main]: [
[{ node: targetNode.name, type: NodeConnectionTypes.Main, index: 0 }],
@ -5383,7 +5363,10 @@ describe('useCanvasOperations', () => {
return null;
});
const workflowObject = createTestWorkflowObject(workflowsStore.workflow);
const workflowObject = createTestWorkflowObject({
nodes: workflowDocumentStoreInstance.allNodes,
connections: workflowDocumentStoreInstance.connectionsBySourceNode,
});
vi.mocked(workflowDocumentStoreInstance.getParentNodes).mockImplementation((...args) =>
workflowObject.getParentNodes(...args),
);
@ -5457,12 +5440,10 @@ describe('useCanvasOperations', () => {
let historyStore: ReturnType<typeof mockedStore<typeof useHistoryStore>>;
let nodeTypesStore: ReturnType<typeof mockedStore<typeof useNodeTypesStore>>;
let workflowsStore: ReturnType<typeof mockedStore<typeof useWorkflowsStore>>;
beforeEach(() => {
historyStore = mockedStore(useHistoryStore);
nodeTypesStore = mockedStore(useNodeTypesStore);
workflowsStore = mockedStore(useWorkflowsStore);
const nodeTypeDescription = mockNodeTypeDescription({
inputs: [NodeConnectionTypes.Main, NodeConnectionTypes.Main],
@ -5476,8 +5457,13 @@ describe('useCanvasOperations', () => {
describe('common cases', () => {
beforeEach(() => {
workflowsStore.workflow.nodes = [sourceNode, targetNode, replacementNode, nextNode];
workflowsStore.workflow.connections = {
workflowDocumentStoreInstance.allNodes = [
sourceNode,
targetNode,
replacementNode,
nextNode,
];
workflowDocumentStoreInstance.connectionsBySourceNode = {
[sourceNode.name]: {
[NodeConnectionTypes.Main]: [[connectionTargetMain0], [connectionTargetMain1]],
},
@ -5485,11 +5471,6 @@ describe('useCanvasOperations', () => {
[NodeConnectionTypes.Main]: [[connectionNextMain0]],
},
};
(
workflowDocumentStoreInstance as unknown as Record<string, unknown>
).connectionsBySourceNode = workflowsStore.workflow.connections;
(workflowDocumentStoreInstance as unknown as Record<string, unknown>).allNodes =
workflowsStore.workflow.nodes;
vi.spyOn(workflowDocumentStoreInstance, 'getNodeById').mockImplementation((id) => {
if (id === sourceNode.id) return sourceNode;
@ -5535,7 +5516,10 @@ describe('useCanvasOperations', () => {
);
});
it('should replace an existing node and track history', () => {
const workflowObject = createTestWorkflowObject(workflowsStore.workflow);
const workflowObject = createTestWorkflowObject({
nodes: workflowDocumentStoreInstance.allNodes,
connections: workflowDocumentStoreInstance.connectionsBySourceNode,
});
vi.mocked(workflowDocumentStoreInstance.getParentNodes).mockImplementation((...args) =>
workflowObject.getParentNodes(...args),
);
@ -5726,24 +5710,7 @@ describe('useCanvasOperations', () => {
const nodeC = createTestNode({ id: 'C', name: 'End', position: [208, 0] });
const pinia = createTestingPinia({
initialState: {
[STORES.WORKFLOWS]: {
workflow: createTestWorkflow({
nodes: [nodeA, nodeB, nodeC],
connections: {
[nodeA.name]: {
main: [[{ node: nodeB.name, type: NodeConnectionTypes.Main, index: 0 }]],
},
[nodeB.name]: {
main: [[{ node: nodeC.name, type: NodeConnectionTypes.Main, index: 0 }]],
},
[nodeC.name]: {
main: [[{ node: nodeA.name, type: NodeConnectionTypes.Main, index: 0 }]],
},
},
}),
},
},
initialState: { [STORES.WORKFLOWS]: { workflowId } },
});
setActivePinia(pinia);
@ -5751,10 +5718,22 @@ describe('useCanvasOperations', () => {
vi.mocked(injectWorkflowState).mockReturnValue(workflowState);
workflowDocumentStoreInstance = useWorkflowDocumentStore(
createWorkflowDocumentId(useWorkflowsStore().workflowId),
);
) as WritableDocumentStore;
workflowDocumentStoreInstance.allNodes = [nodeA, nodeB, nodeC];
workflowDocumentStoreInstance.connectionsBySourceNode = {
[nodeA.name]: {
main: [[{ node: nodeB.name, type: NodeConnectionTypes.Main, index: 0 }]],
},
[nodeB.name]: {
main: [[{ node: nodeC.name, type: NodeConnectionTypes.Main, index: 0 }]],
},
[nodeC.name]: {
main: [[{ node: nodeA.name, type: NodeConnectionTypes.Main, index: 0 }]],
},
};
vi.mocked(workflowDocumentStoreInstance.getConnectedNodes).mockReturnValue([]);
vi.mocked(workflowDocumentStoreInstance.getNodeByName).mockImplementation(
(name) => useWorkflowsStore().workflow.nodes.find((n) => n.name === name) ?? null,
(name) => workflowDocumentStoreInstance.allNodes.find((n) => n.name === name) ?? null,
);
const { getNodesToShift } = useCanvasOperations();
@ -5797,18 +5776,7 @@ describe('useCanvasOperations', () => {
});
const pinia = createTestingPinia({
initialState: {
[STORES.WORKFLOWS]: {
workflow: createTestWorkflow({
nodes: [sourceNode, targetNode, stickyNote],
connections: {
[sourceNode.name]: {
main: [[{ node: targetNode.name, type: NodeConnectionTypes.Main, index: 0 }]],
},
},
}),
},
},
initialState: { [STORES.WORKFLOWS]: { workflowId } },
});
setActivePinia(pinia);
@ -5816,10 +5784,16 @@ describe('useCanvasOperations', () => {
vi.mocked(injectWorkflowState).mockReturnValue(workflowState);
workflowDocumentStoreInstance = useWorkflowDocumentStore(
createWorkflowDocumentId(useWorkflowsStore().workflowId),
);
) as WritableDocumentStore;
workflowDocumentStoreInstance.allNodes = [sourceNode, targetNode, stickyNote];
workflowDocumentStoreInstance.connectionsBySourceNode = {
[sourceNode.name]: {
main: [[{ node: targetNode.name, type: NodeConnectionTypes.Main, index: 0 }]],
},
};
vi.mocked(workflowDocumentStoreInstance.getConnectedNodes).mockReturnValue([]);
vi.mocked(workflowDocumentStoreInstance.getNodeByName).mockImplementation(
(name) => useWorkflowsStore().workflow.nodes.find((n) => n.name === name) ?? null,
(name) => workflowDocumentStoreInstance.allNodes.find((n) => n.name === name) ?? null,
);
const { getNodesToShift } = useCanvasOperations();
@ -5866,18 +5840,7 @@ describe('useCanvasOperations', () => {
});
const pinia = createTestingPinia({
initialState: {
[STORES.WORKFLOWS]: {
workflow: createTestWorkflow({
nodes: [sourceNode, targetNode, stickyNote],
connections: {
[sourceNode.name]: {
main: [[{ node: targetNode.name, type: NodeConnectionTypes.Main, index: 0 }]],
},
},
}),
},
},
initialState: { [STORES.WORKFLOWS]: { workflowId } },
});
setActivePinia(pinia);
@ -5885,10 +5848,16 @@ describe('useCanvasOperations', () => {
vi.mocked(injectWorkflowState).mockReturnValue(workflowState);
workflowDocumentStoreInstance = useWorkflowDocumentStore(
createWorkflowDocumentId(useWorkflowsStore().workflowId),
);
) as WritableDocumentStore;
workflowDocumentStoreInstance.allNodes = [sourceNode, targetNode, stickyNote];
workflowDocumentStoreInstance.connectionsBySourceNode = {
[sourceNode.name]: {
main: [[{ node: targetNode.name, type: NodeConnectionTypes.Main, index: 0 }]],
},
};
vi.mocked(workflowDocumentStoreInstance.getConnectedNodes).mockReturnValue([]);
vi.mocked(workflowDocumentStoreInstance.getNodeByName).mockImplementation(
(name) => useWorkflowsStore().workflow.nodes.find((n) => n.name === name) ?? null,
(name) => workflowDocumentStoreInstance.allNodes.find((n) => n.name === name) ?? null,
);
const { getNodesToShift } = useCanvasOperations();
@ -5939,18 +5908,7 @@ describe('useCanvasOperations', () => {
});
const pinia = createTestingPinia({
initialState: {
[STORES.WORKFLOWS]: {
workflow: createTestWorkflow({
nodes: [sourceNode, targetNode, stickyNote],
connections: {
[sourceNode.name]: {
main: [[{ node: targetNode.name, type: NodeConnectionTypes.Main, index: 0 }]],
},
},
}),
},
},
initialState: { [STORES.WORKFLOWS]: { workflowId } },
});
setActivePinia(pinia);
@ -5958,10 +5916,16 @@ describe('useCanvasOperations', () => {
vi.mocked(injectWorkflowState).mockReturnValue(workflowState);
workflowDocumentStoreInstance = useWorkflowDocumentStore(
createWorkflowDocumentId(useWorkflowsStore().workflowId),
);
) as WritableDocumentStore;
workflowDocumentStoreInstance.allNodes = [sourceNode, targetNode, stickyNote];
workflowDocumentStoreInstance.connectionsBySourceNode = {
[sourceNode.name]: {
main: [[{ node: targetNode.name, type: NodeConnectionTypes.Main, index: 0 }]],
},
};
vi.mocked(workflowDocumentStoreInstance.getConnectedNodes).mockReturnValue([]);
vi.mocked(workflowDocumentStoreInstance.getNodeByName).mockImplementation(
(name) => useWorkflowsStore().workflow.nodes.find((n) => n.name === name) ?? null,
(name) => workflowDocumentStoreInstance.allNodes.find((n) => n.name === name) ?? null,
);
const { getNodesToShift } = useCanvasOperations();
@ -6008,18 +5972,7 @@ describe('useCanvasOperations', () => {
});
const pinia = createTestingPinia({
initialState: {
[STORES.WORKFLOWS]: {
workflow: createTestWorkflow({
nodes: [sourceNode, targetNode, stickyNote],
connections: {
[sourceNode.name]: {
main: [[{ node: targetNode.name, type: NodeConnectionTypes.Main, index: 0 }]],
},
},
}),
},
},
initialState: { [STORES.WORKFLOWS]: { workflowId } },
});
setActivePinia(pinia);
@ -6027,10 +5980,16 @@ describe('useCanvasOperations', () => {
vi.mocked(injectWorkflowState).mockReturnValue(workflowState);
workflowDocumentStoreInstance = useWorkflowDocumentStore(
createWorkflowDocumentId(useWorkflowsStore().workflowId),
);
) as WritableDocumentStore;
workflowDocumentStoreInstance.allNodes = [sourceNode, targetNode, stickyNote];
workflowDocumentStoreInstance.connectionsBySourceNode = {
[sourceNode.name]: {
main: [[{ node: targetNode.name, type: NodeConnectionTypes.Main, index: 0 }]],
},
};
vi.mocked(workflowDocumentStoreInstance.getConnectedNodes).mockReturnValue([]);
vi.mocked(workflowDocumentStoreInstance.getNodeByName).mockImplementation(
(name) => useWorkflowsStore().workflow.nodes.find((n) => n.name === name) ?? null,
(name) => workflowDocumentStoreInstance.allNodes.find((n) => n.name === name) ?? null,
);
const { getNodesToShift } = useCanvasOperations();
@ -6093,18 +6052,7 @@ describe('useCanvasOperations', () => {
});
const pinia = createTestingPinia({
initialState: {
[STORES.WORKFLOWS]: {
workflow: createTestWorkflow({
nodes: [sourceNode, targetNode, stickyAnchor, stickyWithTarget, stickyMove],
connections: {
[sourceNode.name]: {
main: [[{ node: targetNode.name, type: NodeConnectionTypes.Main, index: 0 }]],
},
},
}),
},
},
initialState: { [STORES.WORKFLOWS]: { workflowId } },
});
setActivePinia(pinia);
@ -6112,10 +6060,22 @@ describe('useCanvasOperations', () => {
vi.mocked(injectWorkflowState).mockReturnValue(workflowState);
workflowDocumentStoreInstance = useWorkflowDocumentStore(
createWorkflowDocumentId(useWorkflowsStore().workflowId),
);
) as WritableDocumentStore;
workflowDocumentStoreInstance.allNodes = [
sourceNode,
targetNode,
stickyAnchor,
stickyWithTarget,
stickyMove,
];
workflowDocumentStoreInstance.connectionsBySourceNode = {
[sourceNode.name]: {
main: [[{ node: targetNode.name, type: NodeConnectionTypes.Main, index: 0 }]],
},
};
vi.mocked(workflowDocumentStoreInstance.getConnectedNodes).mockReturnValue([]);
vi.mocked(workflowDocumentStoreInstance.getNodeByName).mockImplementation(
(name) => useWorkflowsStore().workflow.nodes.find((n) => n.name === name) ?? null,
(name) => workflowDocumentStoreInstance.allNodes.find((n) => n.name === name) ?? null,
);
const { getNodesToShift } = useCanvasOperations();
@ -6168,18 +6128,7 @@ describe('useCanvasOperations', () => {
});
const pinia = createTestingPinia({
initialState: {
[STORES.WORKFLOWS]: {
workflow: createTestWorkflow({
nodes: [sourceNode, targetNode, stickyNote],
connections: {
[sourceNode.name]: {
main: [[{ node: targetNode.name, type: NodeConnectionTypes.Main, index: 0 }]],
},
},
}),
},
},
initialState: { [STORES.WORKFLOWS]: { workflowId } },
});
setActivePinia(pinia);
@ -6187,10 +6136,16 @@ describe('useCanvasOperations', () => {
vi.mocked(injectWorkflowState).mockReturnValue(workflowState);
workflowDocumentStoreInstance = useWorkflowDocumentStore(
createWorkflowDocumentId(useWorkflowsStore().workflowId),
);
) as WritableDocumentStore;
workflowDocumentStoreInstance.allNodes = [sourceNode, targetNode, stickyNote];
workflowDocumentStoreInstance.connectionsBySourceNode = {
[sourceNode.name]: {
main: [[{ node: targetNode.name, type: NodeConnectionTypes.Main, index: 0 }]],
},
};
vi.mocked(workflowDocumentStoreInstance.getConnectedNodes).mockReturnValue([]);
vi.mocked(workflowDocumentStoreInstance.getNodeByName).mockImplementation(
(name) => useWorkflowsStore().workflow.nodes.find((n) => n.name === name) ?? null,
(name) => workflowDocumentStoreInstance.allNodes.find((n) => n.name === name) ?? null,
);
const { getNodesToShift } = useCanvasOperations();
@ -6251,23 +6206,7 @@ describe('useCanvasOperations', () => {
});
const pinia = createTestingPinia({
initialState: {
[STORES.WORKFLOWS]: {
workflow: createTestWorkflow({
nodes: [sourceNode, targetNode1, targetNode2, stickyNote],
connections: {
[sourceNode.name]: {
main: [
[
{ node: targetNode1.name, type: NodeConnectionTypes.Main, index: 0 },
{ node: targetNode2.name, type: NodeConnectionTypes.Main, index: 0 },
],
],
},
},
}),
},
},
initialState: { [STORES.WORKFLOWS]: { workflowId } },
});
setActivePinia(pinia);
@ -6275,10 +6214,21 @@ describe('useCanvasOperations', () => {
vi.mocked(injectWorkflowState).mockReturnValue(workflowState);
workflowDocumentStoreInstance = useWorkflowDocumentStore(
createWorkflowDocumentId(useWorkflowsStore().workflowId),
);
) as WritableDocumentStore;
workflowDocumentStoreInstance.allNodes = [sourceNode, targetNode1, targetNode2, stickyNote];
workflowDocumentStoreInstance.connectionsBySourceNode = {
[sourceNode.name]: {
main: [
[
{ node: targetNode1.name, type: NodeConnectionTypes.Main, index: 0 },
{ node: targetNode2.name, type: NodeConnectionTypes.Main, index: 0 },
],
],
},
};
vi.mocked(workflowDocumentStoreInstance.getConnectedNodes).mockReturnValue([]);
vi.mocked(workflowDocumentStoreInstance.getNodeByName).mockImplementation(
(name) => useWorkflowsStore().workflow.nodes.find((n) => n.name === name) ?? null,
(name) => workflowDocumentStoreInstance.allNodes.find((n) => n.name === name) ?? null,
);
const { getNodesToShift } = useCanvasOperations();
@ -6301,7 +6251,6 @@ describe('useCanvasOperations', () => {
describe('createConnectionToLastInteractedWithNode - HITL node handling', () => {
it('should create HITL node connection pattern when adding HITL tool node with existing connection', async () => {
const workflowsStore = mockedStore(useWorkflowsStore);
const uiStore = mockedStore(useUIStore);
const nodeTypesStore = mockedStore(useNodeTypesStore);
@ -6349,8 +6298,8 @@ describe('useCanvasOperations', () => {
return null;
});
workflowsStore.workflow.nodes = [agentNode, toolNode];
workflowsStore.workflow.connections = {
workflowDocumentStoreInstance.allNodes = [agentNode, toolNode];
workflowDocumentStoreInstance.connectionsBySourceNode = {
[agentNode.name]: {
[NodeConnectionTypes.AiTool]: [
[{ node: toolNode.name, type: NodeConnectionTypes.AiTool, index: 0 }],

View File

@ -1,6 +1,6 @@
import { JSONPath } from 'jsonpath-plus';
import { useDataSchema, useFlattenSchema, type SchemaNode } from './useDataSchema';
import type { INodeUi, Schema, IWorkflowDb } from '@/Interface';
import type { INodeUi, Schema } from '@/Interface';
import type { IExecutionResponse } from '@/features/execution/executions/executions.types';
import { setActivePinia } from 'pinia';
import { createTestingPinia } from '@pinia/testing';
@ -1173,24 +1173,7 @@ describe('useFlattenSchema', () => {
it('should flatten node schemas', () => {
vi.mocked(useWorkflowsStore).mockReturnValue({
...useWorkflowsStore(),
workflow: {
id: '1',
name: 'Test Workflow',
active: false,
activeVersionId: null,
isArchived: false,
createdAt: '2024-01-01',
updatedAt: '2024-01-01',
nodes: [],
connections: {},
settings: {
executionOrder: 'v1',
binaryMode: undefined,
},
tags: [],
pinData: {},
versionId: '',
} as IWorkflowDb,
workflowId: '1',
});
const { flattenMultipleSchemas } = useFlattenSchema();

View File

@ -34,7 +34,7 @@ describe(useFloatingUiOffsets, () => {
setActivePinia(createTestingPinia({ stubActions: false }));
currentRouteName = '';
const workflowsStore = useWorkflowsStore();
workflowsStore.workflow.id = 'test-workflow';
workflowsStore.setWorkflowId('test-workflow');
const workflowDocumentStore = useWorkflowDocumentStore(
createWorkflowDocumentId(workflowsStore.workflowId),
);

View File

@ -54,7 +54,7 @@ describe(useNodeDirtiness, () => {
setup() {
nodeTypeStore = useNodeTypesStore();
workflowsStore = useWorkflowsStore();
workflowsStore.workflow.id = TEST_WORKFLOW_ID;
workflowsStore.setWorkflowId(TEST_WORKFLOW_ID);
historyHelper = useHistoryHelper({} as RouteLocationNormalizedLoaded);
workflowState = useWorkflowState();
vi.mocked(injectWorkflowState).mockReturnValue(workflowState);
@ -164,11 +164,11 @@ describe(useNodeDirtiness, () => {
const workflowState = useWorkflowState();
workflowState.setWorkflowExecutionData({
id: workflowsStore.workflow.id,
id: workflowsStore.workflowId,
finished: true,
mode: 'manual',
status: 'success',
workflowData: workflowsStore.workflow,
workflowData: workflowDocumentStore.getSnapshot(),
startedAt: runAt,
createdAt: runAt,
data: createRunExecutionData({
@ -326,7 +326,7 @@ describe(useNodeDirtiness, () => {
// Simulate updating pinned data for node 'b' (set metadata timestamp as usePinnedData.setData would)
const workflowDocumentStore = useWorkflowDocumentStore(
createWorkflowDocumentId(workflowsStore.workflow.id),
createWorkflowDocumentId(workflowsStore.workflowId),
);
workflowDocumentStore.touchPinnedDataLastUpdatedAt('b');
@ -488,7 +488,7 @@ describe(useNodeDirtiness, () => {
const workflow = createTestWorkflow({ nodes: Object.values(nodes), connections });
const workflowDocumentStore = useWorkflowDocumentStore(
createWorkflowDocumentId(workflowsStore.workflow.id),
createWorkflowDocumentId(workflowsStore.workflowId),
);
workflowDocumentStore.setNodes(workflow.nodes);
workflowDocumentStore.setConnections(workflow.connections);

View File

@ -89,22 +89,18 @@ describe('usePinnedData', () => {
});
it('should set data correctly for valid inputs', () => {
const workflowsStore = useWorkflowsStore();
workflowsStore.workflow.id = 'test-workflow';
const node = ref({ name: 'testNode' } as INodeUi);
const { setData } = usePinnedData(node);
const testData = [{ json: { key: 'value' } }];
expect(() => setData(testData, 'pin-icon-click')).not.toThrow();
const workflowDocumentStore = useWorkflowDocumentStore(
createWorkflowDocumentId(workflowsStore.workflow.id),
);
const workflowDocumentStore = useWorkflowDocumentStore(createWorkflowDocumentId(''));
expect(workflowDocumentStore.pinData?.[node.value.name]).toEqual(testData);
});
it('should throw and not pin data when input contains the trimmed-execution-data marker', () => {
const workflowsStore = useWorkflowsStore();
workflowsStore.workflow.id = 'test-workflow';
workflowsStore.workflowId = 'test-workflow';
const telemetry = useTelemetry();
const trackSpy = vi.spyOn(telemetry, 'track');
const node = ref({ name: 'testNode' } as INodeUi);
@ -119,7 +115,7 @@ describe('usePinnedData', () => {
expect(() => setData(trimmedData, 'pin-icon-click')).toThrow();
const workflowDocumentStore = useWorkflowDocumentStore(
createWorkflowDocumentId(workflowsStore.workflow.id),
createWorkflowDocumentId(workflowsStore.workflowId),
);
expect(workflowDocumentStore.pinData?.[node.value.name]).toBeUndefined();
expect(trackSpy).toHaveBeenCalledWith(
@ -131,8 +127,6 @@ describe('usePinnedData', () => {
describe('unsetData()', () => {
it('should unset data correctly', () => {
const workflowsStore = useWorkflowsStore();
workflowsStore.workflow.id = 'test-workflow';
const node = ref({ name: 'testNode' } as INodeUi);
const { setData, unsetData } = usePinnedData(node);
const testData = [{ json: { key: 'value' } }];
@ -140,9 +134,7 @@ describe('usePinnedData', () => {
setData(testData, 'pin-icon-click');
unsetData('context-menu');
const workflowDocumentStore = useWorkflowDocumentStore(
createWorkflowDocumentId(workflowsStore.workflow.id),
);
const workflowDocumentStore = useWorkflowDocumentStore(createWorkflowDocumentId(''));
expect(workflowDocumentStore.pinData?.[node.value.name]).toBeUndefined();
});
});
@ -150,7 +142,7 @@ describe('usePinnedData', () => {
describe('onSetDataSuccess()', () => {
it('should trigger telemetry on successful data setting with correct payload values', async () => {
const workflowsStore = useWorkflowsStore();
workflowsStore.workflow.id = 'test-workflow-id';
workflowsStore.workflowId = 'test-workflow-id';
const telemetry = useTelemetry();
const spy = vi.spyOn(telemetry, 'track');
@ -209,11 +201,6 @@ describe('usePinnedData', () => {
});
describe('canPinData()', () => {
beforeEach(() => {
const workflowsStore = useWorkflowsStore();
workflowsStore.workflow.id = 'test-workflow';
});
afterEach(() => {
vi.clearAllMocks();
});

View File

@ -421,7 +421,7 @@ describe('usePostMessageHandler', () => {
mockOpenExecution.mockImplementation(async () => {
// Simulate what openExecution does: sets workflowId on the store
workflowsStore.workflow.id = 'test-wf-id';
workflowsStore.workflowId = 'test-wf-id';
return {
workflowData: { id: 'test-wf-id', name: 'Test' },
mode: 'trigger',

View File

@ -17,6 +17,10 @@ import type { WorkflowState } from '@/app/composables/useWorkflowState';
import { createTestingPinia } from '@pinia/testing';
import { setActivePinia } from 'pinia';
import { useWorkflowsStore } from '@/app/stores/workflows.store';
import {
useWorkflowDocumentStore,
createWorkflowDocumentId,
} from '@/app/stores/workflowDocument.store';
import { useWorkflowsListStore } from '@/app/stores/workflowsList.store';
import { useUIStore } from '@/app/stores/ui.store';
import { mockedStore } from '@/__tests__/utils';
@ -710,9 +714,10 @@ describe('manual execution stats tracking', () => {
},
} as unknown as IExecutionResponse);
workflowsStore.workflow.nodes = [
const workflowDocumentStore = useWorkflowDocumentStore(createWorkflowDocumentId(''));
workflowDocumentStore.setNodes([
mock<INodeUi>({ name: nodeName, type: 'n8n-nodes-base.telegram', typeVersion: 1 }),
];
]);
nodeTypesStore.getNodeType = () =>
mock<INodeTypeDescription>({ polling: undefined, group: [] });
@ -739,9 +744,10 @@ describe('manual execution stats tracking', () => {
},
} as unknown as IExecutionResponse);
workflowsStore.workflow.nodes = [
const workflowDocumentStore = useWorkflowDocumentStore(createWorkflowDocumentId(''));
workflowDocumentStore.setNodes([
mock<INodeUi>({ name: nodeName, type: 'n8n-nodes-base.vonage', typeVersion: 1 }),
];
]);
nodeTypesStore.getNodeType = () =>
mock<INodeTypeDescription>({ polling: undefined, group: [] });
@ -768,9 +774,10 @@ describe('manual execution stats tracking', () => {
},
} as unknown as IExecutionResponse);
workflowsStore.workflow.nodes = [
const docStore2 = useWorkflowDocumentStore(createWorkflowDocumentId(''));
docStore2.setNodes([
mock<INodeUi>({ name: nodeName, type: 'n8n-nodes-base.vonage', typeVersion: 1 }),
];
]);
nodeTypesStore.getNodeType = () =>
mock<INodeTypeDescription>({ polling: undefined, group: [] });

View File

@ -45,8 +45,8 @@ describe('executionStarted', () => {
it('should accept execution when activeExecutionId is null and populate workflowData from store', async () => {
workflowsStore.activeExecutionId = null;
workflowsStore.setWorkflowId('wf-123');
workflowsStore.setWorkflowExecutionData(null);
workflowsStore.workflow.id = 'wf-123';
const workflowDocumentStore = useWorkflowDocumentStore(createWorkflowDocumentId('wf-123'));
workflowDocumentStore.setName('My Workflow');

View File

@ -77,7 +77,7 @@ describe('workflowSettingsUpdated', () => {
});
it('does nothing for the document store when the workflow is not the active one', async () => {
workflowsStore.workflow.id = 'other-workflow';
workflowsStore.setWorkflowId('other-workflow');
await workflowSettingsUpdated(makeEvent('wf-1', { availableInMCP: true }));
@ -86,7 +86,7 @@ describe('workflowSettingsUpdated', () => {
});
it('merges settings and uses payload checksum for the active document', async () => {
workflowsStore.workflow.id = 'wf-current';
workflowsStore.setWorkflowId('wf-current');
workflowsListStore.workflowsById = {
'wf-current': {
id: 'wf-current',
@ -106,7 +106,7 @@ describe('workflowSettingsUpdated', () => {
});
it('applies settings but skips checksum refresh when none is provided', async () => {
workflowsStore.workflow.id = 'wf-current';
workflowsStore.setWorkflowId('wf-current');
workflowsListStore.workflowsById = {
'wf-current': {
id: 'wf-current',
@ -124,7 +124,7 @@ describe('workflowSettingsUpdated', () => {
});
it('merges multiple settings keys in one event', async () => {
workflowsStore.workflow.id = 'wf-current';
workflowsStore.setWorkflowId('wf-current');
workflowsListStore.workflowsById = {
'wf-current': {
id: 'wf-current',

View File

@ -122,7 +122,7 @@ describe('useResolvedExpression', () => {
it('should re-resolve when workflow name changes', async () => {
const workflowsStore = useWorkflowsStore();
workflowsStore.workflow.id = 'test-workflow';
workflowsStore.setWorkflowId('test-workflow');
const workflowDocumentStore = useWorkflowDocumentStore(
createWorkflowDocumentId('test-workflow'),
);

View File

@ -101,18 +101,6 @@ vi.mock('@/app/stores/workflows.store', () => {
previousExecutionId: undefined,
executionWaitingForWebhook: false,
chatPartialExecutionDestinationNode: null,
workflow: {
nodes: [],
id: '',
name: '',
active: false,
isArchived: false,
createdAt: '',
updatedAt: '',
connections: {},
versionId: '',
activeVersionId: null,
},
workflowId: '123',
isWorkflowSaved: {
'123': true,

View File

@ -46,7 +46,7 @@ describe('useToolParameters', () => {
// Setup default mocks
mockWorkflowDocumentStore.getNodeByName.mockReset();
projectsStore.currentProjectId = 'test-project';
workflowsStore.workflowId = 'test-workflow';
workflowsStore.setWorkflowId('test-workflow');
workflowsStore.getWorkflowExecution = null;
agentRequestStore.getQueryValue = vi.fn().mockReturnValue(null);
});

View File

@ -13,7 +13,7 @@ const TEST_WF_ID = 'test-wf-id';
describe('useUniqueNodeName', () => {
beforeAll(() => {
setActivePinia(createTestingPinia());
useWorkflowsStore().workflow.id = TEST_WF_ID;
useWorkflowsStore().workflowId = TEST_WF_ID;
});
afterEach(() => {

View File

@ -832,11 +832,6 @@ export function useWorkflowHelpers() {
workflowsListStore.updateWorkflowInCache(workflowData.id, { name: payload.name });
});
// Sync document store versionId → workflow ref (for IWorkflowDb compatibility)
initializedWorkflowDocumentStore.onVersionDataChange(({ payload }) => {
workflowsStore.workflow.versionId = payload.versionId;
});
initializedWorkflowDocumentStore.setName(workflowData.name);
initializedWorkflowDocumentStore.setTags(tagIds);
initializedWorkflowDocumentStore.setActiveState({

View File

@ -140,10 +140,11 @@ describe('useWorkflowSaving', () => {
checksum: 'test-checksum',
});
workflowsStore.workflow = workflow;
workflowsStore.setWorkflowId(workflow.id);
useWorkflowDocumentStore(createWorkflowDocumentId(workflow.id)).hydrate(workflow);
// Populate workflowsById to mark workflow as existing (not new)
workflowsListStore.workflowsById = { [workflow.id]: workflow };
workflowsStore.workflowId = workflow.id;
workflowsStore.setWorkflowId(workflow.id);
const next = vi.fn();
const confirm = vi.fn().mockResolvedValue(true);
@ -223,7 +224,7 @@ describe('useWorkflowSaving', () => {
const workflowListStore = useWorkflowsListStore();
const MOCK_ID = 'existing-workflow-id';
const existingWorkflow = createTestWorkflow({ id: MOCK_ID });
workflowStore.workflow.id = MOCK_ID;
workflowStore.setWorkflowId(MOCK_ID);
// Populate workflowsById to mark workflow as existing (not new)
workflowListStore.workflowsById = { [MOCK_ID]: existingWorkflow };
@ -258,7 +259,7 @@ describe('useWorkflowSaving', () => {
uiStore.markStateDirty();
const workflowStore = useWorkflowsStore();
workflowStore.workflow.id = '';
workflowStore.setWorkflowId('');
// Mock message.confirm
modalConfirmSpy.mockResolvedValue('close');
@ -308,7 +309,8 @@ describe('useWorkflowSaving', () => {
vi.spyOn(workflowsListStore, 'fetchWorkflow').mockResolvedValue(workflow);
vi.spyOn(workflowsStore, 'updateWorkflow').mockResolvedValue(workflow);
workflowsStore.workflow = workflow;
workflowsStore.setWorkflowId(workflow.id);
useWorkflowDocumentStore(createWorkflowDocumentId(workflow.id)).hydrate(workflow);
// Populate workflowsById to mark workflow as existing (not new)
workflowsListStore.workflowsById = { [workflow.id]: workflow };
@ -458,7 +460,8 @@ describe('useWorkflowSaving', () => {
vi.spyOn(workflowsListStore, 'fetchWorkflow').mockResolvedValue(workflow);
vi.spyOn(workflowsStore, 'updateWorkflow').mockResolvedValue(workflow);
workflowsStore.workflow = workflow;
workflowsStore.setWorkflowId(workflow.id);
useWorkflowDocumentStore(createWorkflowDocumentId(workflow.id)).hydrate(workflow);
workflowsListStore.workflowsById = { [workflow.id]: workflow };
setDocumentStoreActive(workflow.id);
@ -481,7 +484,8 @@ describe('useWorkflowSaving', () => {
vi.spyOn(workflowsListStore, 'fetchWorkflow').mockResolvedValue(workflow);
vi.spyOn(workflowsStore, 'updateWorkflow').mockResolvedValue(workflow);
workflowsStore.workflow = workflow;
workflowsStore.setWorkflowId(workflow.id);
useWorkflowDocumentStore(createWorkflowDocumentId(workflow.id)).hydrate(workflow);
// Populate workflowsById to mark workflow as existing (not new)
workflowsListStore.workflowsById = { [workflow.id]: workflow };
@ -504,7 +508,8 @@ describe('useWorkflowSaving', () => {
vi.spyOn(workflowsListStore, 'fetchWorkflow').mockResolvedValue(workflow);
vi.spyOn(workflowsStore, 'updateWorkflow').mockResolvedValue(workflow);
workflowsStore.workflow = workflow;
workflowsStore.setWorkflowId(workflow.id);
useWorkflowDocumentStore(createWorkflowDocumentId(workflow.id)).hydrate(workflow);
workflowsListStore.workflowsById = { w2: workflow };
workflowsStore.isWorkflowSaved = { w2: true };
setDocumentStoreActive(workflow.id);
@ -528,7 +533,8 @@ describe('useWorkflowSaving', () => {
vi.spyOn(workflowsListStore, 'fetchWorkflow').mockResolvedValue(workflow);
vi.spyOn(workflowsStore, 'updateWorkflow').mockResolvedValue(workflow);
workflowsStore.workflow = workflow;
workflowsStore.setWorkflowId(workflow.id);
useWorkflowDocumentStore(createWorkflowDocumentId(workflow.id)).hydrate(workflow);
workflowsListStore.workflowsById = { w3: workflow };
workflowsStore.isWorkflowSaved = { w3: true };
setDocumentStoreActive(workflow.id);
@ -565,9 +571,10 @@ describe('useWorkflowSaving', () => {
vi.spyOn(workflowsListStore, 'fetchWorkflow').mockResolvedValue(workflow);
vi.spyOn(workflowsStore, 'updateWorkflow').mockResolvedValue(workflowResponse);
workflowsStore.workflow = workflow;
workflowsStore.setWorkflowId(workflow.id);
useWorkflowDocumentStore(createWorkflowDocumentId(workflow.id)).hydrate(workflow);
workflowsListStore.workflowsById = { [workflow.id]: workflow };
workflowsStore.workflowId = workflow.id;
workflowsStore.setWorkflowId(workflow.id);
// Tags are now managed by workflowDocumentStore, not workflowState
const documentId = createWorkflowDocumentId(workflowId);
@ -634,9 +641,10 @@ describe('useWorkflowSaving', () => {
checksum: 'test-checksum',
});
workflowsStore.workflow = workflow;
workflowsStore.setWorkflowId(workflow.id);
useWorkflowDocumentStore(createWorkflowDocumentId(workflow.id)).hydrate(workflow);
workflowsListStore.workflowsById = { [workflow.id]: workflow };
workflowsStore.workflowId = workflow.id;
workflowsStore.setWorkflowId(workflow.id);
const uiStore = useUIStore();
const saveStore = useWorkflowSaveStore();
@ -675,9 +683,10 @@ describe('useWorkflowSaving', () => {
checksum: 'test-checksum',
});
workflowsStore.workflow = workflow;
workflowsStore.setWorkflowId(workflow.id);
useWorkflowDocumentStore(createWorkflowDocumentId(workflow.id)).hydrate(workflow);
workflowsListStore.workflowsById = { [workflow.id]: workflow };
workflowsStore.workflowId = workflow.id;
workflowsStore.setWorkflowId(workflow.id);
const uiStore = useUIStore();
@ -708,9 +717,10 @@ describe('useWorkflowSaving', () => {
checksum: 'test-checksum',
});
workflowsStore.workflow = workflow;
workflowsStore.setWorkflowId(workflow.id);
useWorkflowDocumentStore(createWorkflowDocumentId(workflow.id)).hydrate(workflow);
workflowsListStore.workflowsById = { [workflow.id]: workflow };
workflowsStore.workflowId = workflow.id;
workflowsStore.setWorkflowId(workflow.id);
const saveStore = useWorkflowSaveStore();
@ -769,9 +779,10 @@ describe('useWorkflowSaving', () => {
versionId: 'v2',
});
workflowsStore.workflow = workflow;
workflowsStore.setWorkflowId(workflow.id);
useWorkflowDocumentStore(createWorkflowDocumentId(workflow.id)).hydrate(workflow);
workflowsListStore.workflowsById = { [workflow.id]: workflow };
workflowsStore.workflowId = workflow.id;
workflowsStore.setWorkflowId(workflow.id);
const { saveCurrentWorkflow } = useWorkflowSaving({
router,
@ -821,9 +832,10 @@ describe('useWorkflowSaving', () => {
versionId: 'v1',
});
workflowsStore.workflow = workflow;
workflowsStore.setWorkflowId(workflow.id);
useWorkflowDocumentStore(createWorkflowDocumentId(workflow.id)).hydrate(workflow);
workflowsListStore.workflowsById = { [workflow.id]: workflow };
workflowsStore.workflowId = workflow.id;
workflowsStore.setWorkflowId(workflow.id);
const saveStore = useWorkflowSaveStore();
@ -878,9 +890,10 @@ describe('useWorkflowSaving', () => {
async () => await blockedPromise,
);
workflowsStore.workflow = workflow;
workflowsStore.setWorkflowId(workflow.id);
useWorkflowDocumentStore(createWorkflowDocumentId(workflow.id)).hydrate(workflow);
workflowsListStore.workflowsById = { [workflow.id]: workflow };
workflowsStore.workflowId = workflow.id;
workflowsStore.setWorkflowId(workflow.id);
const saveStore = useWorkflowSaveStore();
@ -951,9 +964,10 @@ describe('useWorkflowSaving', () => {
};
});
workflowsStore.workflow = workflow;
workflowsStore.setWorkflowId(workflow.id);
useWorkflowDocumentStore(createWorkflowDocumentId(workflow.id)).hydrate(workflow);
workflowsListStore.workflowsById = { [workflow.id]: workflow };
workflowsStore.workflowId = workflow.id;
workflowsStore.setWorkflowId(workflow.id);
const saveStore = useWorkflowSaveStore();
@ -1010,9 +1024,10 @@ describe('useWorkflowSaving', () => {
vi.spyOn(workflowsListStore, 'fetchWorkflow').mockResolvedValue(workflow);
vi.spyOn(workflowsStore, 'updateWorkflow').mockRejectedValue(new Error('Network error'));
workflowsStore.workflow = workflow;
workflowsStore.setWorkflowId(workflow.id);
useWorkflowDocumentStore(createWorkflowDocumentId(workflow.id)).hydrate(workflow);
workflowsListStore.workflowsById = { [workflow.id]: workflow };
workflowsStore.workflowId = workflow.id;
workflowsStore.setWorkflowId(workflow.id);
const saveStore = useWorkflowSaveStore();
@ -1040,9 +1055,10 @@ describe('useWorkflowSaving', () => {
const errorMessage = 'Network timeout';
vi.spyOn(workflowsStore, 'updateWorkflow').mockRejectedValue(new Error(errorMessage));
workflowsStore.workflow = workflow;
workflowsStore.setWorkflowId(workflow.id);
useWorkflowDocumentStore(createWorkflowDocumentId(workflow.id)).hydrate(workflow);
workflowsListStore.workflowsById = { [workflow.id]: workflow };
workflowsStore.workflowId = workflow.id;
workflowsStore.setWorkflowId(workflow.id);
const saveStore = useWorkflowSaveStore();
const initialRetryCount = saveStore.retryCount;

View File

@ -12,7 +12,7 @@ import { useNodeTypesStore } from '@/app/stores/nodeTypes.store';
import { useBuilderStore } from '@/features/ai/assistant/builder.store';
import { mockedStore } from '@/__tests__/utils';
import { createTestNode } from '@/__tests__/mocks';
import type { INodeUi, IWorkflowDb } from '@/Interface';
import type { INodeUi } from '@/Interface';
import { DEFAULT_NEW_WORKFLOW_NAME } from '@/app/constants';
import type { Workflow } from 'n8n-workflow';
@ -115,13 +115,6 @@ describe('useWorkflowUpdate', () => {
vi.mocked(mockDocumentStore.setNodeIssue).mockClear();
vi.mocked(mockDocumentStore.updateNodeProperties).mockClear();
workflowsStore.workflowId = 'test-workflow';
workflowsStore.workflow = {
id: 'test-workflow',
name: DEFAULT_NEW_WORKFLOW_NAME,
nodes: [],
connections: {},
} as Partial<IWorkflowDb> as IWorkflowDb;
workflowsStore.workflowId = 'test-workflow';
vi.mocked(mockDocumentStore.cloneWorkflowObject).mockReturnValue({
nodes: {},
connectionsBySourceNode: {},

View File

@ -19,7 +19,6 @@ import { describe, it, expect, vi, beforeEach } from 'vitest';
import { setActivePinia, createPinia } from 'pinia';
import type { IConnection, NodeConnectionType } from 'n8n-workflow';
import { NodeConnectionTypes } from 'n8n-workflow';
import { useWorkflowsStore } from '@/app/stores/workflows.store';
import { createTestNode } from '@/__tests__/mocks';
import type { INodeUi } from '@/Interface';
import {
@ -60,15 +59,11 @@ function createConnectionData(
}
describe('useWorkflowDocumentConnections', () => {
let workflowsStore: ReturnType<typeof useWorkflowsStore>;
let deps: WorkflowDocumentConnectionsDeps;
beforeEach(() => {
setActivePinia(createPinia());
workflowsStore = useWorkflowsStore();
deps = createDeps();
workflowsStore.workflow.connections = {};
});
describe('round-trip: setConnections → read', () => {

View File

@ -1,8 +1,7 @@
import { computed } from 'vue';
import { computed, ref } from 'vue';
import { createEventHook } from '@vueuse/core';
import type { IConnection, IConnections, INodeConnections } from 'n8n-workflow';
import type { INodeUi } from '@/Interface';
import { useWorkflowsStore } from '@/app/stores/workflows.store';
import { CHANGE_ACTION } from './types';
import type { ChangeEvent } from './types';
import * as workflowUtils from 'n8n-workflow/common';
@ -32,7 +31,7 @@ export interface WorkflowDocumentConnectionsDeps {
// private state owned by workflowDocumentStore. Once that happens, the direct import
// (and the import-cycle warning it causes) will go away.
export function useWorkflowDocumentConnections(deps: WorkflowDocumentConnectionsDeps) {
const workflowsStore = useWorkflowsStore();
const connections = ref<IConnections>({});
const onConnectionsChange = createEventHook<ConnectionsChangeEvent>();
// eslint-disable-next-line @typescript-eslint/no-invalid-void-type
@ -43,8 +42,8 @@ export function useWorkflowDocumentConnections(deps: WorkflowDocumentConnections
// -----------------------------------------------------------------------
function applySetConnections(value: IConnections) {
workflowsStore.workflow.connections = value;
deps.syncWorkflowObject(workflowsStore.workflow.connections);
connections.value = value;
deps.syncWorkflowObject(connections.value);
}
function applyAddConnection(data: { connection: IConnection[] }) {
@ -52,7 +51,7 @@ export function useWorkflowDocumentConnections(deps: WorkflowDocumentConnections
const sourceData: IConnection = data.connection[0];
const destinationData: IConnection = data.connection[1];
const wfConnections = workflowsStore.workflow.connections;
const wfConnections = connections.value;
if (!wfConnections.hasOwnProperty(sourceData.node)) {
wfConnections[sourceData.node] = {};
@ -100,7 +99,7 @@ export function useWorkflowDocumentConnections(deps: WorkflowDocumentConnections
}
}
deps.syncWorkflowObject(workflowsStore.workflow.connections);
deps.syncWorkflowObject(connections.value);
void onConnectionsChange.trigger({
action: CHANGE_ACTION.ADD,
payload: { connection: data.connection },
@ -111,26 +110,26 @@ export function useWorkflowDocumentConnections(deps: WorkflowDocumentConnections
function applyRemoveConnection(data: { connection: IConnection[] }) {
const sourceData = data.connection[0];
const destinationData = data.connection[1];
const wfConnections = workflowsStore.workflow.connections;
if (!wfConnections.hasOwnProperty(sourceData.node)) return;
if (!wfConnections[sourceData.node].hasOwnProperty(sourceData.type)) return;
if (wfConnections[sourceData.node][sourceData.type].length < sourceData.index + 1) return;
if (!connections.value.hasOwnProperty(sourceData.node)) return;
if (!connections.value[sourceData.node].hasOwnProperty(sourceData.type)) return;
if (connections.value[sourceData.node][sourceData.type].length < sourceData.index + 1) return;
const connections = wfConnections[sourceData.node][sourceData.type][sourceData.index];
if (!connections) return;
const matchedConnections =
connections.value[sourceData.node][sourceData.type][sourceData.index];
if (!matchedConnections) return;
for (const index in connections) {
for (const index in matchedConnections) {
if (
connections[index].node === destinationData.node &&
connections[index].type === destinationData.type &&
connections[index].index === destinationData.index
matchedConnections[index].node === destinationData.node &&
matchedConnections[index].type === destinationData.type &&
matchedConnections[index].index === destinationData.index
) {
connections.splice(Number.parseInt(index, 10), 1);
matchedConnections.splice(Number.parseInt(index, 10), 1);
}
}
deps.syncWorkflowObject(workflowsStore.workflow.connections);
deps.syncWorkflowObject(connections.value);
void onConnectionsChange.trigger({
action: CHANGE_ACTION.DELETE,
payload: { connection: data.connection },
@ -144,7 +143,7 @@ export function useWorkflowDocumentConnections(deps: WorkflowDocumentConnections
) {
const preserveInput = opts?.preserveInputConnections ?? false;
const preserveOutput = opts?.preserveOutputConnections ?? false;
const wfConnections = workflowsStore.workflow.connections;
const wfConnections = connections.value;
if (!preserveOutput) {
delete wfConnections[node.name];
@ -172,7 +171,7 @@ export function useWorkflowDocumentConnections(deps: WorkflowDocumentConnections
}
}
deps.syncWorkflowObject(workflowsStore.workflow.connections);
deps.syncWorkflowObject(connections.value);
void onConnectionsChange.trigger({
action: CHANGE_ACTION.DELETE,
payload: { nodeName: node.name },
@ -181,18 +180,18 @@ export function useWorkflowDocumentConnections(deps: WorkflowDocumentConnections
}
function applyRemoveAllConnections() {
workflowsStore.workflow.connections = {};
deps.syncWorkflowObject(workflowsStore.workflow.connections);
connections.value = {};
deps.syncWorkflowObject(connections.value);
}
// -----------------------------------------------------------------------
// Read API
// -----------------------------------------------------------------------
const connectionsBySourceNode = computed(() => workflowsStore.workflow.connections);
const connectionsBySourceNode = computed(() => connections.value);
const connectionsByDestinationNode = computed<IConnections>(() =>
workflowUtils.mapConnectionsByDestination(workflowsStore.workflow.connections),
workflowUtils.mapConnectionsByDestination(connections.value),
);
function outgoingConnectionsByNodeName(nodeName: string): INodeConnections {

View File

@ -25,7 +25,6 @@ import { CHAT_TRIGGER_NODE_TYPE, NodeConnectionTypes } from 'n8n-workflow';
import type { IConnections } from 'n8n-workflow';
import { createTestNode } from '@/__tests__/mocks';
import type { INodeUi } from '@/Interface';
import { useWorkflowsStore } from '@/app/stores/workflows.store';
import {
useWorkflowDocumentNodes,
type WorkflowDocumentNodesDeps,
@ -53,11 +52,9 @@ describe('useWorkflowDocumentGraph', () => {
let nodes: ReturnType<typeof useWorkflowDocumentNodes>;
let connections: ReturnType<typeof useWorkflowDocumentConnections>;
let workflowObj: ReturnType<typeof useWorkflowDocumentWorkflowObject>;
let workflowsStore: ReturnType<typeof useWorkflowsStore>;
beforeEach(() => {
setActivePinia(createPinia());
workflowsStore = useWorkflowsStore();
nodes = useWorkflowDocumentNodes(createNodesDeps());
connections = useWorkflowDocumentConnections({
getNodeById: (id) => nodes.getNodeById(id),
@ -74,8 +71,8 @@ describe('useWorkflowDocumentGraph', () => {
): ReturnType<typeof useWorkflowDocumentGraph> {
nodes.setNodes(nodeList);
connections.setConnections(connectionMap);
workflowObj.syncWorkflowObjectNodes(workflowsStore.workflow.nodes);
workflowObj.syncWorkflowObjectConnections(workflowsStore.workflow.connections);
workflowObj.syncWorkflowObjectNodes(nodes.allNodes.value);
workflowObj.syncWorkflowObjectConnections(connections.connectionsBySourceNode.value);
return useWorkflowDocumentGraph(workflowObj.workflowObject);
}

View File

@ -1,4 +1,4 @@
import { computed } from 'vue';
import { computed, ref } from 'vue';
import { createEventHook } from '@vueuse/core';
import type {
INode,
@ -26,7 +26,6 @@ import { CHANGE_ACTION } from './types';
import type { ChangeEvent } from './types';
import type { useWorkflowDocumentNodeMetadata } from './useWorkflowDocumentNodeMetadata';
import { isPresent } from '@/app/utils/typesUtils';
import { useWorkflowsStore } from '../workflows.store';
import { useNodeTypesStore } from '../nodeTypes.store';
// --- Event types ---
@ -61,7 +60,7 @@ export interface WorkflowDocumentNodesDeps {
// will go away.
export function useWorkflowDocumentNodes(deps: WorkflowDocumentNodesDeps) {
const nodeTypesStore = useNodeTypesStore();
const workflowsStore = useWorkflowsStore();
const nodes = ref<INodeUi[]>([]);
const onNodesChange = createEventHook<NodesChangeEvent>();
// eslint-disable-next-line @typescript-eslint/no-invalid-void-type
@ -78,14 +77,14 @@ export function useWorkflowDocumentNodes(deps: WorkflowDocumentNodesDeps) {
function updateNodeAtIndex(nodeIndex: number, nodeData: Partial<INodeUi>): boolean {
if (nodeIndex === -1) return false;
const node = workflowsStore.workflow.nodes[nodeIndex];
const node = nodes.value[nodeIndex];
const existingData = pick<Partial<INodeUi>>(node, Object.keys(nodeData));
const changed = !isEqual(existingData, nodeData);
if (changed) {
Object.assign(node, nodeData);
workflowsStore.workflow.nodes[nodeIndex] = node;
deps.syncWorkflowObject(workflowsStore.workflow.nodes);
nodes.value[nodeIndex] = node;
deps.syncWorkflowObject(nodes.value);
void onNodesChange.trigger({
action: CHANGE_ACTION.UPDATE,
payload: { name: node.name },
@ -99,8 +98,8 @@ export function useWorkflowDocumentNodes(deps: WorkflowDocumentNodesDeps) {
// Apply methods — will become the CRDT entry point. Today they delegate.
// -----------------------------------------------------------------------
function applySetNodes(nodes: INodeUi[]) {
for (const node of nodes) {
function applySetNodes(newNodes: INodeUi[]) {
for (const node of newNodes) {
if (!node.id) {
deps.assignNodeId(node);
}
@ -114,11 +113,11 @@ export function useWorkflowDocumentNodes(deps: WorkflowDocumentNodesDeps) {
}
}
workflowsStore.workflow.nodes = nodes;
deps.syncWorkflowObject(workflowsStore.workflow.nodes);
nodes.value = newNodes;
deps.syncWorkflowObject(nodes.value);
// setNodes replaces the full node list, so reset metadata to match
deps.nodeMetadata.setAllNodeMetadata({});
for (const node of nodes) {
for (const node of newNodes) {
deps.nodeMetadata.initPristineNodeMetadata(node.name);
}
}
@ -128,8 +127,8 @@ export function useWorkflowDocumentNodes(deps: WorkflowDocumentNodesDeps) {
return;
}
workflowsStore.workflow.nodes.push(node);
deps.syncWorkflowObject(workflowsStore.workflow.nodes);
nodes.value.push(node);
deps.syncWorkflowObject(nodes.value);
deps.nodeMetadata.initNodeMetadata(node.name);
void onNodesChange.trigger({
action: CHANGE_ACTION.ADD,
@ -139,15 +138,12 @@ export function useWorkflowDocumentNodes(deps: WorkflowDocumentNodesDeps) {
}
function applyRemoveNode(node: INodeUi) {
const idx = workflowsStore.workflow.nodes.findIndex((n) => n.name === node.name);
const idx = nodes.value.findIndex((n) => n.name === node.name);
if (idx !== -1) {
workflowsStore.workflow.nodes = [
...workflowsStore.workflow.nodes.slice(0, idx),
...workflowsStore.workflow.nodes.slice(idx + 1),
];
nodes.value = [...nodes.value.slice(0, idx), ...nodes.value.slice(idx + 1)];
}
deps.syncWorkflowObject(workflowsStore.workflow.nodes);
deps.syncWorkflowObject(nodes.value);
deps.nodeMetadata.removeNodeMetadata(node.name);
deps.unpinNodeData(node.name);
void onNodesChange.trigger({
@ -158,15 +154,12 @@ export function useWorkflowDocumentNodes(deps: WorkflowDocumentNodesDeps) {
}
function applyRemoveNodeById(id: string) {
const node = workflowsStore.workflow.nodes.find((n) => n.id === id);
const idx = workflowsStore.workflow.nodes.findIndex((n) => n.id === id);
const node = nodes.value.find((n) => n.id === id);
const idx = nodes.value.findIndex((n) => n.id === id);
if (idx !== -1) {
workflowsStore.workflow.nodes = [
...workflowsStore.workflow.nodes.slice(0, idx),
...workflowsStore.workflow.nodes.slice(idx + 1),
];
nodes.value = [...nodes.value.slice(0, idx), ...nodes.value.slice(idx + 1)];
}
deps.syncWorkflowObject(workflowsStore.workflow.nodes);
deps.syncWorkflowObject(nodes.value);
if (node) {
deps.nodeMetadata.removeNodeMetadata(node.name);
deps.unpinNodeData(node.name);
@ -182,10 +175,10 @@ export function useWorkflowDocumentNodes(deps: WorkflowDocumentNodesDeps) {
// Read API
// -----------------------------------------------------------------------
const allNodes = computed<INodeUi[]>(() => workflowsStore.workflow.nodes);
const allNodes = computed<INodeUi[]>(() => nodes.value);
const nodesByName = computed(() => {
return allNodes.value.reduce<Record<string, INodeUi>>((acc, node) => {
return nodes.value.reduce<Record<string, INodeUi>>((acc, node) => {
acc[node.name] = node;
return acc;
}, {});
@ -194,14 +187,14 @@ export function useWorkflowDocumentNodes(deps: WorkflowDocumentNodesDeps) {
const canvasNames = computed(() => new Set(allNodes.value.map((n) => n.name)));
const workflowTriggerNodes = computed(() =>
allNodes.value.filter((node: INodeUi) => {
nodes.value.filter((node: INodeUi) => {
const nodeType = nodeTypesStore.getNodeType(node.type, node.typeVersion);
return nodeType && nodeType.group.includes('trigger');
}),
);
function getNodeById(id: string): INodeUi | undefined {
return workflowsStore.workflow.nodes.find((node) => node.id === id);
return nodes.value.find((node) => node.id === id);
}
function getNodeByName(name: string): INodeUi | null {
@ -209,7 +202,7 @@ export function useWorkflowDocumentNodes(deps: WorkflowDocumentNodesDeps) {
}
function findNodeByPartialId(partialId: string): INodeUi | undefined {
return workflowsStore.workflow.nodes.find((node) => node.id.startsWith(partialId));
return nodes.value.find((node) => node.id.startsWith(partialId));
}
function getNodesByIds(nodeIds: string[]): INodeUi[] {
@ -237,9 +230,7 @@ export function useWorkflowDocumentNodes(deps: WorkflowDocumentNodesDeps) {
}
function setNodeParameters(updateInformation: IUpdateInformation, append?: boolean): void {
const nodeIndex = workflowsStore.workflow.nodes.findIndex(
(node) => node.name === updateInformation.name,
);
const nodeIndex = nodes.value.findIndex((node) => node.name === updateInformation.name);
if (nodeIndex === -1) {
throw new Error(
@ -247,7 +238,7 @@ export function useWorkflowDocumentNodes(deps: WorkflowDocumentNodesDeps) {
);
}
const { name, parameters } = workflowsStore.workflow.nodes[nodeIndex];
const { name, parameters } = nodes.value[nodeIndex];
const newParameters =
!!append && isObject(updateInformation.value)
@ -265,10 +256,7 @@ export function useWorkflowDocumentNodes(deps: WorkflowDocumentNodesDeps) {
}
function setLastNodeParameters(updateInformation: IUpdateInformation): void {
const latestNode = findLast(
workflowsStore.workflow.nodes,
(node) => node.type === updateInformation.key,
);
const latestNode = findLast(nodes.value, (node) => node.type === updateInformation.key);
if (!latestNode) return;
const nodeType = deps.getNodeType(latestNode.type);
@ -287,9 +275,7 @@ export function useWorkflowDocumentNodes(deps: WorkflowDocumentNodesDeps) {
}
function setNodeValue(updateInformation: IUpdateInformation): void {
const nodeIndex = workflowsStore.workflow.nodes.findIndex(
(node) => node.name === updateInformation.name,
);
const nodeIndex = nodes.value.findIndex((node) => node.name === updateInformation.name);
if (nodeIndex === -1 || !updateInformation.key) {
throw new Error(
@ -308,27 +294,25 @@ export function useWorkflowDocumentNodes(deps: WorkflowDocumentNodesDeps) {
const excludeKeys = ['position', 'notes', 'notesInFlow'];
if (changed && !excludeKeys.includes(updateInformation.key)) {
deps.nodeMetadata.touchParametersLastUpdatedAt(workflowsStore.workflow.nodes[nodeIndex].name);
deps.nodeMetadata.touchParametersLastUpdatedAt(nodes.value[nodeIndex].name);
}
}
function setNodePositionById(id: string, position: XYPosition): void {
const node = workflowsStore.workflow.nodes.find((n) => n.id === id);
const node = nodes.value.find((n) => n.id === id);
if (!node) return;
setNodeValue({ name: node.name, key: 'position', value: position });
}
function updateNodeById(nodeId: string, nodeData: Partial<INodeUi>): boolean {
const nodeIndex = workflowsStore.workflow.nodes.findIndex((node) => node.id === nodeId);
const nodeIndex = nodes.value.findIndex((node) => node.id === nodeId);
if (nodeIndex === -1) return false;
return updateNodeAtIndex(nodeIndex, nodeData);
}
function updateNodeProperties(updateInformation: INodeUpdatePropertiesInformation): void {
const nodeIndex = workflowsStore.workflow.nodes.findIndex(
(node) => node.name === updateInformation.name,
);
const nodeIndex = nodes.value.findIndex((node) => node.name === updateInformation.name);
if (nodeIndex !== -1) {
for (const key of Object.keys(updateInformation.properties)) {
@ -345,14 +329,12 @@ export function useWorkflowDocumentNodes(deps: WorkflowDocumentNodesDeps) {
}
function setNodeIssue(nodeIssueData: INodeIssueData): void {
const nodeIndex = workflowsStore.workflow.nodes.findIndex(
(node) => node.name === nodeIssueData.node,
);
const nodeIndex = nodes.value.findIndex((node) => node.name === nodeIssueData.node);
if (nodeIndex === -1) {
return;
}
const node = workflowsStore.workflow.nodes[nodeIndex];
const node = nodes.value[nodeIndex];
if (nodeIssueData.value === null) {
if (node.issues?.[nodeIssueData.type] === undefined) {
@ -375,8 +357,8 @@ export function useWorkflowDocumentNodes(deps: WorkflowDocumentNodesDeps) {
}
function removeAllNodes(): void {
workflowsStore.workflow.nodes.splice(0, workflowsStore.workflow.nodes.length);
deps.syncWorkflowObject(workflowsStore.workflow.nodes);
nodes.value.splice(0, nodes.value.length);
deps.syncWorkflowObject(nodes.value);
deps.nodeMetadata.setAllNodeMetadata({});
void onNodesChange.trigger({
action: CHANGE_ACTION.DELETE,
@ -385,7 +367,7 @@ export function useWorkflowDocumentNodes(deps: WorkflowDocumentNodesDeps) {
}
function resetAllNodesIssues(): boolean {
workflowsStore.workflow.nodes.forEach((node) => {
nodes.value.forEach((node) => {
node.issues = undefined;
});
return true;
@ -397,7 +379,7 @@ export function useWorkflowDocumentNodes(deps: WorkflowDocumentNodesDeps) {
invalid: INodeCredentialsDetails;
type: string;
}) {
workflowsStore.workflow.nodes.forEach((node: INodeUi) => {
nodes.value.forEach((node: INodeUi) => {
const nodeCredentials: INodeCredentials | undefined = (node as unknown as INode).credentials;
if (!nodeCredentials?.[data.type]) {
return;
@ -434,7 +416,7 @@ export function useWorkflowDocumentNodes(deps: WorkflowDocumentNodesDeps) {
}): number {
let updatedNodesCount = 0;
workflowsStore.workflow.nodes.forEach((node: INodeUi) => {
nodes.value.forEach((node: INodeUi) => {
// Skip the current node (it was just set)
if (node.name === data.currentNodeName) {
return;

View File

@ -106,7 +106,7 @@ describe('useWorkflowsStore', () => {
});
it('should initialize with default state', () => {
expect(workflowsStore.workflow.id).toBe('');
expect(workflowsStore.workflowId).toBe('');
});
describe('allWorkflows', () => {
@ -415,7 +415,7 @@ describe('useWorkflowsStore', () => {
const workflowsListStore = useWorkflowsListStore();
uiStore.markStateDirty();
workflowsListStore.workflowsById = { '1': { active: false } as IWorkflowDb };
workflowsStore.workflow.id = '1';
workflowsStore.setWorkflowId('1');
const mockActiveVersion: WorkflowHistory = {
versionId: 'test-version-id',
@ -438,7 +438,7 @@ describe('useWorkflowsStore', () => {
const workflowsListStore = useWorkflowsListStore();
workflowsListStore.activeWorkflows = ['1'];
workflowsListStore.workflowsById = { '1': { active: true } as IWorkflowDb };
workflowsStore.workflow.id = '1';
workflowsStore.setWorkflowId('1');
const mockActiveVersion: WorkflowHistory = {
versionId: 'test-version-id',
@ -459,7 +459,7 @@ describe('useWorkflowsStore', () => {
it('should not clear dirty state when targeting a different workflow', () => {
const workflowsListStore = useWorkflowsListStore();
uiStore.markStateDirty();
workflowsStore.workflow.id = '1';
workflowsStore.setWorkflowId('1');
workflowsListStore.workflowsById = { '1': { active: false } as IWorkflowDb };
const mockActiveVersion: WorkflowHistory = {
@ -501,7 +501,7 @@ describe('useWorkflowsStore', () => {
const workflowsListStore = useWorkflowsListStore();
workflowsListStore.activeWorkflows = ['1'];
workflowsListStore.workflowsById = { '1': { active: true } as IWorkflowDb };
workflowsStore.workflow.id = '1';
workflowsStore.setWorkflowId('1');
workflowsStore.setWorkflowInactive('1');
expect(workflowsListStore.workflowsById['1'].active).toBe(false);
expect(workflowsListStore.activeWorkflows).toEqual([]);
@ -639,7 +639,9 @@ describe('useWorkflowsStore', () => {
it('should add node success run data', () => {
useWorkflowState().setWorkflowExecutionData(executionResponse);
workflowsStore.workflow.nodes.push(
workflowsStore.setWorkflowId('test-wf');
const workflowDocumentStore = useWorkflowDocumentStore(createWorkflowDocumentId('test-wf'));
workflowDocumentStore.addNode(
mock<INodeUi>({
name: successEvent.nodeName,
type: 'n8n-nodes-base.manualTrigger',
@ -664,10 +666,13 @@ describe('useWorkflowsStore', () => {
});
it('should add node error event and track errored executions', async () => {
workflowsStore.workflow.id = 'test-workflow';
workflowsStore.workflow.pinData = {};
workflowsStore.setWorkflowId('test-workflow');
const workflowDocumentStore = useWorkflowDocumentStore(
createWorkflowDocumentId('test-workflow'),
);
workflowDocumentStore.setPinData({});
useWorkflowState().setWorkflowExecutionData(executionResponse);
workflowsStore.workflow.nodes.push({
workflowDocumentStore.addNode({
parameters: {},
id: '554c7ff4-7ee2-407c-8931-e34234c5056a',
name: 'Edit Fields',
@ -736,7 +741,9 @@ describe('useWorkflowsStore', () => {
});
useWorkflowState().setWorkflowExecutionData(runWithExistingRunData);
workflowsStore.workflow.nodes.push(
workflowsStore.setWorkflowId('test-wf');
const workflowDocumentStore = useWorkflowDocumentStore(createWorkflowDocumentId('test-wf'));
workflowDocumentStore.addNode(
mock<INodeUi>({
name: successEvent.nodeName,
type: 'n8n-nodes-base.manualTrigger',
@ -797,7 +804,9 @@ describe('useWorkflowsStore', () => {
});
useWorkflowState().setWorkflowExecutionData(runWithExistingRunData);
workflowsStore.workflow.nodes.push(
workflowsStore.setWorkflowId('test-wf');
const workflowDocumentStore = useWorkflowDocumentStore(createWorkflowDocumentId('test-wf'));
workflowDocumentStore.addNode(
mock<INodeUi>({
name: successEvent.nodeName,
type: 'n8n-nodes-base.manualTrigger',
@ -835,7 +844,9 @@ describe('useWorkflowsStore', () => {
] as Array<[string[], string, string]>)(
'with input %s , %s returns %s',
(ids, id, expected) => {
workflowsStore.workflow.nodes = ids.map((x) => ({ id: x }) as never);
workflowsStore.setWorkflowId('test-wf');
const workflowDocumentStore = useWorkflowDocumentStore(createWorkflowDocumentId('test-wf'));
workflowDocumentStore.setNodes(ids.map((x) => ({ id: x }) as never));
expect(workflowsStore.getPartialIdForNode(id)).toBe(expected);
},
@ -852,11 +863,13 @@ describe('useWorkflowsStore', () => {
workflowsListStore.workflowsById = {
'1': { active: true, isArchived: false, versionId } as IWorkflowDb,
};
workflowsStore.workflow.active = true;
workflowsStore.workflow.id = workflowId;
workflowsStore.workflow.versionId = versionId;
workflowsStore.setWorkflowId(workflowId);
const workflowDocumentStore = useWorkflowDocumentStore(createWorkflowDocumentId(workflowId));
workflowDocumentStore.setActiveState({
activeVersionId: 'active-version',
activeVersion: null,
});
workflowDocumentStore.setVersionData({ versionId, name: null, description: null });
workflowDocumentStore.setIsArchived(false);
const makeRestApiRequestSpy = vi
@ -894,10 +907,9 @@ describe('useWorkflowsStore', () => {
workflowsListStore.workflowsById = {
'1': { active: true, isArchived: false, versionId } as IWorkflowDb,
};
workflowsStore.workflow.id = workflowId;
workflowsStore.workflow.versionId = versionId;
workflowsStore.setWorkflowId(workflowId);
const workflowDocumentStore = useWorkflowDocumentStore(createWorkflowDocumentId(workflowId));
workflowDocumentStore.setVersionData({ versionId, name: null, description: null });
workflowDocumentStore.setIsArchived(false);
const makeRestApiRequestSpy = vi
@ -931,11 +943,10 @@ describe('useWorkflowsStore', () => {
workflowsListStore.workflowsById = {
'1': { active: false, isArchived: true, versionId } as IWorkflowDb,
};
workflowsStore.workflow.active = false;
workflowsStore.workflow.id = workflowId;
workflowsStore.workflow.versionId = versionId;
workflowsStore.setWorkflowId(workflowId);
const workflowDocumentStore = useWorkflowDocumentStore(createWorkflowDocumentId(workflowId));
workflowDocumentStore.setActiveState({ activeVersionId: null, activeVersion: null });
workflowDocumentStore.setVersionData({ versionId, name: null, description: null });
workflowDocumentStore.setIsArchived(true);
const makeRestApiRequestSpy = vi
@ -969,14 +980,7 @@ describe('useWorkflowsStore', () => {
});
it('updates current workflow setting and store state', async () => {
workflowsStore.workflow.id = 'w1';
workflowsStore.workflow.versionId = 'v1';
workflowsStore.workflow.settings = {
executionOrder: 'v1',
timezone: 'UTC',
};
// Also populate the document store since updateWorkflowSetting reads from it
workflowsStore.setWorkflowId('w1');
const workflowDocumentStore = useWorkflowDocumentStore(createWorkflowDocumentId('w1'));
workflowDocumentStore.setVersionData({ versionId: 'v1', name: null, description: null });
workflowDocumentStore.setSettings({ executionOrder: 'v1', timezone: 'UTC' });
@ -1011,7 +1015,7 @@ describe('useWorkflowsStore', () => {
// Assert returned value and store updates
expect(result.versionId).toBe('v1');
expect(workflowsStore.workflow.versionId).toBe('v1');
expect(workflowDocumentStore.versionId).toBe('v1');
expect(workflowDocumentStore.settings).toEqual({
executionOrder: 'v1',
binaryMode: 'separate',
@ -1195,10 +1199,10 @@ describe('useWorkflowsStore', () => {
},
} as unknown as IExecutionResponse);
workflowsStore.workflow.id = 'test-workflow-id';
workflowsStore.setWorkflowId('test-workflow-id');
const workflowDocumentStore = useWorkflowDocumentStore(
createWorkflowDocumentId(workflowsStore.workflow.id),
createWorkflowDocumentId(workflowsStore.workflowId),
);
workflowDocumentStore.addNode({
parameters: {},
@ -1312,7 +1316,7 @@ describe('useWorkflowsStore', () => {
scopes: ['workflow:update'],
});
workflowsStore.workflow = testWorkflow;
workflowsStore.setWorkflowId(workflowId);
// Add workflow to workflowsById to simulate it being loaded from backend
workflowsListStore.addWorkflow(testWorkflow);
@ -1320,8 +1324,8 @@ describe('useWorkflowsStore', () => {
workflowDocumentStore.setScopes(testWorkflow.scopes ?? []);
// Verify the mock is set up correctly
expect(workflowsStore.workflow.scopes).toContain('workflow:update');
expect(workflowsStore.workflow.id).toBe('workflow-123');
expect(workflowDocumentStore.scopes).toContain('workflow:update');
expect(workflowsStore.workflowId).toBe('workflow-123');
expect(workflowDocumentStore.isArchived).toBe(false);
vi.mocked(workflowsApi).getLastSuccessfulExecution.mockResolvedValue(mockExecution);
@ -1344,7 +1348,7 @@ describe('useWorkflowsStore', () => {
scopes: ['workflow:update'],
});
workflowsStore.workflow = testWorkflow;
workflowsStore.setWorkflowId(testWorkflow.id);
// Add workflow to workflowsById to simulate it being loaded from backend
workflowsListStore.addWorkflow(testWorkflow);
@ -1373,7 +1377,7 @@ describe('useWorkflowsStore', () => {
scopes: ['workflow:update'],
});
workflowsStore.workflow = testWorkflow;
workflowsStore.setWorkflowId(testWorkflow.id);
// Add workflow to workflowsById to simulate it being loaded from backend
workflowsListStore.addWorkflow(testWorkflow);
@ -1398,21 +1402,24 @@ describe('useWorkflowsStore', () => {
});
it('should not fetch when workflow is placeholder empty workflow', async () => {
workflowsStore.workflow = createTestWorkflow({
id: '',
scopes: ['workflow:update'],
});
// workflowId defaults to '' which represents an empty placeholder workflow
await workflowsStore.fetchLastSuccessfulExecution();
expect(workflowsApi.getLastSuccessfulExecution).not.toHaveBeenCalled();
});
it('should not fetch when workflow is read-only', async () => {
workflowsStore.workflow = createTestWorkflow({
const testWorkflow = createTestWorkflow({
id: 'workflow-123',
scopes: ['workflow:update'],
});
workflowsStore.setWorkflowId(testWorkflow.id);
workflowsListStore.addWorkflow(testWorkflow);
const workflowDocumentStore = useWorkflowDocumentStore(
createWorkflowDocumentId('workflow-123'),
);
workflowDocumentStore.setScopes(testWorkflow.scopes ?? []);
// Set currentView to a read-only view (not WORKFLOW, NEW_WORKFLOW, or EXECUTION_DEBUG)
uiStore.currentView = 'execution';
@ -1423,13 +1430,15 @@ describe('useWorkflowsStore', () => {
it('should not fetch when workflow is archived', async () => {
const workflowId = 'workflow-123';
workflowsStore.workflow = createTestWorkflow({
const testWorkflow = createTestWorkflow({
id: workflowId,
scopes: ['workflow:update'],
});
workflowsListStore.addWorkflow(workflowsStore.workflow);
workflowsStore.setWorkflowId(workflowId);
workflowsListStore.addWorkflow(testWorkflow);
const workflowDocumentStore = useWorkflowDocumentStore(createWorkflowDocumentId(workflowId));
workflowDocumentStore.setScopes(testWorkflow.scopes ?? []);
workflowDocumentStore.setIsArchived(true);
await workflowsStore.fetchLastSuccessfulExecution();
@ -1438,10 +1447,17 @@ describe('useWorkflowsStore', () => {
});
it('should not fetch when user does not have update permissions', async () => {
workflowsStore.workflow = createTestWorkflow({
const testWorkflow = createTestWorkflow({
id: 'workflow-123',
scopes: ['workflow:read'],
});
workflowsStore.setWorkflowId(testWorkflow.id);
workflowsListStore.addWorkflow(testWorkflow);
const workflowDocumentStore = useWorkflowDocumentStore(
createWorkflowDocumentId('workflow-123'),
);
workflowDocumentStore.setScopes(testWorkflow.scopes ?? []);
await workflowsStore.fetchLastSuccessfulExecution();
@ -1459,11 +1475,19 @@ describe('useWorkflowsStore', () => {
// Create a fresh Pinia instance and reinitialize the workflows store to pick up the new mock
setActivePinia(createPinia());
workflowsStore = useWorkflowsStore();
workflowsListStore = useWorkflowsListStore();
workflowsStore.workflow = createTestWorkflow({
const testWorkflow = createTestWorkflow({
id: 'workflow-123',
scopes: ['workflow:update'],
});
workflowsStore.setWorkflowId(testWorkflow.id);
workflowsListStore.addWorkflow(testWorkflow);
const workflowDocumentStore = useWorkflowDocumentStore(
createWorkflowDocumentId('workflow-123'),
);
workflowDocumentStore.setScopes(testWorkflow.scopes ?? []);
await workflowsStore.fetchLastSuccessfulExecution();
@ -1482,7 +1506,9 @@ describe('useWorkflowsStore', () => {
const signedFormUrl = 'http://localhost:5678/form-waiting/exec-123?signature=abc123';
// Setup workflow with a node
workflowsStore.workflow.nodes = [createTestNode({ name: nodeName, type: WAIT_NODE_TYPE })];
workflowsStore.setWorkflowId('test-wf');
const workflowDocumentStore = useWorkflowDocumentStore(createWorkflowDocumentId('test-wf'));
workflowDocumentStore.setNodes([createTestNode({ name: nodeName, type: WAIT_NODE_TYPE })]);
// Initialize execution data directly
workflowsStore.setWorkflowExecutionData({
@ -1523,7 +1549,9 @@ describe('useWorkflowsStore', () => {
const executionId = 'exec-456';
// Setup workflow with a node
workflowsStore.workflow.nodes = [createTestNode({ name: nodeName, type: WAIT_NODE_TYPE })];
workflowsStore.setWorkflowId('test-wf');
const wfDocStore = useWorkflowDocumentStore(createWorkflowDocumentId('test-wf'));
wfDocStore.setNodes([createTestNode({ name: nodeName, type: WAIT_NODE_TYPE })]);
// Initialize execution data directly
workflowsStore.setWorkflowExecutionData({

View File

@ -54,27 +54,8 @@ import {
useWorkflowDocumentStore,
createWorkflowDocumentId,
} from '@/app/stores/workflowDocument.store';
import { DEFAULT_SETTINGS } from '@/app/stores/workflowDocument/useWorkflowDocumentSettings';
import { getPairedItemsMapping } from '@/app/utils/pairedItemUtils';
const createEmptyWorkflow = (): IWorkflowDb => ({
id: '',
name: '',
description: '',
active: false,
activeVersionId: null,
isArchived: false,
createdAt: -1,
updatedAt: -1,
connections: {},
nodes: [],
settings: { ...DEFAULT_SETTINGS },
tags: [],
pinData: {},
versionId: '',
usedCredentials: [],
});
export const useWorkflowsStore = defineStore(STORES.WORKFLOWS, () => {
const uiStore = useUIStore();
const telemetry = useTelemetry();
@ -85,8 +66,6 @@ export const useWorkflowsStore = defineStore(STORES.WORKFLOWS, () => {
const sourceControlStore = useSourceControlStore();
const workflowsListStore = useWorkflowsListStore();
const workflow = ref<IWorkflowDb>(createEmptyWorkflow());
const currentWorkflowExecutions = ref<ExecutionSummary[]>([]);
const workflowExecutionData = ref<IExecutionResponse | null>(null);
const lastSuccessfulExecution = ref<IExecutionResponse | null>(null);
@ -100,17 +79,17 @@ export const useWorkflowsStore = defineStore(STORES.WORKFLOWS, () => {
const chatPartialExecutionDestinationNode = ref<string | null>(null);
const selectedTriggerNodeName = ref<string>();
const workflowId = computed(() => workflow.value.id);
const workflowId = ref('');
// A workflow is new if it hasn't been saved to the backend yet.
// TODO: move to workflowDocumentStore after `workflow` ref is removed from this store.
// When moved, preserve the `workflowsListStore.getWorkflowById` coupling — pure
// `workflowId === ''` semantics regress the imported-workflow-with-stale-ID case.
const isNewWorkflow = computed(() => {
if (!workflow.value.id) return true;
if (!workflowId.value) return true;
// Check if the workflow exists in workflowsById
const existingWorkflow = workflowsListStore.getWorkflowById(workflow.value.id);
const existingWorkflow = workflowsListStore.getWorkflowById(workflowId.value);
// If workflow doesn't exist in the store or has no ID, it's new
return !existingWorkflow?.id;
});
@ -196,7 +175,11 @@ export const useWorkflowsStore = defineStore(STORES.WORKFLOWS, () => {
function getPartialIdForNode(fullId: string): string {
for (let length = 6; length < fullId.length; ++length) {
const partialId = fullId.slice(0, length);
if (workflow.value.nodes.filter((x) => x.id.startsWith(partialId)).length === 1) {
if (
useWorkflowDocumentStore(createWorkflowDocumentId(workflowId.value)).allNodes.filter((x) =>
x.id.startsWith(partialId),
).length === 1
) {
return partialId;
}
}
@ -242,7 +225,7 @@ export const useWorkflowsStore = defineStore(STORES.WORKFLOWS, () => {
const workflowPermissions = getResourcePermissions(workflowDocumentStore.scopes).workflow;
try {
const wfId = workflow.value.id;
const wfId = workflowId.value;
const workflowDocumentStore = useWorkflowDocumentStore(createWorkflowDocumentId(wfId));
if (
@ -273,12 +256,11 @@ export const useWorkflowsStore = defineStore(STORES.WORKFLOWS, () => {
}
function setWorkflowId(id?: string) {
workflow.value.id = id || '';
workflowId.value = id || '';
}
function resetWorkflow() {
const previousId = workflow.value.id;
workflow.value = createEmptyWorkflow();
const previousId = workflowId.value;
if (previousId) {
const workflowDocumentStore = useWorkflowDocumentStore(createWorkflowDocumentId(previousId));
workflowDocumentStore.reset();
@ -286,19 +268,17 @@ export const useWorkflowsStore = defineStore(STORES.WORKFLOWS, () => {
}
function setWorkflowActiveVersion(version: WorkflowHistory | null) {
workflow.value.activeVersion = deepCopy(version);
const wfId = workflow.value.id;
if (wfId) {
const workflowDocumentStore = useWorkflowDocumentStore(createWorkflowDocumentId(wfId));
workflowDocumentStore.setActiveVersion(deepCopy(version));
}
const workflowDocumentStore = useWorkflowDocumentStore(
createWorkflowDocumentId(workflowId.value),
);
workflowDocumentStore.setActiveVersion(deepCopy(version));
}
async function archiveWorkflow(id: string, expectedChecksum?: string) {
const updatedWorkflow = await workflowsListStore.archiveWorkflowInList(id, expectedChecksum);
setWorkflowInactive(id);
if (id === workflow.value.id) {
if (id === workflowId.value) {
const workflowDocumentStore = useWorkflowDocumentStore(createWorkflowDocumentId(id));
workflowDocumentStore.setVersionData({
versionId: updatedWorkflow.versionId,
@ -313,7 +293,7 @@ export const useWorkflowsStore = defineStore(STORES.WORKFLOWS, () => {
async function unarchiveWorkflow(id: string) {
const updatedWorkflow = await workflowsListStore.unarchiveWorkflowInList(id);
if (id === workflow.value.id) {
if (id === workflowId.value) {
const workflowDocumentStore = useWorkflowDocumentStore(createWorkflowDocumentId(id));
workflowDocumentStore.setVersionData({
versionId: updatedWorkflow.versionId,
@ -332,7 +312,7 @@ export const useWorkflowsStore = defineStore(STORES.WORKFLOWS, () => {
) {
workflowsListStore.setWorkflowActiveInCache(targetWorkflowId, activeVersion);
if (targetWorkflowId === workflow.value.id && clearDirtyState) {
if (targetWorkflowId === workflowId.value && clearDirtyState) {
uiStore.markStateClean();
}
}
@ -702,7 +682,7 @@ export const useWorkflowsStore = defineStore(STORES.WORKFLOWS, () => {
throw new Error('Failed to update workflow');
}
if (id === workflow.value.id) {
if (id === workflowId.value) {
const workflowDocumentStore = useWorkflowDocumentStore(createWorkflowDocumentId(id));
workflowDocumentStore.setVersionData({
versionId: updatedWorkflow.versionId,
@ -752,7 +732,7 @@ export const useWorkflowsStore = defineStore(STORES.WORKFLOWS, () => {
setWorkflowInactive(id);
if (id === workflow.value.id) {
if (id === workflowId.value) {
const workflowDocumentStore = useWorkflowDocumentStore(createWorkflowDocumentId(id));
workflowDocumentStore.setVersionData({
versionId: updatedWorkflow.versionId,
@ -775,7 +755,7 @@ export const useWorkflowsStore = defineStore(STORES.WORKFLOWS, () => {
let currentSettings: IWorkflowSettings = {} as IWorkflowSettings;
let currentVersionId = '';
let currentChecksum = '';
const isCurrentWorkflow = id === workflow.value.id;
const isCurrentWorkflow = id === workflowId.value;
const workflowDocumentStore = useWorkflowDocumentStore(createWorkflowDocumentId(id));
if (isCurrentWorkflow) {
@ -899,10 +879,6 @@ export const useWorkflowsStore = defineStore(STORES.WORKFLOWS, () => {
}
return {
/**
* @deprecated use granular methods or getSnapshot() in workflow document store.
*/
workflow,
currentWorkflowExecutions,
workflowExecutionData,
workflowExecutionPairedItemMappings,

View File

@ -30,10 +30,12 @@ vi.mock('vue-router', () => ({
describe('NodeView', () => {
let workflowsStore: ReturnType<typeof useWorkflowsStore>;
let workflowDocumentStore: ReturnType<typeof useWorkflowDocumentStore>;
beforeEach(() => {
setActivePinia(createPinia());
workflowsStore = useWorkflowsStore();
workflowDocumentStore = useWorkflowDocumentStore(createWorkflowDocumentId('w0'));
});
describe('Trigger node selection', () => {
@ -42,7 +44,7 @@ describe('NodeView', () => {
const n2 = createTestNode({ type: MANUAL_TRIGGER_NODE_TYPE, name: 'n2' });
beforeEach(() => {
workflowsStore.workflow.nodes = [n0, n1];
workflowDocumentStore.setNodes([n0, n1]);
const nodeTypesStore = useNodeTypesStore();
nodeTypesStore.setNodeTypes([
@ -55,13 +57,13 @@ describe('NodeView', () => {
function renderNodeView() {
const workflowDocStore = useWorkflowDocumentStore(
createWorkflowDocumentId(workflowsStore.workflowId),
createWorkflowDocumentId(workflowDocumentStore.workflowId),
);
return renderComponent(NodeView, {
global: {
provide: {
[WorkflowIdKey as symbol]: computed(() => workflowsStore.workflowId),
[WorkflowIdKey as symbol]: computed(() => workflowDocumentStore.workflowId),
[WorkflowDocumentStoreKey as symbol]: shallowRef(workflowDocStore),
},
stubs: {
@ -74,16 +76,20 @@ describe('NodeView', () => {
it('should select newly added trigger node automatically', async () => {
renderNodeView();
await waitFor(() => expect(workflowsStore.selectedTriggerNodeName).toBe('n0'));
workflowsStore.workflow.nodes.push(n2);
workflowDocumentStore.addNode(n2);
await waitFor(() => expect(workflowsStore.selectedTriggerNodeName).toBe('n2'));
});
it('should re-select a trigger when selected trigger gets disabled or removed', async () => {
renderNodeView();
await waitFor(() => expect(workflowsStore.selectedTriggerNodeName).toBe('n0'));
useWorkflowDocumentStore(createWorkflowDocumentId(workflowsStore.workflowId)).removeNode(n0);
useWorkflowDocumentStore(
createWorkflowDocumentId(workflowDocumentStore.workflowId),
).removeNode(n0);
await waitFor(() => expect(workflowsStore.selectedTriggerNodeName).toBe('n1'));
useWorkflowDocumentStore(createWorkflowDocumentId(workflowsStore.workflowId)).setNodeValue({
useWorkflowDocumentStore(
createWorkflowDocumentId(workflowDocumentStore.workflowId),
).setNodeValue({
name: 'n1',
key: 'disabled',
value: true,

View File

@ -456,10 +456,9 @@ describe('AI Assistant store', () => {
it('should call telemetry for opening assistant with build_with_ai source on empty canvas', () => {
const assistantStore = useAssistantStore();
const workflowsStore = useWorkflowsStore();
// Ensure canvas is empty
workflowsStore.workflow.nodes = [];
mockWorkflowDocumentStore.allNodes = [];
assistantStore.trackUserOpenedAssistant({
task: 'placeholder',
@ -485,7 +484,7 @@ describe('AI Assistant store', () => {
const workflowsStore = useWorkflowsStore();
// Set workflow id so workflowDocumentStore is created
workflowsStore.workflow.id = 'test-wf';
workflowsStore.workflowId = 'test-wf';
// Add a node to the workflow
mockWorkflowDocumentStore.allNodes = [

View File

@ -31,6 +31,7 @@ import {
import type { Telemetry } from '@/app/plugins/telemetry';
import type { ChatUI } from '@n8n/design-system/types/assistant';
import type { ChatRequest } from '@/features/ai/assistant/assistant.types';
import type { INodeUi } from '@/Interface';
import { mockedStore } from '@/__tests__/utils';
import { useWorkflowsStore } from '@/app/stores/workflows.store';
import { useNodeTypesStore } from '@/app/stores/nodeTypes.store';
@ -117,6 +118,7 @@ let posthogStore: ReturnType<typeof usePostHog>;
let workflowsStore: ReturnType<typeof mockedStore<typeof useWorkflowsStore>>;
let nodeTypesStore: ReturnType<typeof mockedStore<typeof useNodeTypesStore>>;
let credentialsStore: ReturnType<typeof mockedStore<typeof useCredentialsStore>>;
let workflowDocumentStore: ReturnType<typeof useWorkflowDocumentStore>;
let pinia: ReturnType<typeof createTestingPinia>;
let getNodeTypeSpy: Mock;
@ -172,10 +174,12 @@ describe('AI Builder store', () => {
nodeTypesStore = mockedStore(useNodeTypesStore);
credentialsStore = mockedStore(useCredentialsStore);
workflowsStore.workflowId = 'test-workflow-id';
workflowsStore.workflow.nodes = [];
workflowsStore.workflow.connections = {};
workflowsStore.nodesByName = {};
workflowsStore.setWorkflowId('test-workflow-id');
workflowDocumentStore = useWorkflowDocumentStore(
createWorkflowDocumentId(workflowsStore.workflowId),
);
workflowDocumentStore.setNodes([]);
workflowDocumentStore.setConnections({});
workflowsStore.setWorkflowExecutionData(null);
workflowState = useWorkflowState();
@ -1582,23 +1586,23 @@ describe('AI Builder store', () => {
describe('workflowTodos', () => {
it('returns empty array when no validation issues exist', () => {
workflowsStore.workflow.nodes = [];
workflowDocumentStore.setNodes([]);
const builderStore = useBuilderStore();
expect(builderStore.workflowTodos).toEqual([]);
});
it('includes credential validation issues', () => {
workflowsStore.workflow.nodes = [
workflowDocumentStore.setNodes([
{
...createTestNode({ name: 'HTTP Request' }),
issues: { credentials: { value: ['Missing credentials'] } },
},
createTestNode({ name: 'Issue Target' }),
];
workflowsStore.workflow.connections = {
]);
workflowDocumentStore.setConnections({
'HTTP Request': { main: [[{ node: 'Issue Target', type: 'main', index: 0 }]] },
};
});
const builderStore = useBuilderStore();
expect(builderStore.workflowTodos).toContainEqual(
@ -1607,7 +1611,7 @@ describe('AI Builder store', () => {
});
it('includes placeholder issues from node parameters', () => {
workflowsStore.workflow.nodes = [
workflowDocumentStore.setNodes([
{
id: 'node-1',
name: 'HTTP Request',
@ -1618,7 +1622,7 @@ describe('AI Builder store', () => {
url: '<__PLACEHOLDER_VALUE__Enter URL__>',
},
},
];
]);
const builderStore = useBuilderStore();
expect(builderStore.workflowTodos).toContainEqual(
@ -1627,7 +1631,7 @@ describe('AI Builder store', () => {
});
it('combines credential and placeholder issues', () => {
workflowsStore.workflow.nodes = [
workflowDocumentStore.setNodes([
{
id: 'node-1',
name: 'HTTP Request',
@ -1642,10 +1646,10 @@ describe('AI Builder store', () => {
},
},
createTestNode({ id: 'issue-target-node', name: 'Issue Target' }),
];
workflowsStore.workflow.connections = {
]);
workflowDocumentStore.setConnections({
'HTTP Request': { main: [[{ node: 'Issue Target', type: 'main', index: 0 }]] },
};
});
const builderStore = useBuilderStore();
expect(builderStore.workflowTodos.length).toBeGreaterThanOrEqual(2);
@ -1660,7 +1664,7 @@ describe('AI Builder store', () => {
describe('placeholderIssues', () => {
it('returns empty array when nodes have no parameters', () => {
workflowsStore.workflow.nodes = [
workflowDocumentStore.setNodes([
{
id: 'node-1',
name: 'Start',
@ -1669,29 +1673,29 @@ describe('AI Builder store', () => {
position: [0, 0],
parameters: {},
},
];
]);
const builderStore = useBuilderStore();
expect(builderStore.workflowTodos).toEqual([]);
});
it('returns empty array when node has undefined parameters', () => {
workflowsStore.workflow.nodes = [
workflowDocumentStore.setNodes([
{
id: 'node-1',
name: 'Start',
type: 'n8n-nodes-base.manualTrigger',
typeVersion: 1,
position: [0, 0],
} as Parameters<typeof workflowsStore.workflow.nodes.push>[0],
];
} as INodeUi,
]);
const builderStore = useBuilderStore();
expect(builderStore.workflowTodos).toEqual([]);
});
it('detects placeholders in nested object parameters', () => {
workflowsStore.workflow.nodes = [
workflowDocumentStore.setNodes([
{
id: 'node-1',
name: 'HTTP Request',
@ -1706,7 +1710,7 @@ describe('AI Builder store', () => {
},
},
},
];
]);
const builderStore = useBuilderStore();
const placeholderIssues = builderStore.workflowTodos.filter((t) => t.type === 'parameters');
@ -1718,7 +1722,7 @@ describe('AI Builder store', () => {
});
it('detects placeholders in array parameters', () => {
workflowsStore.workflow.nodes = [
workflowDocumentStore.setNodes([
{
id: 'node-1',
name: 'HTTP Request',
@ -1732,7 +1736,7 @@ describe('AI Builder store', () => {
],
},
},
];
]);
const builderStore = useBuilderStore();
const placeholderIssues = builderStore.workflowTodos.filter((t) => t.type === 'parameters');
@ -1744,7 +1748,7 @@ describe('AI Builder store', () => {
});
it('detects multiple placeholders in the same node', () => {
workflowsStore.workflow.nodes = [
workflowDocumentStore.setNodes([
{
id: 'node-1',
name: 'HTTP Request',
@ -1756,7 +1760,7 @@ describe('AI Builder store', () => {
body: '<__PLACEHOLDER_VALUE__Enter Body__>',
},
},
];
]);
const builderStore = useBuilderStore();
const placeholderIssues = builderStore.workflowTodos.filter((t) => t.type === 'parameters');
@ -1764,7 +1768,7 @@ describe('AI Builder store', () => {
});
it('detects placeholders across multiple nodes', () => {
workflowsStore.workflow.nodes = [
workflowDocumentStore.setNodes([
{
id: 'node-1',
name: 'HTTP Request',
@ -1785,7 +1789,7 @@ describe('AI Builder store', () => {
channel: '<__PLACEHOLDER_VALUE__Enter Channel__>',
},
},
];
]);
const builderStore = useBuilderStore();
const placeholderIssues = builderStore.workflowTodos.filter((t) => t.type === 'parameters');
@ -1797,7 +1801,7 @@ describe('AI Builder store', () => {
it('deduplicates identical placeholder issues (same node, path, and label)', () => {
// Simulate a scenario where the same placeholder appears twice
// (which shouldn't happen in practice but tests the deduplication)
workflowsStore.workflow.nodes = [
workflowDocumentStore.setNodes([
{
id: 'node-1',
name: 'HTTP Request',
@ -1808,7 +1812,7 @@ describe('AI Builder store', () => {
url: '<__PLACEHOLDER_VALUE__Enter URL__>',
},
},
];
]);
const builderStore = useBuilderStore();
const placeholderIssues = builderStore.workflowTodos.filter((t) => t.type === 'parameters');
@ -1821,7 +1825,7 @@ describe('AI Builder store', () => {
// The message format from the store uses i18n which is mocked to return the key
const expectedMessage = 'aiAssistant.builder.executeMessage.fillParameter';
workflowsStore.workflow.nodes = [
workflowDocumentStore.setNodes([
{
id: 'node-1',
name: 'HTTP Request',
@ -1837,7 +1841,7 @@ describe('AI Builder store', () => {
},
},
},
];
]);
const builderStore = useBuilderStore();
const placeholderIssues = builderStore.workflowTodos.filter((t) => t.type === 'parameters');
@ -1846,7 +1850,7 @@ describe('AI Builder store', () => {
});
it('does not skip placeholder when existing parameter issue has different message', () => {
workflowsStore.workflow.nodes = [
workflowDocumentStore.setNodes([
{
id: 'node-1',
name: 'HTTP Request',
@ -1862,7 +1866,7 @@ describe('AI Builder store', () => {
},
},
},
];
]);
const builderStore = useBuilderStore();
const placeholderIssues = builderStore.workflowTodos.filter((t) => t.type === 'parameters');
@ -1871,7 +1875,7 @@ describe('AI Builder store', () => {
});
it('ignores non-string parameter values', () => {
workflowsStore.workflow.nodes = [
workflowDocumentStore.setNodes([
{
id: 'node-1',
name: 'HTTP Request',
@ -1884,14 +1888,14 @@ describe('AI Builder store', () => {
config: null,
},
},
];
]);
const builderStore = useBuilderStore();
expect(builderStore.workflowTodos).toEqual([]);
});
it('ignores strings that do not match placeholder format', () => {
workflowsStore.workflow.nodes = [
workflowDocumentStore.setNodes([
{
id: 'node-1',
name: 'HTTP Request',
@ -1905,14 +1909,14 @@ describe('AI Builder store', () => {
wrongPrefix: 'PLACEHOLDER__test__>',
},
},
];
]);
const builderStore = useBuilderStore();
expect(builderStore.workflowTodos).toEqual([]);
});
it('ignores placeholder with empty label', () => {
workflowsStore.workflow.nodes = [
workflowDocumentStore.setNodes([
{
id: 'node-1',
name: 'HTTP Request',
@ -1924,14 +1928,14 @@ describe('AI Builder store', () => {
body: '<__PLACEHOLDER_VALUE__ __>', // whitespace-only label
},
},
];
]);
const builderStore = useBuilderStore();
expect(builderStore.workflowTodos).toEqual([]);
});
it('filters out non-credential and non-parameter validation issues', () => {
workflowsStore.workflow.nodes = [
workflowDocumentStore.setNodes([
{
...createTestNode({ name: 'HTTP Request' }),
issues: {
@ -1941,10 +1945,10 @@ describe('AI Builder store', () => {
},
},
createTestNode({ name: 'Issue Target' }),
];
workflowsStore.workflow.connections = {
]);
workflowDocumentStore.setConnections({
'HTTP Request': { main: [[{ node: 'Issue Target', type: 'main', index: 0 }]] },
};
});
const builderStore = useBuilderStore();
// Should only include credentials and parameters types
@ -2270,21 +2274,21 @@ describe('AI Builder store', () => {
it('should switch to build mode when nodes are added', async () => {
enablePlanModeExperiment();
// Start with nodes so the watcher can observe changes
const docStore = useWorkflowDocumentStore(
const workflowDocumentStore = useWorkflowDocumentStore(
createWorkflowDocumentId(workflowsStore.workflowId),
);
docStore.setNodes([createTestNode({ name: 'Node1' })]);
workflowDocumentStore.setNodes([createTestNode({ name: 'Node1' })]);
const builderStore = useBuilderStore();
await nextTick();
// Clear nodes to trigger plan mode
docStore.setNodes([]);
workflowDocumentStore.setNodes([]);
await nextTick();
expect(builderStore.builderMode).toBe('plan');
// Add nodes back to trigger build mode
docStore.setNodes([createTestNode({ name: 'Node1' })]);
workflowDocumentStore.setNodes([createTestNode({ name: 'Node1' })]);
await nextTick();
expect(builderStore.builderMode).toBe('build');
});
@ -2293,7 +2297,7 @@ describe('AI Builder store', () => {
const builderStore = useBuilderStore();
// Change workflowId to trigger the watcher (nodes stay empty)
workflowsStore.workflowId = 'different-workflow-id';
workflowsStore.setWorkflowId('different-workflow-id');
await nextTick();
expect(builderStore.builderMode).toBe('build');
@ -2302,19 +2306,19 @@ describe('AI Builder store', () => {
it('should not change mode when chat has messages', async () => {
enablePlanModeExperiment();
const builderStore = useBuilderStore();
const docStore = useWorkflowDocumentStore(
const workflowDocumentStore = useWorkflowDocumentStore(
createWorkflowDocumentId(workflowsStore.workflowId),
);
// Add nodes first so we can trigger a change later
docStore.setNodes([createTestNode({ name: 'Node1' })]);
workflowDocumentStore.setNodes([createTestNode({ name: 'Node1' })]);
await nextTick();
// Simulate an active conversation
builderStore.chatMessages = [{ role: 'user', type: 'text', text: 'hello' } as never];
// Remove nodes — would normally switch to plan, but chat has messages
docStore.setNodes([]);
workflowDocumentStore.setNodes([]);
await nextTick();
// Should stay at build because chat has messages
@ -2326,11 +2330,11 @@ describe('AI Builder store', () => {
const builderStore = useBuilderStore();
// Simulate navigating to a new empty workflow
workflowsStore.workflowId = 'new-empty-workflow';
const docStore = useWorkflowDocumentStore(
workflowsStore.setWorkflowId('new-empty-workflow');
const workflowDocumentStore = useWorkflowDocumentStore(
createWorkflowDocumentId(workflowsStore.workflowId),
);
docStore.setNodes([]);
workflowDocumentStore.setNodes([]);
await nextTick();
expect(builderStore.builderMode).toBe('plan');
@ -2339,7 +2343,7 @@ describe('AI Builder store', () => {
it('should not switch to plan mode after restoreToVersion truncates messages', async () => {
enablePlanModeExperiment();
const builderStore = useBuilderStore();
const docStore = useWorkflowDocumentStore(
const workflowDocumentStore = useWorkflowDocumentStore(
createWorkflowDocumentId(workflowsStore.workflowId),
);
@ -2348,14 +2352,14 @@ describe('AI Builder store', () => {
{ role: 'user', type: 'text', text: 'Build me something' } as never,
{ role: 'assistant', type: 'text', text: 'Done' } as never,
];
docStore.setNodes([createTestNode({ name: 'Node1' })]);
workflowDocumentStore.setNodes([createTestNode({ name: 'Node1' })]);
await nextTick();
expect(builderStore.builderMode).toBe('build');
// Simulate what happens during restore: chat messages are truncated to []
// and nodes are cleared. The watcher would normally switch to plan mode.
builderStore.chatMessages = [];
docStore.setNodes([]);
workflowDocumentStore.setNodes([]);
await nextTick();
// The watcher fires and sets plan mode
@ -3319,7 +3323,7 @@ describe('AI Builder store', () => {
it('should not show revertVersion on user message during streaming', async () => {
const builderStore = useBuilderStore();
workflowsStore.workflowId = 'test-workflow-123';
workflowsStore.setWorkflowId('test-workflow-123');
workflowsStore.isNewWorkflow = false;
const workflowDocumentStore = useWorkflowDocumentStore(
createWorkflowDocumentId(workflowsStore.workflowId),
@ -3340,7 +3344,7 @@ describe('AI Builder store', () => {
it('should insert a version card message after streaming when workflow was modified', async () => {
const builderStore = useBuilderStore();
workflowsStore.workflowId = 'test-workflow-123';
workflowsStore.setWorkflowId('test-workflow-123');
workflowsStore.isNewWorkflow = false;
const workflowDocumentStore = useWorkflowDocumentStore(
createWorkflowDocumentId(workflowsStore.workflowId),
@ -3403,7 +3407,7 @@ describe('AI Builder store', () => {
it('should not add revertVersion to user message after streaming when workflow was not modified', async () => {
const builderStore = useBuilderStore();
workflowsStore.workflowId = 'test-workflow-123';
workflowsStore.setWorkflowId('test-workflow-123');
workflowsStore.isNewWorkflow = false;
const workflowDocumentStore = useWorkflowDocumentStore(
createWorkflowDocumentId(workflowsStore.workflowId),
@ -3649,7 +3653,7 @@ describe('AI Builder store', () => {
const triggerSuccessfulStreamingComplete = async () => {
const builderStore = useBuilderStore();
workflowsStore.workflowId = 'test-workflow-123';
workflowsStore.setWorkflowId('test-workflow-123');
const workflowDocumentStore = useWorkflowDocumentStore(
createWorkflowDocumentId('test-workflow-123'),
);
@ -3798,7 +3802,7 @@ describe('AI Builder store', () => {
// Add focused nodes
const { useFocusedNodesStore } = await import('./focusedNodes.store');
const focusedNodesStore = useFocusedNodesStore();
workflowsStore.workflow.nodes = [
workflowDocumentStore.setNodes([
{
id: 'test-node-1',
name: 'HTTP Request',
@ -3807,7 +3811,7 @@ describe('AI Builder store', () => {
position: [0, 0],
parameters: {},
},
];
]);
focusedNodesStore.confirmNodes(['test-node-1'], 'context_menu');
track.mockReset();
@ -3831,7 +3835,7 @@ describe('AI Builder store', () => {
const { useFocusedNodesStore } = await import('./focusedNodes.store');
const focusedNodesStore = useFocusedNodesStore();
workflowsStore.workflow.nodes = [
workflowDocumentStore.setNodes([
{
id: 'test-node-1',
name: 'HTTP Request',
@ -3840,7 +3844,7 @@ describe('AI Builder store', () => {
position: [0, 0],
parameters: {},
},
];
]);
focusedNodesStore.confirmNodes(['test-node-1'], 'context_menu');
track.mockReset();
@ -3876,7 +3880,7 @@ describe('AI Builder store', () => {
const { useFocusedNodesStore } = await import('./focusedNodes.store');
const focusedNodesStore = useFocusedNodesStore();
workflowsStore.workflow.nodes = [
workflowDocumentStore.setNodes([
{
id: 'test-node-1',
name: 'HTTP Request',
@ -3885,7 +3889,7 @@ describe('AI Builder store', () => {
position: [0, 0],
parameters: {},
},
];
]);
focusedNodesStore.confirmNodes(['test-node-1'], 'context_menu');
track.mockReset();
@ -3908,7 +3912,7 @@ describe('AI Builder store', () => {
const { useFocusedNodesStore } = await import('./focusedNodes.store');
const focusedNodesStore = useFocusedNodesStore();
workflowsStore.workflow.nodes = [
workflowDocumentStore.setNodes([
{
id: 'test-node-1',
name: 'HTTP Request',
@ -3917,7 +3921,7 @@ describe('AI Builder store', () => {
position: [0, 0],
parameters: {},
},
];
]);
focusedNodesStore.confirmNodes(['test-node-1'], 'context_menu');
apiSpy.mockImplementationOnce((_ctx, _payload, _onMessage, onDone) => {

View File

@ -153,6 +153,10 @@ import { mockedStore } from '@/__tests__/utils';
import { STORES } from '@n8n/stores';
import { useWorkflowsStore } from '@/app/stores/workflows.store';
import { useWorkflowsListStore } from '@/app/stores/workflowsList.store';
import {
useWorkflowDocumentStore,
createWorkflowDocumentId,
} from '@/app/stores/workflowDocument.store';
import { useHistoryStore } from '@/app/stores/history.store';
import type { INodeUi } from '@/Interface';
import { useUsersStore } from '@/features/settings/users/users.store';
@ -260,6 +264,7 @@ describe('AskAssistantBuild', () => {
let workflowsListStore: ReturnType<typeof mockedStore<typeof useWorkflowsListStore>>;
let historyStore: ReturnType<typeof mockedStore<typeof useHistoryStore>>;
let collaborationStore: ReturnType<typeof mockedStore<typeof useCollaborationStore>>;
let workflowDocumentStore: ReturnType<typeof useWorkflowDocumentStore>;
beforeAll(() => {
Element.prototype.scrollTo = vi.fn(() => {});
@ -275,6 +280,7 @@ describe('AskAssistantBuild', () => {
updateWorkflowMock.mockResolvedValue({ success: true, newNodeIds: [] });
const pinia = createTestingPinia({
stubActions: false,
initialState: {
[STORES.BUILDER]: {
chatMessages: [],
@ -329,6 +335,7 @@ describe('AskAssistantBuild', () => {
historyStore.stopRecordingUndo = vi.fn();
builderStore.trackingSessionId = 'app_session_id';
workflowsStore.workflowId = 'abc123';
workflowDocumentStore = useWorkflowDocumentStore(createWorkflowDocumentId('abc123'));
});
describe('rendering', () => {
@ -487,21 +494,17 @@ describe('AskAssistantBuild', () => {
describe('workflow suggestions visibility', () => {
it('should not show suggestions when workflow has existing nodes', () => {
workflowsStore.$patch({
workflow: {
nodes: [
{
id: 'node1',
name: 'Start',
type: 'n8n-nodes-base.manualTrigger',
position: [0, 0],
typeVersion: 1,
parameters: {},
} as INodeUi,
],
connections: {},
},
});
workflowDocumentStore.setNodes([
{
id: 'node1',
name: 'Start',
type: 'n8n-nodes-base.manualTrigger',
position: [0, 0],
typeVersion: 1,
parameters: {},
} as INodeUi,
]);
workflowDocumentStore.setConnections({});
builderStore.hasMessages = false;
const { container } = renderComponent();
@ -513,9 +516,8 @@ describe('AskAssistantBuild', () => {
});
it('should show suggestions when workflow is empty and has no messages', () => {
workflowsStore.$patch({
workflow: { nodes: [], connections: {} },
});
workflowDocumentStore.setNodes([]);
workflowDocumentStore.setConnections({});
builderStore.hasMessages = false;
const { container } = renderComponent();
@ -529,9 +531,8 @@ describe('AskAssistantBuild', () => {
});
it('should not show suggestions when there are already messages', () => {
workflowsStore.$patch({
workflow: { nodes: [], connections: {} },
});
workflowDocumentStore.setNodes([]);
workflowDocumentStore.setConnections({});
builderStore.hasMessages = true;
const { container } = renderComponent();
@ -546,7 +547,8 @@ describe('AskAssistantBuild', () => {
describe('user message handling', () => {
it('should initialize builder chat when a user sends a message', async () => {
// Mock empty workflow to ensure initialGeneration is true
workflowsStore.$patch({ workflow: { nodes: [], connections: {} } });
workflowDocumentStore.setNodes([]);
workflowDocumentStore.setConnections({});
workflowsListStore.$patch({ workflowsById: { abc123: { id: 'abc123' } } });
const { container } = renderComponent();
@ -567,7 +569,8 @@ describe('AskAssistantBuild', () => {
});
it('should request write access when sending a message', async () => {
workflowsStore.$patch({ workflow: { nodes: [], connections: {} } });
workflowDocumentStore.setNodes([]);
workflowDocumentStore.setConnections({});
workflowsListStore.$patch({ workflowsById: { abc123: { id: 'abc123' } } });
const { container } = renderComponent();
@ -756,7 +759,8 @@ describe('AskAssistantBuild', () => {
describe('initialGeneration flag reset', () => {
it('should reset initialGeneration flag when streaming ends and workflow has nodes', async () => {
// Setup: empty workflow
workflowsStore.$patch({ workflow: { nodes: [], connections: {} } });
workflowDocumentStore.setNodes([]);
workflowDocumentStore.setConnections({});
workflowsListStore.$patch({ workflowsById: { abc123: { id: 'abc123' } } });
renderComponent();
@ -766,21 +770,17 @@ describe('AskAssistantBuild', () => {
await flushPromises();
// Simulate workflow update with nodes
workflowsStore.$patch({
workflow: {
nodes: [
{
id: 'node1',
name: 'Start',
type: 'n8n-nodes-base.manualTrigger',
position: [0, 0],
typeVersion: 1,
parameters: {},
} as INodeUi,
],
connections: {},
},
});
workflowDocumentStore.setNodes([
{
id: 'node1',
name: 'Start',
type: 'n8n-nodes-base.manualTrigger',
position: [0, 0],
typeVersion: 1,
parameters: {},
} as INodeUi,
]);
workflowDocumentStore.setConnections({});
// Verify initialGeneration is true before streaming ends
expect(builderStore.initialGeneration).toBe(true);
@ -795,7 +795,8 @@ describe('AskAssistantBuild', () => {
it('should NOT reset initialGeneration flag when workflow is still empty', async () => {
// Setup: empty workflow
workflowsStore.$patch({ workflow: { nodes: [], connections: {} } });
workflowDocumentStore.setNodes([]);
workflowDocumentStore.setConnections({});
workflowsListStore.$patch({ workflowsById: { abc123: { id: 'abc123' } } });
renderComponent();
@ -833,7 +834,8 @@ describe('AskAssistantBuild', () => {
};
updateWorkflowMock.mockResolvedValue({ success: true, newNodeIds: ['new-node-1'] });
workflowsStore.$patch({ workflow: { nodes: [], connections: {} } });
workflowDocumentStore.setNodes([]);
workflowDocumentStore.setConnections({});
renderComponent();
@ -861,7 +863,8 @@ describe('AskAssistantBuild', () => {
});
it('should NOT emit fitView when streaming ends without new nodes', async () => {
workflowsStore.$patch({ workflow: { nodes: [], connections: {} } });
workflowDocumentStore.setNodes([]);
workflowDocumentStore.setConnections({});
renderComponent();
@ -881,21 +884,17 @@ describe('AskAssistantBuild', () => {
describe('Execute and refine section visibility', () => {
it('should hide ExecuteMessage component when there is an error after workflow update', async () => {
// Setup: workflow with nodes
workflowsStore.$patch({
workflow: {
nodes: [
{
id: 'node1',
name: 'Start',
type: 'n8n-nodes-base.manualTrigger',
position: [0, 0],
typeVersion: 1,
parameters: {},
} as INodeUi,
],
connections: {},
},
});
workflowDocumentStore.setNodes([
{
id: 'node1',
name: 'Start',
type: 'n8n-nodes-base.manualTrigger',
position: [0, 0],
typeVersion: 1,
parameters: {},
} as INodeUi,
]);
workflowDocumentStore.setConnections({});
const { queryByTestId } = renderComponent();
@ -922,21 +921,17 @@ describe('AskAssistantBuild', () => {
it('should show ExecuteMessage component when there is NO error after workflow update', async () => {
// Setup: workflow with nodes
workflowsStore.$patch({
workflow: {
nodes: [
{
id: 'node1',
name: 'Start',
type: 'n8n-nodes-base.manualTrigger',
position: [0, 0],
typeVersion: 1,
parameters: {},
} as INodeUi,
],
connections: {},
},
});
workflowDocumentStore.setNodes([
{
id: 'node1',
name: 'Start',
type: 'n8n-nodes-base.manualTrigger',
position: [0, 0],
typeVersion: 1,
parameters: {},
} as INodeUi,
]);
workflowDocumentStore.setConnections({});
const { queryByTestId } = renderComponent();
@ -963,21 +958,17 @@ describe('AskAssistantBuild', () => {
it('should show ExecuteMessage component when error occurs BEFORE workflow update', async () => {
// Setup: workflow with nodes
workflowsStore.$patch({
workflow: {
nodes: [
{
id: 'node1',
name: 'Start',
type: 'n8n-nodes-base.manualTrigger',
position: [0, 0],
typeVersion: 1,
parameters: {},
} as INodeUi,
],
connections: {},
},
});
workflowDocumentStore.setNodes([
{
id: 'node1',
name: 'Start',
type: 'n8n-nodes-base.manualTrigger',
position: [0, 0],
typeVersion: 1,
parameters: {},
} as INodeUi,
]);
workflowDocumentStore.setConnections({});
const { queryByTestId } = renderComponent();
@ -1005,21 +996,17 @@ describe('AskAssistantBuild', () => {
it('should hide ExecuteMessage component when using update_node_parameters tool followed by error', async () => {
// Setup: workflow with nodes
workflowsStore.$patch({
workflow: {
nodes: [
{
id: 'node1',
name: 'HTTP Request',
type: 'n8n-nodes-base.httpRequest',
position: [0, 0],
typeVersion: 1,
parameters: {},
} as INodeUi,
],
connections: {},
},
});
workflowDocumentStore.setNodes([
{
id: 'node1',
name: 'HTTP Request',
type: 'n8n-nodes-base.httpRequest',
position: [0, 0],
typeVersion: 1,
parameters: {},
} as INodeUi,
]);
workflowDocumentStore.setConnections({});
const { queryByTestId } = renderComponent();
@ -1048,21 +1035,17 @@ describe('AskAssistantBuild', () => {
it('should hide ExecuteMessage component when task is aborted after workflow update', async () => {
// Setup: workflow with nodes
workflowsStore.$patch({
workflow: {
nodes: [
{
id: 'node1',
name: 'Start',
type: 'n8n-nodes-base.manualTrigger',
position: [0, 0],
typeVersion: 1,
parameters: {},
} as INodeUi,
],
connections: {},
},
});
workflowDocumentStore.setNodes([
{
id: 'node1',
name: 'Start',
type: 'n8n-nodes-base.manualTrigger',
position: [0, 0],
typeVersion: 1,
parameters: {},
} as INodeUi,
]);
workflowDocumentStore.setConnections({});
const { queryByTestId } = renderComponent();
@ -1112,7 +1095,8 @@ describe('AskAssistantBuild', () => {
updateWorkflowMock.mockResolvedValue({ success: true, newNodeIds: ['new-node-1'] });
workflowsStore.$patch({ workflow: { nodes: [], connections: {} } });
workflowDocumentStore.setNodes([]);
workflowDocumentStore.setConnections({});
renderComponent();
@ -1180,7 +1164,8 @@ describe('AskAssistantBuild', () => {
// Second update adds node-2
.mockResolvedValueOnce({ success: true, newNodeIds: ['node-2'] });
workflowsStore.$patch({ workflow: { nodes: [], connections: {} } });
workflowDocumentStore.setNodes([]);
workflowDocumentStore.setConnections({});
renderComponent();
@ -1234,7 +1219,8 @@ describe('AskAssistantBuild', () => {
});
it('should reset accumulated node IDs on new user message', async () => {
workflowsStore.$patch({ workflow: { nodes: [], connections: {} } });
workflowDocumentStore.setNodes([]);
workflowDocumentStore.setConnections({});
workflowsListStore.$patch({ workflowsById: { abc123: { id: 'abc123' } } });
const { container } = renderComponent();
@ -1337,7 +1323,8 @@ describe('AskAssistantBuild', () => {
updateWorkflowMock.mockResolvedValue({ success: true, newNodeIds: ['new-node-1'] });
workflowsStore.$patch({ workflow: { nodes: [], connections: {} } });
workflowDocumentStore.setNodes([]);
workflowDocumentStore.setConnections({});
builderStore.initialGeneration = true;
renderComponent();
@ -1418,7 +1405,8 @@ describe('AskAssistantBuild', () => {
const testError = new Error('Failed to update workflow');
updateWorkflowMock.mockResolvedValue({ success: false, error: testError });
workflowsStore.$patch({ workflow: { nodes: [], connections: {} } });
workflowDocumentStore.setNodes([]);
workflowDocumentStore.setConnections({});
renderComponent();
@ -1458,7 +1446,8 @@ describe('AskAssistantBuild', () => {
// Second update fails
.mockResolvedValueOnce({ success: false, error: new Error('Failed') });
workflowsStore.$patch({ workflow: { nodes: [], connections: {} } });
workflowDocumentStore.setNodes([]);
workflowDocumentStore.setConnections({});
renderComponent();
@ -1516,7 +1505,8 @@ describe('AskAssistantBuild', () => {
const testError = new Error('Failed to update workflow');
updateWorkflowMock.mockResolvedValue({ success: false, error: testError });
workflowsStore.$patch({ workflow: { nodes: [], connections: {} } });
workflowDocumentStore.setNodes([]);
workflowDocumentStore.setConnections({});
renderComponent();

View File

@ -8,7 +8,6 @@ import { createTestNode } from '@/__tests__/mocks';
import { mockedStore } from '@/__tests__/utils';
import type { INodeUi } from '@/Interface';
import BuilderSetupWizard from './BuilderSetupWizard.vue';
import { useWorkflowsStore } from '@/app/stores/workflows.store';
import { useBuilderStore } from '../../builder.store';
const mockCards = ref<Array<{ state: Record<string, unknown> }>>([]);
@ -76,7 +75,6 @@ const triggerNode = createTestNode({
const renderComponent = createComponentRenderer(BuilderSetupWizard);
describe('BuilderSetupWizard', () => {
let workflowsStore: ReturnType<typeof mockedStore<typeof useWorkflowsStore>>;
let builderStore: ReturnType<typeof mockedStore<typeof useBuilderStore>>;
let pinia: ReturnType<typeof createTestingPinia>;
@ -92,11 +90,7 @@ describe('BuilderSetupWizard', () => {
pinia = createTestingPinia({ stubActions: false });
setActivePinia(pinia);
workflowsStore = mockedStore(useWorkflowsStore);
builderStore = mockedStore(useBuilderStore);
workflowsStore.workflow.nodes = [triggerNode];
workflowsStore.workflow.connections = {} as never;
Object.defineProperty(builderStore, 'hasTodosHiddenByPinnedData', { get: () => false });
Object.defineProperty(builderStore, 'wizardHasExecutedWorkflow', {
value: false,

View File

@ -11,6 +11,10 @@ import type { INodeUi } from '@/Interface';
import ExecuteMessage from './ExecuteMessage.vue';
import { CHAT_TRIGGER_NODE_TYPE, SETUP_CREDENTIALS_MODAL_KEY } from '@/app/constants';
import { useWorkflowsStore } from '@/app/stores/workflows.store';
import {
useWorkflowDocumentStore,
createWorkflowDocumentId,
} from '@/app/stores/workflowDocument.store';
import { WorkflowIdKey } from '@/app/constants/injectionKeys';
import { useNodeTypesStore } from '@/app/stores/nodeTypes.store';
import { useLogsStore } from '@/app/stores/logs.store';
@ -119,14 +123,17 @@ describe('ExecuteMessage', () => {
setActivePinia(pinia);
workflowsStore = mockedStore(useWorkflowsStore);
workflowsStore.workflow.id = 'test-workflow';
workflowsStore.setWorkflowId('test-workflow');
nodeTypesStore = mockedStore(useNodeTypesStore);
logsStore = mockedStore(useLogsStore);
uiStore = mockedStore(useUIStore);
builderStore = mockedStore(useBuilderStore);
workflowsStore.workflow.nodes = workflowNodes as unknown as INodeUi[];
workflowsStore.workflow.connections = {} as never;
const workflowDocumentStore = useWorkflowDocumentStore(
createWorkflowDocumentId('test-workflow'),
);
workflowDocumentStore.setNodes(workflowNodes);
workflowDocumentStore.setConnections({});
Object.defineProperty(workflowsStore, 'workflowExecutionData', {
get: () => workflowExecutionDataRef,
});

View File

@ -328,23 +328,22 @@ describe('useBuilderTodos', () => {
...overrides,
}) as INodeUi;
function setPinData(pinData: IPinData) {
function getWorkflowDocumentStore() {
const workflowsStore = useWorkflowsStore();
const workflowDocumentStore = useWorkflowDocumentStore(
createWorkflowDocumentId(workflowsStore.workflow.id),
);
workflowDocumentStore.setPinData(pinData);
return useWorkflowDocumentStore(createWorkflowDocumentId(workflowsStore.workflowId));
}
function setPinData(pinData: IPinData) {
getWorkflowDocumentStore().setPinData(pinData);
}
beforeEach(() => {
setActivePinia(createPinia());
const workflowsStore = useWorkflowsStore();
workflowsStore.workflow.id = 'test-workflow';
workflowsStore.setWorkflowId('test-workflow');
});
it('excludes placeholder issues from pinned nodes', () => {
const workflowsStore = useWorkflowsStore();
// Setup a node with placeholder in parameters
const nodeWithPlaceholder = createMockNode({
name: 'HTTP Request',
@ -354,7 +353,7 @@ describe('useBuilderTodos', () => {
});
// Set the workflow with the node and pin data for it
workflowsStore.workflow.nodes = [nodeWithPlaceholder];
getWorkflowDocumentStore().setNodes([nodeWithPlaceholder]);
setPinData({
'HTTP Request': [{ json: { data: 'pinned result' } }],
});
@ -366,8 +365,6 @@ describe('useBuilderTodos', () => {
});
it('includes placeholder issues from non-pinned nodes', () => {
const workflowsStore = useWorkflowsStore();
// Setup a node with placeholder in parameters
const nodeWithPlaceholder = createMockNode({
name: 'HTTP Request',
@ -377,7 +374,7 @@ describe('useBuilderTodos', () => {
});
// Set the workflow with the node but NO pin data
workflowsStore.workflow.nodes = [nodeWithPlaceholder];
getWorkflowDocumentStore().setNodes([nodeWithPlaceholder]);
setPinData({});
const { workflowTodos } = useBuilderTodos();
@ -388,8 +385,6 @@ describe('useBuilderTodos', () => {
});
it('excludes validation issues from pinned nodes', () => {
const workflowsStore = useWorkflowsStore();
// Setup a connected node with credential issues
const nodeWithIssues = createMockNode({
name: 'HTTP Request',
@ -401,12 +396,12 @@ describe('useBuilderTodos', () => {
});
// Set the workflow with connections (node must be connected for issues to count)
workflowsStore.workflow.nodes = [nodeWithIssues];
workflowsStore.workflow.connections = {
getWorkflowDocumentStore().setNodes([nodeWithIssues]);
getWorkflowDocumentStore().setConnections({
'HTTP Request': {
main: [[{ node: 'Other Node', type: 'main' as const, index: 0 }]],
},
};
});
setPinData({
'HTTP Request': [{ json: { data: 'pinned result' } }],
});
@ -439,21 +434,21 @@ describe('useBuilderTodos', () => {
// Connections are stored by SOURCE node. AI Agent connects TO the model node.
// This gives the model node an INCOMING connection.
workflowsStore.workflow.nodes = [aiModelNode, agentNode];
workflowsStore.workflow.connections = {
getWorkflowDocumentStore().setNodes([aiModelNode, agentNode]);
getWorkflowDocumentStore().setConnections({
'AI Agent': {
ai_languageModel: [
[{ node: 'OpenAI GPT-4o-mini', type: 'ai_languageModel' as const, index: 0 }],
],
},
};
});
setPinData({
'OpenAI GPT-4o-mini': [{ json: { response: 'pinned AI response' } }],
});
// Verify the issue exists in nodeValidationIssues before filtering
const workflowDocumentStore = useWorkflowDocumentStore(
createWorkflowDocumentId(workflowsStore.workflow.id),
createWorkflowDocumentId(workflowsStore.workflowId),
);
const validationIssues = workflowDocumentStore.nodeValidationIssues;
expect(validationIssues.some((i) => i.node === 'OpenAI GPT-4o-mini')).toBe(true);
@ -485,16 +480,16 @@ describe('useBuilderTodos', () => {
type: '@n8n/n8n-nodes-langchain.agent',
});
workflowsStore.workflow.nodes = [aiModelSubNode, parentNode];
getWorkflowDocumentStore().setNodes([aiModelSubNode, parentNode]);
// Sub-node outputs TO the parent node (stored by source node)
workflowsStore.workflow.connections = {
getWorkflowDocumentStore().setConnections({
'OpenAI GPT-4.1-mini': {
ai_languageModel: [
[{ node: 'Analyze Emails', type: 'ai_languageModel' as const, index: 0 }],
],
},
};
});
// Parent node has pinned data, but sub-node does NOT
setPinData({
@ -503,7 +498,7 @@ describe('useBuilderTodos', () => {
// Verify validation issue exists for the sub-node
const workflowDocumentStore = useWorkflowDocumentStore(
createWorkflowDocumentId(workflowsStore.workflow.id),
createWorkflowDocumentId(workflowsStore.workflowId),
);
const validationIssues = workflowDocumentStore.nodeValidationIssues;
expect(validationIssues.some((i) => i.node === 'OpenAI GPT-4.1-mini')).toBe(true);
@ -515,8 +510,6 @@ describe('useBuilderTodos', () => {
});
it('excludes credential issues from nested sub-nodes when ancestor has pinned data', () => {
const workflowsStore = useWorkflowsStore();
// Setup: Nested sub-node structure
// grandparentNode (has pinned data) <- parentSubNode <- childSubNode (has credential issues)
const childSubNode = createMockNode({
@ -541,17 +534,17 @@ describe('useBuilderTodos', () => {
type: '@n8n/n8n-nodes-langchain.agent',
});
workflowsStore.workflow.nodes = [childSubNode, parentSubNode, grandparentNode];
getWorkflowDocumentStore().setNodes([childSubNode, parentSubNode, grandparentNode]);
// Child outputs to parent, parent outputs to grandparent
workflowsStore.workflow.connections = {
getWorkflowDocumentStore().setConnections({
'Child Tool': {
ai_tool: [[{ node: 'AI Model', type: 'ai_tool' as const, index: 0 }]],
},
'AI Model': {
ai_languageModel: [[{ node: 'AI Agent', type: 'ai_languageModel' as const, index: 0 }]],
},
};
});
// Only grandparent has pinned data
setPinData({
@ -565,8 +558,6 @@ describe('useBuilderTodos', () => {
});
it('verifies pinData structure is correct for filtering', () => {
const workflowsStore = useWorkflowsStore();
// Setup pinData with various structures to verify filtering works
const nodeWithIssues = createMockNode({
name: 'Test Node',
@ -577,12 +568,12 @@ describe('useBuilderTodos', () => {
},
});
workflowsStore.workflow.nodes = [nodeWithIssues];
workflowsStore.workflow.connections = {
getWorkflowDocumentStore().setNodes([nodeWithIssues]);
getWorkflowDocumentStore().setConnections({
'Test Node': {
main: [[{ node: 'Other', type: 'main' as const, index: 0 }]],
},
};
});
// Verify pinData must have array with length > 0 to be considered pinned
const { workflowTodos } = useBuilderTodos();
@ -601,8 +592,6 @@ describe('useBuilderTodos', () => {
});
it('includes validation issues from non-pinned nodes', () => {
const workflowsStore = useWorkflowsStore();
// Setup a connected node with credential issues
const nodeWithIssues = createMockNode({
name: 'HTTP Request',
@ -614,12 +603,12 @@ describe('useBuilderTodos', () => {
});
// Set the workflow with connections (node must be connected for issues to count)
workflowsStore.workflow.nodes = [nodeWithIssues];
workflowsStore.workflow.connections = {
getWorkflowDocumentStore().setNodes([nodeWithIssues]);
getWorkflowDocumentStore().setConnections({
'HTTP Request': {
main: [[{ node: 'Other Node', type: 'main' as const, index: 0 }]],
},
};
});
setPinData({});
const { workflowTodos } = useBuilderTodos();
@ -630,8 +619,6 @@ describe('useBuilderTodos', () => {
});
it('handles mixed pinned and non-pinned nodes correctly', () => {
const workflowsStore = useWorkflowsStore();
// Setup two nodes: one pinned with issues, one not pinned with issues
const pinnedNode = createMockNode({
name: 'Pinned Node',
@ -647,7 +634,7 @@ describe('useBuilderTodos', () => {
},
});
workflowsStore.workflow.nodes = [pinnedNode, unpinnedNode];
getWorkflowDocumentStore().setNodes([pinnedNode, unpinnedNode]);
setPinData({
'Pinned Node': [{ json: { data: 'pinned result' } }],
// 'Unpinned Node' has no pinned data
@ -661,8 +648,6 @@ describe('useBuilderTodos', () => {
});
it('excludes placeholder issues from disabled nodes', () => {
const workflowsStore = useWorkflowsStore();
// Setup a disabled node with placeholder in parameters
const disabledNode = createMockNode({
name: 'HTTP Request',
@ -672,7 +657,7 @@ describe('useBuilderTodos', () => {
},
});
workflowsStore.workflow.nodes = [disabledNode];
getWorkflowDocumentStore().setNodes([disabledNode]);
setPinData({});
const { workflowTodos } = useBuilderTodos();
@ -682,8 +667,6 @@ describe('useBuilderTodos', () => {
});
it('excludes validation issues from disabled nodes', () => {
const workflowsStore = useWorkflowsStore();
// Setup a disabled node with credential issues
const disabledNode = createMockNode({
name: 'HTTP Request',
@ -695,12 +678,12 @@ describe('useBuilderTodos', () => {
},
});
workflowsStore.workflow.nodes = [disabledNode];
workflowsStore.workflow.connections = {
getWorkflowDocumentStore().setNodes([disabledNode]);
getWorkflowDocumentStore().setConnections({
'HTTP Request': {
main: [[{ node: 'Other Node', type: 'main' as const, index: 0 }]],
},
};
});
setPinData({});
const { workflowTodos } = useBuilderTodos();
@ -731,19 +714,19 @@ describe('useBuilderTodos', () => {
disabled: true,
});
workflowsStore.workflow.nodes = [aiModelSubNode, parentNode];
getWorkflowDocumentStore().setNodes([aiModelSubNode, parentNode]);
// Sub-node outputs TO the parent node (stored by source node)
workflowsStore.workflow.connections = {
getWorkflowDocumentStore().setConnections({
'OpenAI GPT-4.1-mini': {
ai_languageModel: [[{ node: 'AI Agent', type: 'ai_languageModel' as const, index: 0 }]],
},
};
});
setPinData({});
// Verify validation issue exists for the sub-node
const workflowDocumentStore = useWorkflowDocumentStore(
createWorkflowDocumentId(workflowsStore.workflow.id),
createWorkflowDocumentId(workflowsStore.workflowId),
);
const validationIssues = workflowDocumentStore.nodeValidationIssues;
expect(validationIssues.some((i) => i.node === 'OpenAI GPT-4.1-mini')).toBe(true);
@ -755,8 +738,6 @@ describe('useBuilderTodos', () => {
});
it('includes issues from enabled nodes', () => {
const workflowsStore = useWorkflowsStore();
// Setup an enabled node with placeholder in parameters
const enabledNode = createMockNode({
name: 'HTTP Request',
@ -766,7 +747,7 @@ describe('useBuilderTodos', () => {
},
});
workflowsStore.workflow.nodes = [enabledNode];
getWorkflowDocumentStore().setNodes([enabledNode]);
setPinData({});
const { workflowTodos } = useBuilderTodos();
@ -777,8 +758,6 @@ describe('useBuilderTodos', () => {
});
it('handles mixed disabled and enabled nodes correctly', () => {
const workflowsStore = useWorkflowsStore();
// Setup two nodes: one disabled with issues, one enabled with issues
const disabledNode = createMockNode({
name: 'Disabled Node',
@ -796,7 +775,7 @@ describe('useBuilderTodos', () => {
},
});
workflowsStore.workflow.nodes = [disabledNode, enabledNode];
getWorkflowDocumentStore().setNodes([disabledNode, enabledNode]);
setPinData({});
const { workflowTodos } = useBuilderTodos();
@ -808,8 +787,6 @@ describe('useBuilderTodos', () => {
describe('hasTodosHiddenByPinnedData', () => {
it('returns false when there are visible todos', () => {
const workflowsStore = useWorkflowsStore();
// Setup a node with placeholder in parameters (no pinned data)
const nodeWithPlaceholder = createMockNode({
name: 'HTTP Request',
@ -818,7 +795,7 @@ describe('useBuilderTodos', () => {
},
});
workflowsStore.workflow.nodes = [nodeWithPlaceholder];
getWorkflowDocumentStore().setNodes([nodeWithPlaceholder]);
setPinData({});
const { workflowTodos, hasTodosHiddenByPinnedData } = useBuilderTodos();
@ -829,8 +806,6 @@ describe('useBuilderTodos', () => {
});
it('returns false when there are no todos and no pinned data', () => {
const workflowsStore = useWorkflowsStore();
// Setup a node without any issues
const cleanNode = createMockNode({
name: 'HTTP Request',
@ -839,7 +814,7 @@ describe('useBuilderTodos', () => {
},
});
workflowsStore.workflow.nodes = [cleanNode];
getWorkflowDocumentStore().setNodes([cleanNode]);
setPinData({});
const { workflowTodos, hasTodosHiddenByPinnedData } = useBuilderTodos();
@ -849,8 +824,6 @@ describe('useBuilderTodos', () => {
});
it('returns true when placeholder todos are hidden by pinned data', () => {
const workflowsStore = useWorkflowsStore();
// Setup a node with placeholder that would show as todo
const nodeWithPlaceholder = createMockNode({
name: 'HTTP Request',
@ -859,7 +832,7 @@ describe('useBuilderTodos', () => {
},
});
workflowsStore.workflow.nodes = [nodeWithPlaceholder];
getWorkflowDocumentStore().setNodes([nodeWithPlaceholder]);
// Pin data hides the todo
setPinData({
'HTTP Request': [{ json: { data: 'pinned result' } }],
@ -873,8 +846,6 @@ describe('useBuilderTodos', () => {
});
it('returns true when credential todos are hidden by pinned data', () => {
const workflowsStore = useWorkflowsStore();
// Setup a connected node with credential issues
const nodeWithIssues = createMockNode({
name: 'HTTP Request',
@ -885,12 +856,12 @@ describe('useBuilderTodos', () => {
},
});
workflowsStore.workflow.nodes = [nodeWithIssues];
workflowsStore.workflow.connections = {
getWorkflowDocumentStore().setNodes([nodeWithIssues]);
getWorkflowDocumentStore().setConnections({
'HTTP Request': {
main: [[{ node: 'Other Node', type: 'main' as const, index: 0 }]],
},
};
});
// Pin data hides the credential issue
setPinData({
'HTTP Request': [{ json: { data: 'pinned result' } }],
@ -904,8 +875,6 @@ describe('useBuilderTodos', () => {
});
it('returns false when todos are hidden by disabled nodes (not pinned)', () => {
const workflowsStore = useWorkflowsStore();
// Setup a disabled node with placeholder
const disabledNode = createMockNode({
name: 'HTTP Request',
@ -915,7 +884,7 @@ describe('useBuilderTodos', () => {
},
});
workflowsStore.workflow.nodes = [disabledNode];
getWorkflowDocumentStore().setNodes([disabledNode]);
setPinData({});
const { workflowTodos, hasTodosHiddenByPinnedData } = useBuilderTodos();
@ -926,8 +895,6 @@ describe('useBuilderTodos', () => {
});
it('returns true when sub-node todos are hidden by parent pinned data', () => {
const workflowsStore = useWorkflowsStore();
// Setup: AI model sub-node with placeholder
const aiModelSubNode = createMockNode({
name: 'OpenAI GPT-4.1-mini',
@ -943,12 +910,12 @@ describe('useBuilderTodos', () => {
type: '@n8n/n8n-nodes-langchain.agent',
});
workflowsStore.workflow.nodes = [aiModelSubNode, parentNode];
workflowsStore.workflow.connections = {
getWorkflowDocumentStore().setNodes([aiModelSubNode, parentNode]);
getWorkflowDocumentStore().setConnections({
'OpenAI GPT-4.1-mini': {
ai_languageModel: [[{ node: 'AI Agent', type: 'ai_languageModel' as const, index: 0 }]],
},
};
});
// Parent node has pinned data
setPinData({
'AI Agent': [{ json: { response: 'pinned response' } }],
@ -962,8 +929,6 @@ describe('useBuilderTodos', () => {
});
it('returns false when node is both pinned AND disabled', () => {
const workflowsStore = useWorkflowsStore();
// Setup a node that is both pinned and disabled
const pinnedDisabledNode = createMockNode({
name: 'HTTP Request',
@ -973,7 +938,7 @@ describe('useBuilderTodos', () => {
},
});
workflowsStore.workflow.nodes = [pinnedDisabledNode];
getWorkflowDocumentStore().setNodes([pinnedDisabledNode]);
setPinData({
'HTTP Request': [{ json: { data: 'pinned result' } }],
});
@ -986,8 +951,6 @@ describe('useBuilderTodos', () => {
});
it('handles mixed scenarios: some todos visible, some hidden by pin', () => {
const workflowsStore = useWorkflowsStore();
// One node pinned (hiding its todo), another unpinned (showing its todo)
const pinnedNode = createMockNode({
name: 'Pinned Node',
@ -1003,7 +966,7 @@ describe('useBuilderTodos', () => {
},
});
workflowsStore.workflow.nodes = [pinnedNode, unpinnedNode];
getWorkflowDocumentStore().setNodes([pinnedNode, unpinnedNode]);
setPinData({
'Pinned Node': [{ json: { data: 'pinned result' } }],
});
@ -1020,8 +983,6 @@ describe('useBuilderTodos', () => {
describe('reactivity for subnode todos', () => {
it('shows subnode todos when parent node is unpinned', async () => {
const workflowsStore = useWorkflowsStore();
const subnodeWithPlaceholder = createMockNode({
name: 'OpenAI Model',
type: '@n8n/n8n-nodes-langchain.lmChatOpenAi',
@ -1036,12 +997,12 @@ describe('useBuilderTodos', () => {
type: '@n8n/n8n-nodes-langchain.agent',
});
workflowsStore.workflow.nodes = [subnodeWithPlaceholder, parentNode];
workflowsStore.workflow.connections = {
getWorkflowDocumentStore().setNodes([subnodeWithPlaceholder, parentNode]);
getWorkflowDocumentStore().setConnections({
'OpenAI Model': {
ai_languageModel: [[{ node: 'AI Agent', type: 'ai_languageModel' as const, index: 0 }]],
},
};
});
// Initially parent is pinned - no todos expected
setPinData({
@ -1060,8 +1021,6 @@ describe('useBuilderTodos', () => {
});
it('shows subnode todos when parent node is enabled', () => {
const workflowsStore = useWorkflowsStore();
const subnodeWithPlaceholder = createMockNode({
name: 'OpenAI Model',
type: '@n8n/n8n-nodes-langchain.lmChatOpenAi',
@ -1077,12 +1036,12 @@ describe('useBuilderTodos', () => {
disabled: true,
});
workflowsStore.workflow.nodes = [subnodeWithPlaceholder, parentNode];
workflowsStore.workflow.connections = {
getWorkflowDocumentStore().setNodes([subnodeWithPlaceholder, parentNode]);
getWorkflowDocumentStore().setConnections({
'OpenAI Model': {
ai_languageModel: [[{ node: 'AI Agent', type: 'ai_languageModel' as const, index: 0 }]],
},
};
});
setPinData({});
const { workflowTodos } = useBuilderTodos();
@ -1091,11 +1050,10 @@ describe('useBuilderTodos', () => {
expect(workflowTodos.value).toHaveLength(0);
// Enable the parent by updating the node
const parentIndex = workflowsStore.workflow.nodes.findIndex((n) => n.name === 'AI Agent');
workflowsStore.workflow.nodes[parentIndex] = {
...workflowsStore.workflow.nodes[parentIndex],
disabled: false,
};
getWorkflowDocumentStore().updateNodeProperties({
name: 'AI Agent',
properties: { disabled: false },
});
// Should now show the subnode's placeholder todo
expect(workflowTodos.value).toHaveLength(1);

View File

@ -88,8 +88,7 @@ describe('useNodeMention', () => {
workflowsStore = useWorkflowsStore();
focusedNodesStore = useFocusedNodesStore();
// @ts-expect-error -- mock readonly getter
workflowsStore.workflowId = 'test-wf';
workflowsStore.setWorkflowId('test-wf');
// @ts-expect-error -- mock readonly property for focusedNodesStore which still reads workflowsStore.allNodes
workflowsStore.allNodes = mockNodes;
mockWorkflowDocumentStore.allNodes = mockNodes;

View File

@ -75,8 +75,7 @@ describe('useFocusedNodesStore', () => {
);
workflowsStore = mockedStore(useWorkflowsStore);
workflowsStore.workflowId = 'wf-1';
workflowsStore.workflow.connections = {};
workflowsStore.setWorkflowId('wf-1');
const workflowDocumentStore = useWorkflowDocumentStore(
createWorkflowDocumentId(workflowsStore.workflowId),
@ -768,14 +767,17 @@ describe('useFocusedNodesStore', () => {
});
it('should include connections (deduplicated)', () => {
workflowsStore.workflow.connections = {
const workflowDocumentStore = useWorkflowDocumentStore(
createWorkflowDocumentId(workflowsStore.workflowId),
);
workflowDocumentStore.setConnections({
Trigger: {
main: [[{ node: 'HTTP Request', type: 'main', index: 0 }]],
},
'HTTP Request': {
main: [[{ node: 'Code', type: 'main', index: 0 }]],
},
};
});
focusedNodesStore.confirmNodes(['node-1'], 'context_menu');
track.mockReset();
@ -840,7 +842,7 @@ describe('useFocusedNodesStore', () => {
focusedNodesStore.confirmNodes(['node-1'], 'context_menu');
track.mockReset();
workflowsStore.workflowId = 'wf-2';
workflowsStore.setWorkflowId('wf-2');
await nextTick();
expect(focusedNodesStore.focusedNodesMap).toEqual({});
@ -853,7 +855,7 @@ describe('useFocusedNodesStore', () => {
it('should not track telemetry on workflowId change if no confirmed and oldId undefined', async () => {
// The initial wf-1 is set in beforeEach but no confirmed nodes
workflowsStore.workflowId = 'wf-2';
workflowsStore.setWorkflowId('wf-2');
await nextTick();
expect(track).not.toHaveBeenCalled();

View File

@ -14,6 +14,10 @@ import {
useWorkflowDocumentStore,
} from '@/app/stores/workflowDocument.store';
vi.mock('@/app/stores/workflows.store', () => ({
useWorkflowsStore: vi.fn(() => ({ workflowId: '' })),
}));
// ---------------------------------------------------------------------------
// Helpers
// ---------------------------------------------------------------------------

View File

@ -6,6 +6,10 @@ import type { InstanceAiWorkflowSetupNode } from '@n8n/api-types';
import type { INodeTypeDescription } from 'n8n-workflow';
import { useNodeTypesStore } from '@/app/stores/nodeTypes.store';
import { useWorkflowsStore } from '@/app/stores/workflows.store';
import {
useWorkflowDocumentStore,
createWorkflowDocumentId,
} from '@/app/stores/workflowDocument.store';
import { useSetupCards } from '../composables/useSetupCards';
vi.mock('@/features/setupPanel/setupPanel.utils', () => ({
@ -43,7 +47,11 @@ describe('useSetupCards', () => {
const pinia = createTestingPinia({ stubActions: false });
setActivePinia(pinia);
workflowsStore = useWorkflowsStore();
workflowsStore.workflow.nodes = [
workflowsStore.setWorkflowId('test-workflow');
const workflowDocumentStore = useWorkflowDocumentStore(
createWorkflowDocumentId('test-workflow'),
);
workflowDocumentStore.setNodes([
{
name: 'DataTable',
type: 'n8n-nodes-base.dataTable',
@ -52,7 +60,7 @@ describe('useSetupCards', () => {
position: [0, 0] as [number, number],
id: 'node-1',
},
];
]);
});
describe('param-issue card creation', () => {

View File

@ -85,7 +85,7 @@ describe('mcp.store', () => {
});
it('merges settings into the active workflow document when toggling its own id', async () => {
workflowsStore.workflow.id = 'wf-current';
workflowsStore.workflowId = 'wf-current';
vi.spyOn(mcpApi, 'toggleWorkflowsMcpAccessApi').mockResolvedValue({
updatedCount: 1,

View File

@ -63,7 +63,7 @@ describe('useExecutionDebugging()', () => {
mockWorkflowDocumentStore.getParentNodes.mockReturnValue([]);
const workflowStore = mockedStore(useWorkflowsStore);
workflowStore.workflow.id = 'test-workflow';
workflowStore.setWorkflowId('test-workflow');
toast = useToast();

View File

@ -108,7 +108,7 @@ describe('LogsOverviewPanel', () => {
setActivePinia(pinia);
workflowsStore = mockedStore(useWorkflowsStore);
workflowsStore.workflowId = 'test-workflow-id';
workflowsStore.setWorkflowId('test-workflow-id');
pushConnectionStore = mockedStore(usePushConnectionStore);
pushConnectionStore.isConnected = true;

View File

@ -84,21 +84,23 @@ describe('LogsPanel', () => {
let aiChatExecutionResponse: typeof aiChatExecutionResponseTemplate;
function hydrateDocumentStore(workflow: IWorkflowDb) {
const store = useWorkflowDocumentStore(createWorkflowDocumentId('test-workflow-id'));
function setWorkflow(workflow: IWorkflowDb) {
workflowsStore.setWorkflowId(workflow.id);
const store = useWorkflowDocumentStore(createWorkflowDocumentId(workflow.id));
store.hydrate(workflow);
}
function render() {
const wfId = workflowsStore.workflowId;
const wrapper = renderComponent(LogsPanel, {
global: {
provide: {
[ChatSymbol as symbol]: {},
[ChatOptionsSymbol as symbol]: {},
[WorkflowStateKey as symbol]: workflowState,
[WorkflowIdKey as unknown as string]: computed(() => 'test-workflow-id'),
[WorkflowIdKey as unknown as string]: computed(() => wfId),
[WorkflowDocumentStoreKey as symbol]: shallowRef(
useWorkflowDocumentStore(createWorkflowDocumentId('test-workflow-id')),
useWorkflowDocumentStore(createWorkflowDocumentId(wfId)),
),
},
plugins: [
@ -166,7 +168,7 @@ describe('LogsPanel', () => {
});
it('should only render logs panel if the workflow has no chat trigger', async () => {
workflowsStore.workflow = aiManualWorkflow;
setWorkflow(aiManualWorkflow);
const rendered = render();
@ -175,7 +177,7 @@ describe('LogsPanel', () => {
});
it('should render chat panel and logs panel if the workflow has chat trigger', async () => {
workflowsStore.workflow = aiChatWorkflow;
setWorkflow(aiChatWorkflow);
const rendered = render();
@ -185,7 +187,7 @@ describe('LogsPanel', () => {
it('should render only output panel of selected node by default', async () => {
logsStore.toggleOpen(true);
workflowsStore.workflow = aiManualWorkflow;
setWorkflow(aiManualWorkflow);
workflowState.setWorkflowExecutionData(aiManualExecutionResponse);
const rendered = render();
@ -199,7 +201,7 @@ describe('LogsPanel', () => {
it('should render both input and output panel of selected node by default if it is sub node', async () => {
logsStore.toggleOpen(true);
workflowsStore.workflow = aiChatWorkflow;
setWorkflow(aiChatWorkflow);
workflowState.setWorkflowExecutionData(aiChatExecutionResponse);
const rendered = render();
@ -212,7 +214,7 @@ describe('LogsPanel', () => {
});
it('toggles panel when header is clicked', async () => {
workflowsStore.workflow = aiChatWorkflow;
setWorkflow(aiChatWorkflow);
const rendered = render();
@ -228,7 +230,7 @@ describe('LogsPanel', () => {
});
it('should toggle panel when chevron icon button in the overview panel is clicked', async () => {
workflowsStore.workflow = aiChatWorkflow;
setWorkflow(aiChatWorkflow);
const rendered = render();
@ -242,7 +244,7 @@ describe('LogsPanel', () => {
});
it('should open log details panel when a log entry is clicked in the logs overview panel', async () => {
workflowsStore.workflow = aiChatWorkflow;
setWorkflow(aiChatWorkflow);
workflowState.setWorkflowExecutionData(aiChatExecutionResponse);
const rendered = render();
@ -259,7 +261,7 @@ describe('LogsPanel', () => {
});
it("should show the button to toggle panel in the header of log details panel when it's opened", async () => {
workflowsStore.workflow = aiChatWorkflow;
setWorkflow(aiChatWorkflow);
workflowState.setWorkflowExecutionData(aiChatExecutionResponse);
const rendered = render();
@ -324,7 +326,7 @@ describe('LogsPanel', () => {
it('should reflect changes to execution data in workflow store if execution is in progress', async () => {
logsStore.toggleOpen(true);
workflowsStore.workflow = aiChatWorkflow;
setWorkflow(aiChatWorkflow);
workflowState.setWorkflowExecutionData({
...aiChatExecutionResponse,
id: IN_PROGRESS_EXECUTION_ID,
@ -400,7 +402,8 @@ describe('LogsPanel', () => {
it('should still show logs for a removed node', async () => {
const operations = useCanvasOperations();
workflowsStore.workflow = deepCopy(aiChatWorkflow);
const workflow = deepCopy(aiChatWorkflow);
setWorkflow(workflow);
logsStore.toggleOpen(true);
workflowState.setWorkflowExecutionData({
...aiChatExecutionResponse,
@ -419,13 +422,14 @@ describe('LogsPanel', () => {
await nextTick();
expect(workflowsStore.workflow.nodes.find((n) => n.name === 'AI Agent')).toBeUndefined();
const docStore = useWorkflowDocumentStore(createWorkflowDocumentId(workflow.id));
expect(docStore.allNodes.find((n) => n.name === 'AI Agent')).toBeUndefined();
expect(rendered.queryByText('AI Agent')).toBeInTheDocument();
});
it('should open NDV if the button is clicked', async () => {
logsStore.toggleOpen(true);
workflowsStore.workflow = aiChatWorkflow;
setWorkflow(aiChatWorkflow);
workflowState.setWorkflowExecutionData(aiChatExecutionResponse);
const rendered = render();
@ -444,7 +448,7 @@ describe('LogsPanel', () => {
it('should toggle subtree when chevron icon button is pressed', async () => {
logsStore.toggleOpen(true);
workflowsStore.workflow = aiChatWorkflow;
setWorkflow(aiChatWorkflow);
workflowState.setWorkflowExecutionData(aiChatExecutionResponse);
const rendered = render();
@ -471,7 +475,7 @@ describe('LogsPanel', () => {
it('should toggle input and output panel when the button is clicked', async () => {
logsStore.toggleOpen(true);
workflowsStore.workflow = aiChatWorkflow;
setWorkflow(aiChatWorkflow);
workflowState.setWorkflowExecutionData(aiChatExecutionResponse);
const rendered = render();
@ -501,8 +505,7 @@ describe('LogsPanel', () => {
// Create deep copy so that renaming doesn't affect other test cases
const workflow = deepCopy(aiChatWorkflow);
workflow.id = 'test-workflow-id';
workflowsStore.workflow = workflow;
hydrateDocumentStore(workflow);
setWorkflow(workflow);
workflowState.setWorkflowExecutionData(aiChatExecutionResponse);
const rendered = render();
@ -527,7 +530,7 @@ describe('LogsPanel', () => {
describe('selection', () => {
beforeEach(() => {
logsStore.toggleOpen(true);
workflowsStore.workflow = aiChatWorkflow;
setWorkflow(aiChatWorkflow);
workflowState.setWorkflowExecutionData(aiChatExecutionResponse);
});
@ -586,8 +589,7 @@ describe('LogsPanel', () => {
const workflow = deepCopy(aiChatWorkflow);
workflow.id = 'test-workflow-id';
workflowsStore.workflow = workflow;
hydrateDocumentStore(workflow);
setWorkflow(workflow);
workflowState.setWorkflowExecutionData(aiChatExecutionResponse);
logsStore.toggleLogSelectionSync(true);
@ -609,7 +611,7 @@ describe('LogsPanel', () => {
describe('chat', () => {
beforeEach(() => {
logsStore.toggleOpen(true);
workflowsStore.workflow = aiChatWorkflow;
setWorkflow(aiChatWorkflow);
});
describe('rendering', () => {

View File

@ -7,6 +7,11 @@ import { useWorkflowsStore } from '@/app/stores/workflows.store';
import { useLogsStore } from '@/app/stores/logs.store';
import { useRootStore } from '@n8n/stores/useRootStore';
import { useNodeTypesStore } from '@/app/stores/nodeTypes.store';
import {
useWorkflowDocumentStore,
createWorkflowDocumentId,
} from '@/app/stores/workflowDocument.store';
import { createTestWorkflow } from '@/__tests__/mocks';
import type { INode } from 'n8n-workflow';
import * as useRunWorkflowModule from '@/app/composables/useRunWorkflow';
@ -140,30 +145,28 @@ describe('useChatState', () => {
},
};
function setWorkflowNodes(nodes: INode[]) {
const docStore = useWorkflowDocumentStore(createWorkflowDocumentId('workflow-123'));
docStore.setNodes(nodes);
}
beforeEach(() => {
const pinia = createTestingPinia({
stubActions: false,
initialState: {
workflows: {
workflow: {
id: 'workflow-123',
name: 'Test Workflow',
active: false,
createdAt: 1234567890,
updatedAt: 1234567890,
nodes: [mockChatTriggerNode],
connections: {},
settings: {},
tags: [],
pinData: {},
versionId: '',
isArchived: false,
},
},
},
});
setActivePinia(pinia);
workflowsStore = useWorkflowsStore();
workflowsStore.setWorkflowId('workflow-123');
const testWorkflow = createTestWorkflow({
id: 'workflow-123',
name: 'Test Workflow',
nodes: [mockChatTriggerNode],
connections: {},
});
const docStore = useWorkflowDocumentStore(createWorkflowDocumentId('workflow-123'));
docStore.hydrate(testWorkflow);
logsStore = useLogsStore();
const rootStore = useRootStore();
nodeTypesStore = useNodeTypesStore();
@ -213,9 +216,7 @@ describe('useChatState', () => {
});
it('should return null for chatTriggerNode when not present', () => {
workflowsStore.$patch((state) => {
state.workflow.nodes = [];
});
setWorkflowNodes([]);
const chatState = useChatState(false);
@ -256,18 +257,16 @@ describe('useChatState', () => {
describe('file upload configuration', () => {
it('should detect file uploads allowed from options', async () => {
workflowsStore.$patch((state) => {
state.workflow.nodes = [
{
...mockChatTriggerNode,
parameters: {
options: {
allowFileUploads: true,
},
setWorkflowNodes([
{
...mockChatTriggerNode,
parameters: {
options: {
allowFileUploads: true,
},
},
];
});
},
]);
const chatState = useChatState(false);
await nextTick();
@ -276,18 +275,16 @@ describe('useChatState', () => {
});
it('should detect file uploads disabled', async () => {
workflowsStore.$patch((state) => {
state.workflow.nodes = [
{
...mockChatTriggerNode,
parameters: {
options: {
allowFileUploads: false,
},
setWorkflowNodes([
{
...mockChatTriggerNode,
parameters: {
options: {
allowFileUploads: false,
},
},
];
});
},
]);
const chatState = useChatState(false);
await nextTick();
@ -296,18 +293,16 @@ describe('useChatState', () => {
});
it('should get allowed MIME types from options', async () => {
workflowsStore.$patch((state) => {
state.workflow.nodes = [
{
...mockChatTriggerNode,
parameters: {
options: {
allowedFilesMimeTypes: 'image/*,application/pdf',
},
setWorkflowNodes([
{
...mockChatTriggerNode,
parameters: {
options: {
allowedFilesMimeTypes: 'image/*,application/pdf',
},
},
];
});
},
]);
const chatState = useChatState(false);
await nextTick();
@ -343,9 +338,7 @@ describe('useChatState', () => {
});
it('should return empty webhook URL when no chatTriggerNode', () => {
workflowsStore.$patch((state) => {
state.workflow.nodes = [];
});
setWorkflowNodes([]);
const chatState = useChatState(false);
@ -353,9 +346,7 @@ describe('useChatState', () => {
});
it('should return empty webhook URL when no workflow ID', () => {
workflowsStore.$patch((state) => {
state.workflow.id = '';
});
workflowsStore.setWorkflowId('');
const chatState = useChatState(false);
@ -371,9 +362,7 @@ describe('useChatState', () => {
});
it('should not be ready when no chatTriggerNode', () => {
workflowsStore.$patch((state) => {
state.workflow.nodes = [];
});
setWorkflowNodes([]);
const chatState = useChatState(false);
@ -428,9 +417,7 @@ describe('useChatState', () => {
});
it('should not register if no chatTriggerNode', async () => {
workflowsStore.$patch((state) => {
state.workflow.nodes = [];
});
setWorkflowNodes([]);
const chatState = useChatState(false);
await chatState.registerChatWebhook();
@ -454,20 +441,18 @@ describe('useChatState', () => {
describe('chatOptions', () => {
it('should generate correct chatOptions with all configurations', async () => {
workflowsStore.$patch((state) => {
state.workflow.nodes = [
{
...mockChatTriggerNode,
parameters: {
options: {
responseMode: 'streaming',
allowFileUploads: true,
allowedFilesMimeTypes: 'image/*,application/pdf',
},
setWorkflowNodes([
{
...mockChatTriggerNode,
parameters: {
options: {
responseMode: 'streaming',
allowFileUploads: true,
allowedFilesMimeTypes: 'image/*,application/pdf',
},
},
];
});
},
]);
const chatState = useChatState(false);
await nextTick();
@ -560,9 +545,7 @@ describe('useChatState', () => {
});
it('should resolve all defaults from node type when options not set', async () => {
workflowsStore.$patch((state) => {
state.workflow.nodes = [{ ...mockChatTriggerNode, parameters: {} }];
});
setWorkflowNodes([{ ...mockChatTriggerNode, parameters: {} }]);
const chatState = useChatState(false);
await nextTick();
@ -573,14 +556,12 @@ describe('useChatState', () => {
});
it('should default to lastNode when availableInChat is false', async () => {
workflowsStore.$patch((state) => {
state.workflow.nodes = [
{
...mockChatTriggerNode,
parameters: { availableInChat: false },
},
];
});
setWorkflowNodes([
{
...mockChatTriggerNode,
parameters: { availableInChat: false },
},
]);
const chatState = useChatState(false);
await nextTick();
@ -589,14 +570,12 @@ describe('useChatState', () => {
});
it('should default to streaming when availableInChat (Chat hub) is true', async () => {
workflowsStore.$patch((state) => {
state.workflow.nodes = [
{
...mockChatTriggerNode,
parameters: { availableInChat: true },
},
];
});
setWorkflowNodes([
{
...mockChatTriggerNode,
parameters: { availableInChat: true },
},
]);
const chatState = useChatState(false);
await nextTick();
@ -605,14 +584,12 @@ describe('useChatState', () => {
});
it('should pick the correct options collection when public is true', async () => {
workflowsStore.$patch((state) => {
state.workflow.nodes = [
{
...mockChatTriggerNode,
parameters: { public: true, availableInChat: true },
},
];
});
setWorkflowNodes([
{
...mockChatTriggerNode,
parameters: { public: true, availableInChat: true },
},
]);
const chatState = useChatState(false);
await nextTick();
@ -623,17 +600,15 @@ describe('useChatState', () => {
it('should use explicit responseMode over default when set', async () => {
// availableInChat is true (default would be streaming),
// but user explicitly set responseMode to lastNode
workflowsStore.$patch((state) => {
state.workflow.nodes = [
{
...mockChatTriggerNode,
parameters: {
availableInChat: true,
options: { responseMode: 'lastNode' },
},
setWorkflowNodes([
{
...mockChatTriggerNode,
parameters: {
availableInChat: true,
options: { responseMode: 'lastNode' },
},
];
});
},
]);
const chatState = useChatState(false);
await nextTick();

View File

@ -1,7 +1,6 @@
import { createTestNode, createTestWorkflow, createTestWorkflowObject } from '@/__tests__/mocks';
import { createComponentRenderer } from '@/__tests__/render';
import InputPanel, { type Props } from './InputPanel.vue';
import { useWorkflowsStore } from '@/app/stores/workflows.store';
import { createTestingPinia } from '@pinia/testing';
import { waitFor } from '@testing-library/vue';
import {
@ -67,7 +66,6 @@ const render = (props: Partial<Props> = {}, pinData?: INodeExecutionData[], runD
setActivePinia(pinia);
const workflow = createTestWorkflow({ nodes, connections });
const workflowStore = useWorkflowsStore();
const workflowState = useWorkflowState();
const workflowDocumentStore = useWorkflowDocumentStore(createWorkflowDocumentId(workflow.id));
@ -76,7 +74,7 @@ const render = (props: Partial<Props> = {}, pinData?: INodeExecutionData[], runD
vi.mocked(injectWorkflowDocumentStore).mockReturnValue(shallowRef(workflowDocumentStore));
if (pinData) {
workflowStore.workflow.pinData = Object.fromEntries(nodes.map((n) => [n.name, pinData]));
workflowDocumentStore.setPinData(Object.fromEntries(nodes.map((n) => [n.name, pinData])));
}
if (runData) {

View File

@ -27,13 +27,13 @@ describe('TriggerPanel.vue', () => {
beforeEach(async () => {
setActivePinia(createTestingPinia({ stubActions: false }));
workflowsStore = mockedStore(useWorkflowsStore);
workflowsStore.workflowId = '1';
workflowsStore.setWorkflowId('1');
const node = createTestNode({ id: '0', name: 'Webhook', type: 'n8n-nodes-base.webhook' });
workflowsStore.workflow.nodes = [node];
workflowDocStore = useWorkflowDocumentStore(
createWorkflowDocumentId(workflowsStore.workflowId),
);
workflowDocStore.setNodes([node]);
vi.mocked(injectWorkflowDocumentStore).mockReturnValue(shallowRef(workflowDocStore));
nodeTypesStore = mockedStore(useNodeTypesStore);

View File

@ -73,14 +73,13 @@ exports[`InputPanel > should render 1`] = `
data-test-id="run-data-pane-header"
data-v-5b5900d0=""
>
<!--v-if-->
<!---->
<!--v-if-->
<div
class="n8n-radio-buttons radioGroup"
data-test-id="ndv-run-data-display-mode"
data-v-5b5900d0=""
role="radiogroup"
style="display: none;"
>
<label
@ -210,108 +209,7 @@ exports[`InputPanel > should render 1`] = `
data-v-5b5900d0=""
>
<!--v-if-->
<div
class="center"
data-v-5b5900d0=""
>
<div
class="noOutputData"
>
<article
class="empty"
>
<svg
aria-hidden="true"
class="n8n-icon"
data-icon="arrow-right-to-line"
focusable="false"
height="20px"
role="img"
viewBox="0 0 24 24"
width="20px"
>
<path
d="M17 12H3m8 6l6-6l-6-6m10-1v14"
fill="none"
stroke="currentColor"
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
/>
</svg>
<h1
class="title"
>
No input data
</h1>
<p
class="description"
>
<span>
<span
class=""
data-grace-area-trigger=""
data-state="closed"
>
<button
aria-live="polite"
class="button button solid medium"
data-test-id="execute-previous-node"
square="false"
title=""
transparent-background="true"
type="submit"
>
<transition-stub
appear="false"
css="true"
name="n8n-button-fade"
persisted="false"
>
<!--v-if-->
</transition-stub>
<div
class="button-inner"
>
<!--v-if-->
<span>
Execute previous nodes
</span>
</div>
</button>
</span>
<!--teleport start-->
<!--teleport end-->
<br />
to view input data
</span>
</p>
<!--v-if-->
</article>
</div>
</div>
<!---->
</div>
<!--v-if-->
<transition-stub

View File

@ -1469,13 +1469,13 @@ describe('RunData', () => {
nodeTypesStore.setNodeTypes(defaultNodeDescriptions);
const testWorkflowId = workflowId ?? 'test-workflow';
workflowDocumentStore = useWorkflowDocumentStore(createWorkflowDocumentId(testWorkflowId));
vi.mocked(workflowDocumentStore).getNodeByName.mockReturnValue(workflowNodes[0]);
vi.spyOn(workflowDocumentStore, 'getNodeByName').mockReturnValue(workflowNodes[0]);
// Mock ndvStore methods
ndvStore.setOutputPanelEditModeEnabled = vi.fn();
ndvStore.setOutputPanelEditModeValue = vi.fn();
workflowsStore.workflow.id = testWorkflowId;
workflowsStore.setWorkflowId(testWorkflowId);
if (pinnedData) {
workflowDocumentStore.pinNodeData('Test Node', pinnedData);

View File

@ -59,8 +59,9 @@ async function createPiniaWithActiveNode() {
const ndvStore = useNDVStore();
nodeTypesStore.setNodeTypes(defaultNodeDescriptions);
workflowsStore.workflow = workflow;
workflowsStore.setWorkflowId(workflow.id);
const workflowDocumentStore = useWorkflowDocumentStore(createWorkflowDocumentId(workflow.id));
workflowDocumentStore.addNode(node);
workflowDocumentStore.initPristineNodeMetadata(node.name);
workflowsStore.setWorkflowExecutionData({
id: '1',

View File

@ -1,5 +1,6 @@
import {
createTestNode,
createTestWorkflow,
defaultNodeDescriptions,
mockNodeTypeDescription,
} from '@/__tests__/mocks';
@ -14,7 +15,6 @@ import {
SET_NODE_TYPE,
SPLIT_IN_BATCHES_NODE_TYPE,
} from '@/app/constants';
import type { IWorkflowDb } from '@/Interface';
import { useNDVStore } from '@/features/ndv/shared/ndv.store';
import { useNodeTypesStore } from '@/app/stores/nodeTypes.store';
import { useWorkflowsStore } from '@/app/stores/workflows.store';
@ -147,12 +147,10 @@ const mockI18nKeys: Record<string, string> = {
};
async function setupStore() {
const workflow = {
const workflow = createTestWorkflow({
id: '123',
name: 'Test Workflow',
connections: {},
active: true,
pinData: {} as Record<string, INodeExecutionData[]>,
nodes: [
mockNode1,
mockNode2,
@ -165,7 +163,7 @@ async function setupStore() {
customerDatastoreNode,
mergeNode,
],
};
});
const pinia = createTestingPinia({ stubActions: false });
setActivePinia(pinia);
@ -203,7 +201,9 @@ async function setupStore() {
outputs: [NodeConnectionTypes.Main],
}),
]);
workflowsStore.workflow = workflow as IWorkflowDb;
workflowsStore.setWorkflowId(workflow.id);
const workflowDocumentStore = useWorkflowDocumentStore(createWorkflowDocumentId(workflow.id));
workflowDocumentStore.hydrate(workflow);
ndvStore.setActiveNodeName('Test Node Name', 'other');
return pinia;
@ -212,7 +212,7 @@ async function setupStore() {
function pinData(node: { name: string }, data: INodeExecutionData[]) {
const workflowsStore = useWorkflowsStore();
const workflowDocumentStore = useWorkflowDocumentStore(
createWorkflowDocumentId(workflowsStore.workflow.id),
createWorkflowDocumentId(workflowsStore.workflowId),
);
workflowDocumentStore.pinNodeData(node.name, data);
}
@ -287,10 +287,9 @@ describe('VirtualSchema.vue', () => {
const workflowsStore = useWorkflowsStore();
const workflowDocumentStore = useWorkflowDocumentStore(
createWorkflowDocumentId(workflowsStore.workflow.id),
createWorkflowDocumentId(workflowsStore.workflowId),
);
workflowDocumentStore.setActiveState({ activeVersionId: 'v1', activeVersion: null });
workflowDocumentStore.setName(workflowsStore.workflow.name);
renderComponent = createComponentRenderer(VirtualSchema, {
global: {

View File

@ -43,7 +43,7 @@ async function createPiniaStore(isActiveNode: boolean) {
const ndvStore = useNDVStore();
nodeTypesStore.setNodeTypes(defaultNodeDescriptions);
workflowsStore.workflow = workflow;
workflowsStore.setWorkflowId(workflow.id);
const workflowDocumentStore = useWorkflowDocumentStore(createWorkflowDocumentId(workflow.id));
workflowDocumentStore.setNodes(workflow.nodes);
workflowDocumentStore.setConnections(workflow.connections);

View File

@ -40,7 +40,7 @@ const setupStore = (nodes: Array<ReturnType<typeof createTestNode>>) => {
const nodeTypesStore = useNodeTypesStore();
nodeTypesStore.setNodeTypes(defaultNodeDescriptions);
workflowsStore.workflow = workflow;
workflowsStore.setWorkflowId(workflow.id);
const workflowDocumentStore = useWorkflowDocumentStore(createWorkflowDocumentId(workflow.id));
workflowDocumentStore.hydrate(workflow);
workflowDocumentStore.setAllNodeMetadata(

View File

@ -1,19 +1,22 @@
import { removePreviewToken } from '@/features/shared/nodeCreator/nodeCreator.utils';
import type { IWorkflowDb } from '@/Interface';
import { useCommunityNodesStore } from '../communityNodes.store';
import { useCredentialsStore } from '@/features/credentials/credentials.store';
import { useNodeTypesStore } from '@/app/stores/nodeTypes.store';
import { useUsersStore } from '@/features/settings/users/users.store';
import { useWorkflowsStore } from '@/app/stores/workflows.store';
import {
useWorkflowDocumentStore,
createWorkflowDocumentId,
} from '@/app/stores/workflowDocument.store';
import { useSettingsStore } from '@/app/stores/settings.store';
import type { CommunityNodeType } from '@n8n/api-types';
import { createTestingPinia } from '@pinia/testing';
import type { INode } from 'n8n-workflow';
import { setActivePinia } from 'pinia';
import { useCanvasOperations } from '@/app/composables/useCanvasOperations';
import { useInstallNode } from './useInstallNode';
import { useToast } from '@/app/composables/useToast';
import { useTelemetry } from '@/app/composables/useTelemetry';
import { DEFAULT_SETTINGS } from '@/app/stores/workflowDocument/useWorkflowDocumentSettings';
vi.mock('@/app/composables/useCanvasOperations', () => ({
useCanvasOperations: vi.fn().mockReturnValue({
@ -66,7 +69,7 @@ const showError = vi.fn();
const showMessage = vi.fn();
beforeEach(() => {
const pinia = createTestingPinia();
const pinia = createTestingPinia({ stubActions: false });
setActivePinia(pinia);
nodeTypesStore = useNodeTypesStore(pinia);
@ -102,11 +105,11 @@ beforeEach(() => {
writable: true,
});
vi.mocked(communityNodesStore.installPackage).mockResolvedValue(undefined);
vi.mocked(nodeTypesStore.getNodeTypes).mockResolvedValue(undefined);
vi.mocked(nodeTypesStore.fetchCommunityNodePreviews).mockResolvedValue(undefined);
vi.mocked(credentialsStore.fetchCredentialTypes).mockResolvedValue(undefined);
vi.mocked(nodeTypesStore.getCommunityNodeAttributes).mockResolvedValue({
vi.spyOn(communityNodesStore, 'installPackage').mockResolvedValue(undefined);
vi.spyOn(nodeTypesStore, 'getNodeTypes').mockResolvedValue(undefined);
vi.spyOn(nodeTypesStore, 'fetchCommunityNodePreviews').mockResolvedValue(undefined);
vi.spyOn(credentialsStore, 'fetchCredentialTypes').mockResolvedValue(undefined);
vi.spyOn(nodeTypesStore, 'getCommunityNodeAttributes').mockResolvedValue({
npmVersion: '1.0.0',
authorGithubUrl: 'https://github.com/test',
authorName: 'Test Author',
@ -122,7 +125,9 @@ beforeEach(() => {
version: '1.0.0',
} as unknown as CommunityNodeType);
workflowsStore.workflow = {
workflowsStore.workflowId = 'test-workflow';
const workflowDocumentStore = useWorkflowDocumentStore(createWorkflowDocumentId('test-workflow'));
workflowDocumentStore.hydrate({
id: 'test-workflow',
name: 'Test Workflow',
active: false,
@ -131,12 +136,11 @@ beforeEach(() => {
updatedAt: new Date().toISOString(),
nodes: [],
connections: {},
settings: {},
staticData: {},
settings: { ...DEFAULT_SETTINGS },
tags: [],
triggerCount: 0,
versionId: '1',
} as unknown as IWorkflowDb;
activeVersionId: '1',
});
vi.clearAllMocks();
});
@ -308,7 +312,7 @@ describe('useInstallNode', () => {
type: 'test-node',
name: 'Node 1',
typeVersion: 1,
position: [100, 100] as [number, number],
position: [128, 128] as [number, number],
parameters: {},
},
{
@ -316,11 +320,12 @@ describe('useInstallNode', () => {
type: 'other-node',
name: 'Node 2',
typeVersion: 1,
position: [200, 200] as [number, number],
position: [256, 256] as [number, number],
parameters: {},
},
];
workflowsStore.workflow.nodes = mockNodes as INode[];
const store = useWorkflowDocumentStore(createWorkflowDocumentId('test-workflow'));
store.setNodes(mockNodes);
const { installNode } = useInstallNode();
@ -337,23 +342,23 @@ describe('useInstallNode', () => {
type: 'test-node',
name: 'Node 1',
typeVersion: 1,
position: [100, 100] as [number, number],
position: [128, 128] as [number, number],
parameters: {},
},
]);
});
it('should not initialize nodes when nodeType is not provided', async () => {
workflowsStore.workflow.nodes = [
useWorkflowDocumentStore(createWorkflowDocumentId('test-workflow')).setNodes([
{
id: 'node-1',
type: 'test-node',
name: 'Node 1',
typeVersion: 1,
position: [100, 100] as [number, number],
position: [128, 128] as [number, number],
parameters: {},
},
] as INode[];
]);
const { installNode } = useInstallNode();
@ -366,7 +371,7 @@ describe('useInstallNode', () => {
});
it('should not initialize nodes when workflow has no nodes', async () => {
workflowsStore.workflow.nodes = [];
useWorkflowDocumentStore(createWorkflowDocumentId('test-workflow')).setNodes([]);
const { installNode } = useInstallNode();
@ -553,7 +558,7 @@ describe('useInstallNode', () => {
type: 'preview:test-node',
name: 'Node 1',
typeVersion: 1,
position: [100, 100] as [number, number],
position: [128, 128] as [number, number],
parameters: {},
},
{
@ -561,7 +566,7 @@ describe('useInstallNode', () => {
type: 'test-node',
name: 'Node 2',
typeVersion: 1,
position: [200, 200] as [number, number],
position: [256, 256] as [number, number],
parameters: {},
},
{
@ -569,11 +574,12 @@ describe('useInstallNode', () => {
type: 'other-node',
name: 'Node 3',
typeVersion: 1,
position: [300, 300] as [number, number],
position: [384, 384] as [number, number],
parameters: {},
},
];
workflowsStore.workflow.nodes = mockNodes as INode[];
const store = useWorkflowDocumentStore(createWorkflowDocumentId('test-workflow'));
store.setNodes(mockNodes);
vi.mocked(removePreviewToken).mockReturnValue('test-node');
@ -592,7 +598,7 @@ describe('useInstallNode', () => {
type: 'test-node',
name: 'Node 2',
typeVersion: 1,
position: [200, 200] as [number, number],
position: [256, 256] as [number, number],
parameters: {},
},
]);
@ -605,7 +611,7 @@ describe('useInstallNode', () => {
type: 'test-node',
name: 'Node 1',
typeVersion: 1,
position: [100, 100] as [number, number],
position: [128, 128] as [number, number],
parameters: {},
},
{
@ -613,7 +619,7 @@ describe('useInstallNode', () => {
type: 'test-node',
name: 'Node 2',
typeVersion: 1,
position: [200, 200] as [number, number],
position: [256, 256] as [number, number],
parameters: {},
},
{
@ -621,11 +627,12 @@ describe('useInstallNode', () => {
type: 'other-node',
name: 'Node 3',
typeVersion: 1,
position: [300, 300] as [number, number],
position: [384, 384] as [number, number],
parameters: {},
},
];
workflowsStore.workflow.nodes = mockNodes as INode[];
const store = useWorkflowDocumentStore(createWorkflowDocumentId('test-workflow'));
store.setNodes(mockNodes);
vi.mocked(removePreviewToken).mockReturnValue('test-node');
@ -643,7 +650,7 @@ describe('useInstallNode', () => {
type: 'test-node',
name: 'Node 1',
typeVersion: 1,
position: [100, 100] as [number, number],
position: [128, 128] as [number, number],
parameters: {},
},
{
@ -651,7 +658,7 @@ describe('useInstallNode', () => {
type: 'test-node',
name: 'Node 2',
typeVersion: 1,
position: [200, 200] as [number, number],
position: [256, 256] as [number, number],
parameters: {},
},
]);

View File

@ -166,7 +166,8 @@ describe('NodeSetupCard', () => {
mockComposableState.listeningHint = '';
createTestingPinia();
const workflowsStore = useWorkflowsStore();
workflowsStore.workflow.id = WORKFLOW_ID;
// Directly assign because createTestingPinia stubs actions (setWorkflowId is a no-op)
workflowsStore.workflowId = WORKFLOW_ID;
nodeTypesStore = mockedStore(useNodeTypesStore);
setupPanelStore = mockedStore(useSetupPanelStore);
nodeTypesStore.isTriggerNode = vi.fn().mockReturnValue(false);

View File

@ -75,7 +75,7 @@ describe('useRecentResources', () => {
recentNodesRef.value = {};
mockWorkflowsStore = useWorkflowsStore();
mockWorkflowsStore.workflow.id = 'workflow-1';
mockWorkflowsStore.workflowId = 'workflow-1';
mockWorkflowsListStore = useWorkflowsListStore();
mockNodeTypesStore = useNodeTypesStore();

View File

@ -99,7 +99,7 @@ describe('useWorkflowCommands', () => {
saveCurrentWorkflowMock.mockResolvedValue(true);
mockWorkflowsStore.workflow = mockWorkflow.value;
mockWorkflowsStore.workflowId = mockWorkflow.value.id;
// Mark workflow as existing by adding it to workflowsById
mockWorkflowsListStore.workflowsById = { [mockWorkflow.value.id]: mockWorkflow.value };

View File

@ -60,9 +60,9 @@ describe('useContextMenu', () => {
vi.spyOn(uiStore, 'isReadOnlyView', 'get').mockReturnValue(false);
workflowsStore = useWorkflowsStore();
workflowsStore.workflow.id = testWorkflowId;
workflowsStore.workflow.nodes = nodes;
workflowsStore.setWorkflowId(testWorkflowId);
workflowDocumentStore = useWorkflowDocumentStore(createWorkflowDocumentId(testWorkflowId));
workflowDocumentStore.setNodes(nodes);
workflowDocumentStore.setScopes(['workflow:update']);
vi.mocked(injectWorkflowDocumentStore).mockReturnValue(shallowRef(workflowDocumentStore));

View File

@ -88,7 +88,7 @@ describe('SqlEditor.vue', () => {
setActivePinia(pinia);
const workflowsStore = useWorkflowsStore();
workflowsStore.workflow.id = 'test-workflow';
workflowsStore.setWorkflowId('test-workflow');
const workflowDocumentStore = useWorkflowDocumentStore(
createWorkflowDocumentId(workflowsStore.workflowId),

View File

@ -7,6 +7,7 @@ import { STICKY_NODE_TYPE } from '@/app/constants';
import { CanvasNodeRenderType } from '../canvas.types';
import { createTestNode, createTestWorkflow, defaultNodeDescriptions } from '@/__tests__/mocks';
import { useNodeTypesStore } from '@/app/stores/nodeTypes.store';
import { useWorkflowsStore } from '@/app/stores/workflows.store';
import {
useWorkflowDocumentStore,
createWorkflowDocumentId,
@ -23,6 +24,7 @@ vi.mock('@vueuse/core', async () => {
});
function setupWorkflow(workflow: IWorkflowDb) {
useWorkflowsStore().workflowId = workflow.id;
const workflowDocumentStore = useWorkflowDocumentStore(createWorkflowDocumentId(workflow.id));
workflowDocumentStore.hydrate(workflow);
}

View File

@ -54,7 +54,7 @@ describe('CanvasNodeSettingsIcons', () => {
workflowsStore = mockedStore(useWorkflowsStore);
credentialsStore = mockedStore(useCredentialsStore);
workflowsStore.workflow.id = WORKFLOW_ID;
workflowsStore.workflowId = WORKFLOW_ID;
workflowDocumentStore = useWorkflowDocumentStore(createWorkflowDocumentId(WORKFLOW_ID));
// Default: feature flag disabled

View File

@ -111,7 +111,7 @@ beforeEach(() => {
// Set workflow ID so document store can be created
const workflowsStore = useWorkflowsStore();
workflowsStore.workflow.id = 'test-workflow';
workflowsStore.setWorkflowId('test-workflow');
});
afterEach(() => {
@ -121,7 +121,7 @@ afterEach(() => {
function setPinData(pinData: IPinData) {
const workflowsStore = useWorkflowsStore();
const workflowDocumentStore = useWorkflowDocumentStore(
createWorkflowDocumentId(workflowsStore.workflow.id),
createWorkflowDocumentId(workflowsStore.workflowId),
);
workflowDocumentStore.setPinData(pinData);
}
@ -300,7 +300,10 @@ describe('useCanvasMapping', () => {
},
};
workflowsStore.workflow.connections = connections;
const workflowDocumentStore = useWorkflowDocumentStore(
createWorkflowDocumentId(workflowsStore.workflowId),
);
workflowDocumentStore.setConnections(connections);
const workflowObject = createTestWorkflowObject({
nodes,

View File

@ -47,8 +47,7 @@ describe('ExperimentalNodeDetailsDrawer', () => {
});
workflowsStore = useWorkflowsStore(pinia);
workflowsStore.workflow.id = 'test-workflow';
workflowsStore.workflow.nodes = mockNodes;
workflowsStore.setWorkflowId('test-workflow');
const workflowDocumentStore = useWorkflowDocumentStore(
createWorkflowDocumentId(workflowsStore.workflowId),

View File

@ -115,13 +115,13 @@ describe('SetupWorkflowCredentialsButton', () => {
});
it('renders', () => {
workflowsStore.workflow = EMPTY_WORKFLOW;
workflowsStore.workflowId = EMPTY_WORKFLOW.id;
setWorkflowDocumentStoreState(EMPTY_WORKFLOW.meta, []);
expect(() => renderComponent()).not.toThrow();
});
it('does not render the button if there are no nodes', () => {
workflowsStore.workflow = EMPTY_WORKFLOW;
workflowsStore.workflowId = EMPTY_WORKFLOW.id;
setWorkflowDocumentStoreState(EMPTY_WORKFLOW.meta, []);
const { queryByTestId } = renderComponent();
expect(queryByTestId('setup-credentials-button')).toBeNull();
@ -141,7 +141,7 @@ describe('SetupWorkflowCredentialsButton', () => {
},
],
};
workflowsStore.workflow = workflowWithNodes;
workflowsStore.workflowId = workflowWithNodes.id;
setWorkflowDocumentStoreState(workflowWithNodes.meta, workflowWithNodes.nodes);
setupPanelStore.isFeatureEnabled = true;
focusPanelStore.focusPanelActive = true;
@ -165,7 +165,7 @@ describe('SetupWorkflowCredentialsButton', () => {
},
],
};
workflowsStore.workflow = workflowWithNodes;
workflowsStore.workflowId = workflowWithNodes.id;
setWorkflowDocumentStoreState(workflowWithNodes.meta, workflowWithNodes.nodes);
setupPanelStore.isFeatureEnabled = true;
focusPanelStore.focusPanelActive = false;
@ -189,7 +189,7 @@ describe('SetupWorkflowCredentialsButton', () => {
},
],
};
workflowsStore.workflow = workflowWithNodes;
workflowsStore.workflowId = workflowWithNodes.id;
setWorkflowDocumentStoreState(workflowWithNodes.meta, workflowWithNodes.nodes);
mockDoesNodeHaveAllCredentialsFilled.mockReturnValue(false);
setupPanelStore.isFeatureEnabled = false;
@ -215,7 +215,7 @@ describe('SetupWorkflowCredentialsButton', () => {
},
],
};
workflowsStore.workflow = readyToRunWorkflow;
workflowsStore.workflowId = readyToRunWorkflow.id;
setWorkflowDocumentStoreState(readyToRunWorkflow.meta, readyToRunWorkflow.nodes);
mockGetVariant.mockReturnValue(TEMPLATE_SETUP_EXPERIENCE.variant);
@ -234,7 +234,7 @@ describe('SetupWorkflowCredentialsButton', () => {
meta: { templateId: 'ready-to-run-ai-workflow-v5', templateCredsSetupCompleted: false },
nodes: [],
};
workflowsStore.workflow = templateWorkflow;
workflowsStore.workflowId = templateWorkflow.id;
setWorkflowDocumentStoreState(templateWorkflow.meta, []);
mockGetVariant.mockReturnValue(TEMPLATE_SETUP_EXPERIENCE.variant);
@ -265,7 +265,7 @@ describe('SetupWorkflowCredentialsButton', () => {
};
it('opens modal when all conditions are met and setup panel is disabled', async () => {
workflowsStore.workflow = workflowWithUnfilledCredentials;
workflowsStore.workflowId = workflowWithUnfilledCredentials.id;
setWorkflowDocumentStoreState(
workflowWithUnfilledCredentials.meta,
workflowWithUnfilledCredentials.nodes,
@ -283,7 +283,7 @@ describe('SetupWorkflowCredentialsButton', () => {
});
it('opens setup panel when all conditions are met and setup panel is enabled', async () => {
workflowsStore.workflow = workflowWithUnfilledCredentials;
workflowsStore.workflowId = workflowWithUnfilledCredentials.id;
setWorkflowDocumentStoreState(
workflowWithUnfilledCredentials.meta,
workflowWithUnfilledCredentials.nodes,
@ -303,7 +303,7 @@ describe('SetupWorkflowCredentialsButton', () => {
});
it('does not open modal when not on template import route (no templateId in query)', () => {
workflowsStore.workflow = workflowWithUnfilledCredentials;
workflowsStore.workflowId = workflowWithUnfilledCredentials.id;
setWorkflowDocumentStoreState(
workflowWithUnfilledCredentials.meta,
workflowWithUnfilledCredentials.nodes,
@ -319,7 +319,7 @@ describe('SetupWorkflowCredentialsButton', () => {
});
it('does not open modal when feature flag is disabled', () => {
workflowsStore.workflow = workflowWithUnfilledCredentials;
workflowsStore.workflowId = workflowWithUnfilledCredentials.id;
setWorkflowDocumentStoreState(
workflowWithUnfilledCredentials.meta,
workflowWithUnfilledCredentials.nodes,
@ -339,7 +339,7 @@ describe('SetupWorkflowCredentialsButton', () => {
...workflowWithUnfilledCredentials,
meta: { templateId: '123', templateCredsSetupCompleted: true },
};
workflowsStore.workflow = completedWorkflow;
workflowsStore.workflowId = completedWorkflow.id;
setWorkflowDocumentStoreState(completedWorkflow.meta, completedWorkflow.nodes);
mockDoesNodeHaveAllCredentialsFilled.mockReturnValue(false);
mockGetVariant.mockReturnValue(TEMPLATE_SETUP_EXPERIENCE.variant);
@ -356,7 +356,7 @@ describe('SetupWorkflowCredentialsButton', () => {
...workflowWithUnfilledCredentials,
meta: {},
};
workflowsStore.workflow = nonTemplateWorkflow;
workflowsStore.workflowId = nonTemplateWorkflow.id;
setWorkflowDocumentStoreState(nonTemplateWorkflow.meta, nonTemplateWorkflow.nodes);
mockDoesNodeHaveAllCredentialsFilled.mockReturnValue(false);
mockGetVariant.mockReturnValue(TEMPLATE_SETUP_EXPERIENCE.variant);
@ -369,7 +369,7 @@ describe('SetupWorkflowCredentialsButton', () => {
});
it('does not open modal when all credentials are already filled', () => {
workflowsStore.workflow = workflowWithUnfilledCredentials;
workflowsStore.workflowId = workflowWithUnfilledCredentials.id;
setWorkflowDocumentStoreState(
workflowWithUnfilledCredentials.meta,
workflowWithUnfilledCredentials.nodes,
@ -385,7 +385,7 @@ describe('SetupWorkflowCredentialsButton', () => {
});
it('does not open modal for ready-to-run workflows', () => {
workflowsStore.workflow = workflowWithUnfilledCredentials;
workflowsStore.workflowId = workflowWithUnfilledCredentials.id;
setWorkflowDocumentStoreState(
workflowWithUnfilledCredentials.meta,
workflowWithUnfilledCredentials.nodes,