diff --git a/cypress/e2e/19-execution.cy.ts b/cypress/e2e/19-execution.cy.ts index b2d7830962..a24d8894bf 100644 --- a/cypress/e2e/19-execution.cy.ts +++ b/cypress/e2e/19-execution.cy.ts @@ -489,4 +489,31 @@ describe('Execution', () => { .should('have.class', 'has-run'); }); }); + + it.only('should send proper payload for node rerun', () => { + cy.createFixtureWorkflow( + 'Multiple_trigger_node_rerun.json', + `Multiple trigger node rerun ${uuid()}`, + ); + + workflowPage.getters.zoomToFitButton().click(); + workflowPage.getters.executeWorkflowButton().click(); + + workflowPage.getters.clearExecutionDataButton().should('be.visible'); + + cy.intercept('POST', '/rest/workflows/run').as('workflowRun'); + + workflowPage.getters + .canvasNodeByName('do something with them') + .findChildByTestId('execute-node-button') + .click({ force: true }); + + cy.wait('@workflowRun').then((interception) => { + expect(interception.request.body).to.have.property('runData').that.is.an('object'); + const expectedKeys = ['When clicking "Test workflow"', 'fetch 5 random users']; + + expect(Object.keys(interception.request.body.runData)).to.have.lengthOf(expectedKeys.length); + expect(interception.request.body.runData).to.include.all.keys(expectedKeys); + }); + }); }); diff --git a/cypress/fixtures/Multiple_trigger_node_rerun.json b/cypress/fixtures/Multiple_trigger_node_rerun.json new file mode 100644 index 0000000000..39d231a894 --- /dev/null +++ b/cypress/fixtures/Multiple_trigger_node_rerun.json @@ -0,0 +1,133 @@ +{ + "name": "Multiple trigger node rerun", + "nodes": [ + { + "parameters": {}, + "id": "5ae8991f-08a2-4b27-b61c-85e3b8a83693", + "name": "When clicking \"Test workflow\"", + "type": "n8n-nodes-base.manualTrigger", + "typeVersion": 1, + "position": [ + 460, + 460 + ] + }, + { + "parameters": { + "url": "https://random-data-api.com/api/v2/users?size=5", + "options": {} + }, + "id": "22511d75-ab54-49e1-b8af-08b8b3372373", + "name": "fetch 5 random users", + "type": "n8n-nodes-base.httpRequest", + "typeVersion": 4.1, + "position": [ + 680, + 460 + ] + }, + { + "parameters": { + "jsCode": "// Loop over input items and add a new field called 'myNewField' to the JSON of each one\nfor (const item of $input.all()) {\n item.json.first_name_reversed = item.json = {\n firstName: item.json.first_name,\n firstnNameReversed: item.json.first_name_BUG.split(\"\").reverse().join(\"\")\n };\n}\n\nreturn $input.all();" + }, + "id": "4b66b15a-1685-46c1-a5e3-ebf8cdb11d21", + "name": "do something with them", + "type": "n8n-nodes-base.code", + "typeVersion": 2, + "position": [ + 900, + 460 + ] + }, + { + "parameters": { + "rule": { + "interval": [ + { + "field": "cronExpression", + "expression": "* * * * *" + } + ] + } + }, + "id": "d763fc3b-6c4a-4d39-8857-ff84f7b6dc83", + "name": "Schedule Trigger", + "type": "n8n-nodes-base.scheduleTrigger", + "typeVersion": 1.1, + "position": [ + 460, + 660 + ] + } + ], + "pinData": { + "Schedule Trigger": [ + { + "json": { + "timestamp": "2024-01-29T13:45:00.006+01:00", + "Readable date": "January 29th 2024, 1:45:00 pm", + "Readable time": "1:45:00 pm", + "Day of week": "Monday", + "Year": "2024", + "Month": "January", + "Day of month": "29", + "Hour": "13", + "Minute": "45", + "Second": "00", + "Timezone": "CET +01:00" + } + } + ], + "When clicking \"Test workflow\"": [ + { + "json": {} + } + ] + }, + "connections": { + "When clicking \"Test workflow\"": { + "main": [ + [ + { + "node": "fetch 5 random users", + "type": "main", + "index": 0 + } + ] + ] + }, + "fetch 5 random users": { + "main": [ + [ + { + "node": "do something with them", + "type": "main", + "index": 0 + } + ] + ] + }, + "Schedule Trigger": { + "main": [ + [ + { + "node": "fetch 5 random users", + "type": "main", + "index": 0 + } + ] + ] + } + }, + "active": false, + "settings": { + "executionOrder": "v1" + }, + "versionId": "b9a6c3b0-15cd-4359-a92e-12a691a36b7b", + "meta": { + "templateCredsSetupCompleted": true, + "instanceId": "8a47b83b4479b11330fdf21ccc96d4a8117035a968612e452b4c87bfd09c16c7" + }, + "id": "PymcwIrbqgNh3O0K", + "tags": [] +} \ No newline at end of file diff --git a/packages/editor-ui/src/mixins/workflowRun.ts b/packages/editor-ui/src/mixins/workflowRun.ts index fbc725f16b..17015a294e 100644 --- a/packages/editor-ui/src/mixins/workflowRun.ts +++ b/packages/editor-ui/src/mixins/workflowRun.ts @@ -7,7 +7,9 @@ import type { IRunData, IRunExecutionData, ITaskData, + IPinData, IWorkflowBase, + Workflow, } from 'n8n-workflow'; import { NodeHelpers, @@ -29,6 +31,55 @@ import { useExternalHooks } from '@/composables/useExternalHooks'; import { useWorkflowHelpers } from '@/composables/useWorkflowHelpers'; import { useRouter } from 'vue-router'; +export const consolidateRunDataAndStartNodes = ( + directParentNodes: string[], + runData: IRunData | null, + pinData: IPinData | undefined, + workflow: Workflow, +): { runData: IRunData | undefined; startNodes: string[] } => { + const startNodes: string[] = []; + let newRunData: IRunData | undefined; + + if (runData !== null && Object.keys(runData).length !== 0) { + newRunData = {}; + // Go over the direct parents of the node + for (const directParentNode of directParentNodes) { + // Go over the parents of that node so that we can get a start + // node for each of the branches + const parentNodes = workflow.getParentNodes(directParentNode, NodeConnectionType.Main); + + // Add also the enabled direct parent to be checked + if (workflow.nodes[directParentNode].disabled) continue; + + parentNodes.push(directParentNode); + + for (const parentNode of parentNodes) { + if ( + (runData[parentNode] === undefined || runData[parentNode].length === 0) && + pinData?.[parentNode].length === 0 + ) { + // When we hit a node which has no data we stop and set it + // as a start node the execution from and then go on with other + // direct input nodes + startNodes.push(parentNode); + break; + } + if (runData[parentNode] !== undefined) { + newRunData[parentNode] = runData[parentNode]?.slice(0, 1); + } + } + } + + if (Object.keys(newRunData).length === 0) { + // If there is no data for any of the parent nodes make sure + // that run data is empty that it runs regularly + newRunData = undefined; + } + } + + return { runData: newRunData, startNodes }; +}; + export const workflowRun = defineComponent({ setup() { const nodeHelpers = useNodeHelpers(); @@ -181,43 +232,21 @@ export const workflowRun = defineComponent({ const runData = this.workflowsStore.getWorkflowRunData; - let newRunData: IRunData | undefined; - - const startNodes: string[] = []; - - if (runData !== null && Object.keys(runData).length !== 0) { - newRunData = {}; - - // Go over the direct parents of the node - for (const directParentNode of directParentNodes) { - // Go over the parents of that node so that we can get a start - // node for each of the branches - const parentNodes = workflow.getParentNodes(directParentNode, NodeConnectionType.Main); - - // Add also the enabled direct parent to be checked - if (workflow.nodes[directParentNode].disabled) continue; - - parentNodes.push(directParentNode); - - for (const parentNode of parentNodes) { - if (runData[parentNode] === undefined || runData[parentNode].length === 0) { - // When we hit a node which has no data we stop and set it - // as a start node the execution from and then go on with other - // direct input nodes - startNodes.push(parentNode); - break; - } - newRunData[parentNode] = runData[parentNode].slice(0, 1); - } - } - - if (Object.keys(newRunData).length === 0) { - // If there is no data for any of the parent nodes make sure - // that run data is empty that it runs regularly - newRunData = undefined; - } + if (this.workflowsStore.isNewWorkflow) { + await this.workflowHelpers.saveCurrentWorkflow(); } + const workflowData = await this.workflowHelpers.getWorkflowDataToSave(); + + const consolidatedData = consolidateRunDataAndStartNodes( + directParentNodes, + runData, + workflowData.pinData, + workflow, + ); + + const { startNodes } = consolidatedData; + let { runData: newRunData } = consolidatedData; let executedNode: string | undefined; if ( startNodes.length === 0 && @@ -236,12 +265,6 @@ export const workflowRun = defineComponent({ executedNode = options.triggerNode; } - if (this.workflowsStore.isNewWorkflow) { - await this.workflowHelpers.saveCurrentWorkflow(); - } - - const workflowData = await this.workflowHelpers.getWorkflowDataToSave(); - const startRunData: IStartRunData = { workflowData, runData: newRunData,