diff --git a/packages/editor-ui/src/composables/useCanvasOperations.test.ts b/packages/editor-ui/src/composables/useCanvasOperations.test.ts index b3c9c06b15..3a90f05ff7 100644 --- a/packages/editor-ui/src/composables/useCanvasOperations.test.ts +++ b/packages/editor-ui/src/composables/useCanvasOperations.test.ts @@ -7,7 +7,7 @@ import type { } from 'n8n-workflow'; import { NodeConnectionType, NodeHelpers } from 'n8n-workflow'; import { useCanvasOperations } from '@/composables/useCanvasOperations'; -import type { CanvasNode } from '@/types'; +import type { CanvasConnection, CanvasNode } from '@/types'; import { CanvasConnectionMode } from '@/types'; import type { ICredentialsResponse, INodeUi, IWorkflowDb } from '@/Interface'; import { RemoveNodeCommand } from '@/models/history'; @@ -618,6 +618,48 @@ describe('useCanvasOperations', () => { const added = await addNodes(nodes, {}); expect(added.length).toBe(2); }); + + it('should mark UI state as dirty', async () => { + const workflowsStore = mockedStore(useWorkflowsStore); + const uiStore = mockedStore(useUIStore); + const nodeTypesStore = useNodeTypesStore(); + const nodeTypeName = 'type'; + const nodes = [mockNode({ name: 'Node 1', type: nodeTypeName, position: [30, 40] })]; + + workflowsStore.getCurrentWorkflow.mockReturnValue( + createTestWorkflowObject(workflowsStore.workflow), + ); + + nodeTypesStore.nodeTypes = { + [nodeTypeName]: { 1: mockNodeTypeDescription({ name: nodeTypeName }) }, + }; + + const { addNodes } = useCanvasOperations({ router }); + await addNodes(nodes, { keepPristine: false }); + + expect(uiStore.stateIsDirty).toEqual(true); + }); + + it('should not mark UI state as dirty if keepPristine is true', async () => { + const workflowsStore = mockedStore(useWorkflowsStore); + const uiStore = mockedStore(useUIStore); + const nodeTypesStore = useNodeTypesStore(); + const nodeTypeName = 'type'; + const nodes = [mockNode({ name: 'Node 1', type: nodeTypeName, position: [30, 40] })]; + + workflowsStore.getCurrentWorkflow.mockReturnValue( + createTestWorkflowObject(workflowsStore.workflow), + ); + + nodeTypesStore.nodeTypes = { + [nodeTypeName]: { 1: mockNodeTypeDescription({ name: nodeTypeName }) }, + }; + + const { addNodes } = useCanvasOperations({ router }); + await addNodes(nodes, { keepPristine: true }); + + expect(uiStore.stateIsDirty).toEqual(false); + }); }); describe('revertAddNode', () => { @@ -1013,6 +1055,26 @@ describe('useCanvasOperations', () => { ], }); }); + + it('should set UI state as dirty', async () => { + const uiStore = mockedStore(useUIStore); + const connections: CanvasConnection[] = []; + + const { addConnections } = useCanvasOperations({ router }); + await addConnections(connections, { keepPristine: false }); + + expect(uiStore.stateIsDirty).toBe(true); + }); + + it('should not set UI state as dirty if keepPristine is true', async () => { + const uiStore = mockedStore(useUIStore); + const connections: CanvasConnection[] = []; + + const { addConnections } = useCanvasOperations({ router }); + await addConnections(connections, { keepPristine: true }); + + expect(uiStore.stateIsDirty).toBe(false); + }); }); describe('createConnection', () => { @@ -1102,6 +1164,57 @@ describe('useCanvasOperations', () => { }); expect(uiStore.stateIsDirty).toBe(true); }); + + it('should not set UI state as dirty if keepPristine is true', () => { + const workflowsStore = mockedStore(useWorkflowsStore); + const uiStore = mockedStore(useUIStore); + const nodeTypesStore = mockedStore(useNodeTypesStore); + + const nodeTypeDescription = mockNodeTypeDescription({ + name: SET_NODE_TYPE, + inputs: [NodeConnectionType.Main], + outputs: [NodeConnectionType.Main], + }); + + const nodeA = createTestNode({ + id: 'a', + type: nodeTypeDescription.name, + name: 'Node A', + }); + + const nodeB = createTestNode({ + id: 'b', + type: nodeTypeDescription.name, + name: 'Node B', + }); + + const connection: Connection = { + source: nodeA.id, + sourceHandle: `outputs/${NodeConnectionType.Main}/0`, + target: nodeB.id, + targetHandle: `inputs/${NodeConnectionType.Main}/0`, + }; + + nodeTypesStore.nodeTypes = { + node: { 1: nodeTypeDescription }, + }; + + workflowsStore.workflow.nodes = [nodeA, nodeB]; + workflowsStore.getNodeById.mockReturnValueOnce(nodeA).mockReturnValueOnce(nodeB); + nodeTypesStore.getNodeType = vi.fn().mockReturnValue(nodeTypeDescription); + + const workflowObject = createTestWorkflowObject(workflowsStore.workflow); + workflowsStore.getCurrentWorkflow.mockReturnValue(workflowObject); + + const { createConnection, editableWorkflowObject } = useCanvasOperations({ router }); + + editableWorkflowObject.value.nodes[nodeA.name] = nodeA; + editableWorkflowObject.value.nodes[nodeB.name] = nodeB; + + createConnection(connection, { keepPristine: true }); + + expect(uiStore.stateIsDirty).toBe(false); + }); }); describe('revertCreateConnection', () => { diff --git a/packages/editor-ui/src/composables/useCanvasOperations.ts b/packages/editor-ui/src/composables/useCanvasOperations.ts index a27c4f99db..4b78da01b2 100644 --- a/packages/editor-ui/src/composables/useCanvasOperations.ts +++ b/packages/editor-ui/src/composables/useCanvasOperations.ts @@ -105,14 +105,23 @@ type AddNodeDataWithTypeVersion = AddNodeData & { typeVersion: INodeUi['typeVersion']; }; -type AddNodeOptions = { +type AddNodesBaseOptions = { dragAndDrop?: boolean; - openNDV?: boolean; trackHistory?: boolean; - isAutoAdd?: boolean; + keepPristine?: boolean; telemetry?: boolean; }; +type AddNodesOptions = AddNodesBaseOptions & { + position?: XYPosition; + trackBulk?: boolean; +}; + +type AddNodeOptions = AddNodesBaseOptions & { + openNDV?: boolean; + isAutoAdd?: boolean; +}; + export function useCanvasOperations({ router }: { router: ReturnType }) { const rootStore = useRootStore(); const workflowsStore = useWorkflowsStore(); @@ -479,17 +488,7 @@ export function useCanvasOperations({ router }: { router: ReturnType { workflowsStore.setNodePristine(nodeData.name, true); + if (!options.keepPristine) { + uiStore.stateIsDirty = true; + } + nodeHelpers.matchCredentials(nodeData); nodeHelpers.updateNodeParameterIssues(nodeData); nodeHelpers.updateNodeCredentialIssues(nodeData); @@ -1073,7 +1076,10 @@ export function useCanvasOperations({ router }: { router: ReturnType