mirror of
https://github.com/n8n-io/n8n.git
synced 2026-05-12 16:10:30 +02:00
refactor(editor): Delete workflow ref from workflows.store.ts (#29531)
This commit is contained in:
parent
33c3598e66
commit
149bdebf37
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -128,6 +128,7 @@ describe('FromAiParametersModal', () => {
|
|||
},
|
||||
[STORES.WORKFLOWS]: {
|
||||
workflow: mockWorkflow,
|
||||
workflowId: 'test-workflow',
|
||||
workflowExecutionData: mockRunData,
|
||||
},
|
||||
},
|
||||
|
|
|
|||
|
|
@ -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');
|
||||
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
});
|
||||
|
||||
|
|
|
|||
|
|
@ -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({
|
||||
|
|
|
|||
|
|
@ -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' },
|
||||
|
|
|
|||
|
|
@ -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));
|
||||
});
|
||||
|
||||
|
|
|
|||
|
|
@ -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 }],
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
|
|
|
|||
|
|
@ -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),
|
||||
);
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
});
|
||||
|
|
|
|||
|
|
@ -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',
|
||||
|
|
|
|||
|
|
@ -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: [] });
|
||||
|
|
|
|||
|
|
@ -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');
|
||||
|
||||
|
|
|
|||
|
|
@ -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',
|
||||
|
|
|
|||
|
|
@ -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'),
|
||||
);
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
});
|
||||
|
|
|
|||
|
|
@ -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(() => {
|
||||
|
|
|
|||
|
|
@ -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({
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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: {},
|
||||
|
|
|
|||
|
|
@ -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', () => {
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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({
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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 = [
|
||||
|
|
|
|||
|
|
@ -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) => {
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
});
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
|
|
|
|||
|
|
@ -14,6 +14,10 @@ import {
|
|||
useWorkflowDocumentStore,
|
||||
} from '@/app/stores/workflowDocument.store';
|
||||
|
||||
vi.mock('@/app/stores/workflows.store', () => ({
|
||||
useWorkflowsStore: vi.fn(() => ({ workflowId: '' })),
|
||||
}));
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Helpers
|
||||
// ---------------------------------------------------------------------------
|
||||
|
|
|
|||
|
|
@ -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', () => {
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -63,7 +63,7 @@ describe('useExecutionDebugging()', () => {
|
|||
mockWorkflowDocumentStore.getParentNodes.mockReturnValue([]);
|
||||
|
||||
const workflowStore = mockedStore(useWorkflowsStore);
|
||||
workflowStore.workflow.id = 'test-workflow';
|
||||
workflowStore.setWorkflowId('test-workflow');
|
||||
|
||||
toast = useToast();
|
||||
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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', () => {
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
|
|
|
|||
|
|
@ -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) {
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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',
|
||||
|
|
|
|||
|
|
@ -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: {
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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(
|
||||
|
|
|
|||
|
|
@ -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: {},
|
||||
},
|
||||
]);
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -75,7 +75,7 @@ describe('useRecentResources', () => {
|
|||
recentNodesRef.value = {};
|
||||
|
||||
mockWorkflowsStore = useWorkflowsStore();
|
||||
mockWorkflowsStore.workflow.id = 'workflow-1';
|
||||
mockWorkflowsStore.workflowId = 'workflow-1';
|
||||
mockWorkflowsListStore = useWorkflowsListStore();
|
||||
mockNodeTypesStore = useNodeTypesStore();
|
||||
|
||||
|
|
|
|||
|
|
@ -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 };
|
||||
|
||||
|
|
|
|||
|
|
@ -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));
|
||||
|
||||
|
|
|
|||
|
|
@ -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),
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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),
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
Loading…
Reference in New Issue
Block a user