From 1daf0ff169468c9afde1ab3f37b241426cfd1db3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ra=C3=BAl=20G=C3=B3mez=20Morales?= Date: Thu, 29 May 2025 12:03:15 +0200 Subject: [PATCH] fix(editor): Use last task data for calculating the current state (#15546) --- .../src/composables/useCanvasMapping.test.ts | 588 ++++++++++-------- .../src/composables/useCanvasMapping.ts | 12 +- 2 files changed, 331 insertions(+), 269 deletions(-) diff --git a/packages/frontend/editor-ui/src/composables/useCanvasMapping.test.ts b/packages/frontend/editor-ui/src/composables/useCanvasMapping.test.ts index 036cbf857d..75ee5be9a9 100644 --- a/packages/frontend/editor-ui/src/composables/useCanvasMapping.test.ts +++ b/packages/frontend/editor-ui/src/composables/useCanvasMapping.test.ts @@ -265,298 +265,298 @@ describe('useCanvasMapping', () => { }), ); }); + }); - describe('render', () => { - it('should handle render options for default node type', () => { - const manualTriggerNode = mockNode({ - name: 'Manual Trigger', - type: MANUAL_TRIGGER_NODE_TYPE, - disabled: false, - }); - const nodes = [manualTriggerNode]; - const connections = {}; - const workflowObject = createTestWorkflowObject({ - nodes, - connections, - }); - - const { nodes: mappedNodes } = useCanvasMapping({ - nodes: ref(nodes), - connections: ref(connections), - workflowObject: ref(workflowObject) as Ref, - }); - - const rootStore = mockedStore(useRootStore); - rootStore.baseUrl = 'http://test.local/'; - - expect(mappedNodes.value[0]?.data?.render).toEqual({ - type: CanvasNodeRenderType.Default, - options: { - configurable: false, - configuration: false, - trigger: true, - icon: { - src: 'http://test.local/nodes/test-node/icon.svg', - type: 'file', - }, - inputs: { - labelSize: 'small', - }, - outputs: { - labelSize: 'small', - }, - }, - }); + describe('render', () => { + it('should handle render options for default node type', () => { + const manualTriggerNode = mockNode({ + name: 'Manual Trigger', + type: MANUAL_TRIGGER_NODE_TYPE, + disabled: false, + }); + const nodes = [manualTriggerNode]; + const connections = {}; + const workflowObject = createTestWorkflowObject({ + nodes, + connections, }); - it('should handle render options for addNodes node type', () => { - const addNodesNode = mockNode({ - name: CanvasNodeRenderType.AddNodes, - type: CanvasNodeRenderType.AddNodes, - disabled: false, - }); - const nodes = [addNodesNode]; - const connections = {}; - const workflowObject = createTestWorkflowObject({ - nodes: [], - connections, - }); - - const { nodes: mappedNodes } = useCanvasMapping({ - nodes: ref(nodes), - connections: ref(connections), - workflowObject: ref(workflowObject) as Ref, - }); - - expect(mappedNodes.value[0]?.data?.render).toEqual({ - type: CanvasNodeRenderType.AddNodes, - options: {}, - }); + const { nodes: mappedNodes } = useCanvasMapping({ + nodes: ref(nodes), + connections: ref(connections), + workflowObject: ref(workflowObject) as Ref, }); - it('should handle render options for stickyNote node type', () => { - const stickyNoteNode = mockNode({ - name: 'Sticky', - type: STICKY_NODE_TYPE, - disabled: false, - parameters: { - width: 200, - height: 200, - color: 3, - content: '# Hello world', + const rootStore = mockedStore(useRootStore); + rootStore.baseUrl = 'http://test.local/'; + + expect(mappedNodes.value[0]?.data?.render).toEqual({ + type: CanvasNodeRenderType.Default, + options: { + configurable: false, + configuration: false, + trigger: true, + icon: { + src: 'http://test.local/nodes/test-node/icon.svg', + type: 'file', }, - }); - const nodes = [stickyNoteNode]; - const connections = {}; - const workflowObject = createTestWorkflowObject({ - nodes, - connections, - }); - - const { nodes: mappedNodes } = useCanvasMapping({ - nodes: ref(nodes), - connections: ref(connections), - workflowObject: ref(workflowObject) as Ref, - }); - - expect(mappedNodes.value[0]?.data?.render).toEqual({ - type: CanvasNodeRenderType.StickyNote, - options: stickyNoteNode.parameters, - }); + inputs: { + labelSize: 'small', + }, + outputs: { + labelSize: 'small', + }, + }, }); }); - describe('runData', () => { - describe('nodeExecutionRunDataOutputMapById', () => { - it('should return an empty object when there is no run data', () => { - const workflowsStore = mockedStore(useWorkflowsStore); - const nodes: INodeUi[] = []; - const connections = {}; - const workflowObject = createTestWorkflowObject({ - nodes, - connections, - }); + it('should handle render options for addNodes node type', () => { + const addNodesNode = mockNode({ + name: CanvasNodeRenderType.AddNodes, + type: CanvasNodeRenderType.AddNodes, + disabled: false, + }); + const nodes = [addNodesNode]; + const connections = {}; + const workflowObject = createTestWorkflowObject({ + nodes: [], + connections, + }); - workflowsStore.getWorkflowResultDataByNodeName.mockReturnValue(null); + const { nodes: mappedNodes } = useCanvasMapping({ + nodes: ref(nodes), + connections: ref(connections), + workflowObject: ref(workflowObject) as Ref, + }); - const { nodeExecutionRunDataOutputMapById } = useCanvasMapping({ - nodes: ref(nodes), - connections: ref(connections), - workflowObject: ref(workflowObject) as Ref, - }); + expect(mappedNodes.value[0]?.data?.render).toEqual({ + type: CanvasNodeRenderType.AddNodes, + options: {}, + }); + }); - expect(nodeExecutionRunDataOutputMapById.value).toEqual({}); + it('should handle render options for stickyNote node type', () => { + const stickyNoteNode = mockNode({ + name: 'Sticky', + type: STICKY_NODE_TYPE, + disabled: false, + parameters: { + width: 200, + height: 200, + color: 3, + content: '# Hello world', + }, + }); + const nodes = [stickyNoteNode]; + const connections = {}; + const workflowObject = createTestWorkflowObject({ + nodes, + connections, + }); + + const { nodes: mappedNodes } = useCanvasMapping({ + nodes: ref(nodes), + connections: ref(connections), + workflowObject: ref(workflowObject) as Ref, + }); + + expect(mappedNodes.value[0]?.data?.render).toEqual({ + type: CanvasNodeRenderType.StickyNote, + options: stickyNoteNode.parameters, + }); + }); + }); + + describe('runData', () => { + describe('nodeExecutionRunDataOutputMapById', () => { + it('should return an empty object when there is no run data', () => { + const workflowsStore = mockedStore(useWorkflowsStore); + const nodes: INodeUi[] = []; + const connections = {}; + const workflowObject = createTestWorkflowObject({ + nodes, + connections, }); - it('should calculate iterations and total correctly for single node', () => { - const workflowsStore = mockedStore(useWorkflowsStore); - const nodes = [createTestNode({ name: 'Node 1' })]; - const connections = {}; - const workflowObject = createTestWorkflowObject({ - nodes, - connections, - }); + workflowsStore.getWorkflowResultDataByNodeName.mockReturnValue(null); - workflowsStore.getWorkflowResultDataByNodeName.mockReturnValue([ - { - startTime: 0, - executionTime: 0, - executionIndex: 0, - source: [], - data: { - [NodeConnectionTypes.Main]: [[{ json: {} }, { json: {} }]], - }, - }, - ]); - - const { nodeExecutionRunDataOutputMapById } = useCanvasMapping({ - nodes: ref(nodes), - connections: ref(connections), - workflowObject: ref(workflowObject) as Ref, - }); - - expect(nodeExecutionRunDataOutputMapById.value).toEqual({ - [nodes[0].id]: { - [NodeConnectionTypes.Main]: { - 0: { - iterations: 1, - total: 2, - }, - }, - }, - }); + const { nodeExecutionRunDataOutputMapById } = useCanvasMapping({ + nodes: ref(nodes), + connections: ref(connections), + workflowObject: ref(workflowObject) as Ref, }); - it('should handle multiple nodes with different connection types', () => { - const workflowsStore = mockedStore(useWorkflowsStore); - const nodes = [ - createTestNode({ id: 'node1', name: 'Node 1' }), - createTestNode({ id: 'node2', name: 'Node 2' }), - ]; - const connections = {}; - const workflowObject = createTestWorkflowObject({ - nodes, - connections, - }); + expect(nodeExecutionRunDataOutputMapById.value).toEqual({}); + }); - workflowsStore.getWorkflowResultDataByNodeName.mockImplementation((nodeName: string) => { - if (nodeName === 'Node 1') { - return [ - { - startTime: 0, - executionTime: 0, - executionIndex: 0, - source: [], - data: { - [NodeConnectionTypes.Main]: [[{ json: {} }]], - [NodeConnectionTypes.AiAgent]: [[{ json: {} }, { json: {} }]], - }, - }, - ]; - } else if (nodeName === 'Node 2') { - return [ - { - startTime: 0, - executionTime: 0, - executionIndex: 0, - source: [], - data: { - [NodeConnectionTypes.Main]: [[{ json: {} }, { json: {} }, { json: {} }]], - }, - }, - ]; - } - - return null; - }); - - const { nodeExecutionRunDataOutputMapById } = useCanvasMapping({ - nodes: ref(nodes), - connections: ref(connections), - workflowObject: ref(workflowObject) as Ref, - }); - - expect(nodeExecutionRunDataOutputMapById.value).toEqual({ - node1: { - [NodeConnectionTypes.Main]: { - 0: { - iterations: 1, - total: 1, - }, - }, - [NodeConnectionTypes.AiAgent]: { - 0: { - iterations: 1, - total: 2, - }, - }, - }, - node2: { - [NodeConnectionTypes.Main]: { - 0: { - iterations: 1, - total: 3, - }, - }, - }, - }); + it('should calculate iterations and total correctly for single node', () => { + const workflowsStore = mockedStore(useWorkflowsStore); + const nodes = [createTestNode({ name: 'Node 1' })]; + const connections = {}; + const workflowObject = createTestWorkflowObject({ + nodes, + connections, }); - it('handles multiple iterations correctly', () => { - const workflowsStore = mockedStore(useWorkflowsStore); - const nodes = [createTestNode({ name: 'Node 1' })]; - const connections = {}; - const workflowObject = createTestWorkflowObject({ - nodes, - connections, - }); + workflowsStore.getWorkflowResultDataByNodeName.mockReturnValue([ + { + startTime: 0, + executionTime: 0, + executionIndex: 0, + source: [], + data: { + [NodeConnectionTypes.Main]: [[{ json: {} }, { json: {} }]], + }, + }, + ]); - workflowsStore.getWorkflowResultDataByNodeName.mockReturnValue([ - { - startTime: 0, - executionTime: 0, - executionIndex: 0, - source: [], - data: { - [NodeConnectionTypes.Main]: [[{ json: {} }]], + const { nodeExecutionRunDataOutputMapById } = useCanvasMapping({ + nodes: ref(nodes), + connections: ref(connections), + workflowObject: ref(workflowObject) as Ref, + }); + + expect(nodeExecutionRunDataOutputMapById.value).toEqual({ + [nodes[0].id]: { + [NodeConnectionTypes.Main]: { + 0: { + iterations: 1, + total: 2, }, }, - { - startTime: 0, - executionTime: 0, - executionIndex: 1, - source: [], - data: { - [NodeConnectionTypes.Main]: [[{ json: {} }, { json: {} }, { json: {} }]], - }, - }, - { - startTime: 0, - executionTime: 0, - executionIndex: 2, - source: [], - data: { - [NodeConnectionTypes.Main]: [[{ json: {} }, { json: {} }]], - }, - }, - ]); + }, + }); + }); - const { nodeExecutionRunDataOutputMapById } = useCanvasMapping({ - nodes: ref(nodes), - connections: ref(connections), - workflowObject: ref(workflowObject) as Ref, - }); + it('should handle multiple nodes with different connection types', () => { + const workflowsStore = mockedStore(useWorkflowsStore); + const nodes = [ + createTestNode({ id: 'node1', name: 'Node 1' }), + createTestNode({ id: 'node2', name: 'Node 2' }), + ]; + const connections = {}; + const workflowObject = createTestWorkflowObject({ + nodes, + connections, + }); - expect(nodeExecutionRunDataOutputMapById.value).toEqual({ - [nodes[0].id]: { - [NodeConnectionTypes.Main]: { - 0: { - iterations: 3, - total: 6, + workflowsStore.getWorkflowResultDataByNodeName.mockImplementation((nodeName: string) => { + if (nodeName === 'Node 1') { + return [ + { + startTime: 0, + executionTime: 0, + executionIndex: 0, + source: [], + data: { + [NodeConnectionTypes.Main]: [[{ json: {} }]], + [NodeConnectionTypes.AiAgent]: [[{ json: {} }, { json: {} }]], }, }, + ]; + } else if (nodeName === 'Node 2') { + return [ + { + startTime: 0, + executionTime: 0, + executionIndex: 0, + source: [], + data: { + [NodeConnectionTypes.Main]: [[{ json: {} }, { json: {} }, { json: {} }]], + }, + }, + ]; + } + + return null; + }); + + const { nodeExecutionRunDataOutputMapById } = useCanvasMapping({ + nodes: ref(nodes), + connections: ref(connections), + workflowObject: ref(workflowObject) as Ref, + }); + + expect(nodeExecutionRunDataOutputMapById.value).toEqual({ + node1: { + [NodeConnectionTypes.Main]: { + 0: { + iterations: 1, + total: 1, + }, }, - }); + [NodeConnectionTypes.AiAgent]: { + 0: { + iterations: 1, + total: 2, + }, + }, + }, + node2: { + [NodeConnectionTypes.Main]: { + 0: { + iterations: 1, + total: 3, + }, + }, + }, + }); + }); + + it('handles multiple iterations correctly', () => { + const workflowsStore = mockedStore(useWorkflowsStore); + const nodes = [createTestNode({ name: 'Node 1' })]; + const connections = {}; + const workflowObject = createTestWorkflowObject({ + nodes, + connections, + }); + + workflowsStore.getWorkflowResultDataByNodeName.mockReturnValue([ + { + startTime: 0, + executionTime: 0, + executionIndex: 0, + source: [], + data: { + [NodeConnectionTypes.Main]: [[{ json: {} }]], + }, + }, + { + startTime: 0, + executionTime: 0, + executionIndex: 1, + source: [], + data: { + [NodeConnectionTypes.Main]: [[{ json: {} }, { json: {} }, { json: {} }]], + }, + }, + { + startTime: 0, + executionTime: 0, + executionIndex: 2, + source: [], + data: { + [NodeConnectionTypes.Main]: [[{ json: {} }, { json: {} }]], + }, + }, + ]); + + const { nodeExecutionRunDataOutputMapById } = useCanvasMapping({ + nodes: ref(nodes), + connections: ref(connections), + workflowObject: ref(workflowObject) as Ref, + }); + + expect(nodeExecutionRunDataOutputMapById.value).toEqual({ + [nodes[0].id]: { + [NodeConnectionTypes.Main]: { + 0: { + iterations: 3, + total: 6, + }, + }, + }, }); }); }); @@ -621,7 +621,9 @@ describe('useCanvasMapping', () => { expect(additionalNodePropertiesById.value[nodes[0].id]).toEqual({ style: { zIndex: -100 }, }); - expect(additionalNodePropertiesById.value[nodes[1].id]).toEqual({ style: { zIndex: -99 } }); + expect(additionalNodePropertiesById.value[nodes[1].id]).toEqual({ + style: { zIndex: -99 }, + }); }); it('should calculate zIndex correctly for overlapping sticky nodes', () => { @@ -1141,6 +1143,62 @@ describe('useCanvasMapping', () => { expect(nodeHasIssuesById.value[node2.id]).toBe(true); // Has error status expect(nodeHasIssuesById.value[node3.id]).toBe(false); // No issues }); + + it('should handle node validation issues', () => { + const node1 = createTestNode({ + name: 'Node 1', + issues: { + parameters: { + formTitle: ['Parameter "Form Title" is required.'], + }, + }, + } as Partial); + const nodes = [node1]; + const connections = {}; + const workflowObject = createTestWorkflowObject({ nodes, connections }); + + const { nodeHasIssuesById } = useCanvasMapping({ + nodes: ref(nodes), + connections: ref(connections), + workflowObject: ref(workflowObject) as Ref, + }); + expect(nodeHasIssuesById.value[node1.id]).toBe(true); // Has error status + }); + + it('should handle successful executions after errors', () => { + const workflowsStore = mockedStore(useWorkflowsStore); + const node1 = createTestNode({ name: 'Node 2' }); + const nodes = [node1]; + const connections = {}; + const workflowObject = createTestWorkflowObject({ nodes, connections }); + + workflowsStore.getWorkflowRunData = { + 'Node 2': [ + { + startTime: 0, + executionTime: 0, + executionIndex: 0, + source: [], + executionStatus: 'error', + }, + { + startTime: 0, + executionTime: 0, + executionIndex: 0, + source: [], + executionStatus: 'success', + }, + ], + }; + + const { nodeHasIssuesById } = useCanvasMapping({ + nodes: ref(nodes), + connections: ref(connections), + workflowObject: ref(workflowObject) as Ref, + }); + + expect(nodeHasIssuesById.value[node1.id]).toBe(false); // Last run was successful + }); }); }); diff --git a/packages/frontend/editor-ui/src/composables/useCanvasMapping.ts b/packages/frontend/editor-ui/src/composables/useCanvasMapping.ts index 6bd9d4a9ea..a7098c4b80 100644 --- a/packages/frontend/editor-ui/src/composables/useCanvasMapping.ts +++ b/packages/frontend/editor-ui/src/composables/useCanvasMapping.ts @@ -329,9 +329,9 @@ export function useCanvasMapping({ const nodeExecutionStatusById = computed(() => nodes.value.reduce>((acc, node) => { - acc[node.id] = - workflowsStore.getWorkflowRunData?.[node.name]?.filter(Boolean)[0]?.executionStatus ?? - 'new'; + const tasks = workflowsStore.getWorkflowRunData?.[node.name] ?? []; + + acc[node.id] = tasks.at(-1)?.executionStatus ?? 'new'; return acc; }, {}), ); @@ -406,8 +406,12 @@ export function useCanvasMapping({ acc[node.id] = true; } else if (nodePinnedDataById.value[node.id]) { acc[node.id] = false; + } else if (node.issues && nodeHelpers.nodeIssuesToString(node.issues, node).length) { + acc[node.id] = true; } else { - acc[node.id] = nodeIssuesById.value[node.id].length > 0; + const tasks = workflowsStore.getWorkflowRunData?.[node.name] ?? []; + + acc[node.id] = Boolean(tasks.at(-1)?.error); } return acc;