mirror of
https://github.com/n8n-io/n8n.git
synced 2026-10-12 02:19:43 +00:00
fix(editor): Sort loop node outputs by execution order in setup panel (#27418)
Co-authored-by: Claude Opus 4.6 (1M context) <[email protected]>
This commit is contained in:
co-authored by
Claude Opus 4.6
parent
e04dddcbcc
commit
efa5d02979
@@ -487,6 +487,73 @@ describe('workflowUtils', () => {
|
||||
]);
|
||||
});
|
||||
|
||||
it('should process loop-body output before done output for loop nodes', () => {
|
||||
// Trigger → Loop Over Items → (done) → Wait
|
||||
// → (loop) → HTTP Request → (back to Loop)
|
||||
const trigger = makeNode('Trigger', [0, 0], true);
|
||||
const loop = makeNode('Loop', [100, 0]);
|
||||
const wait = makeNode('Wait', [200, -100]);
|
||||
const httpRequest = makeNode('HTTP Request', [200, 100]);
|
||||
|
||||
const connections = {
|
||||
Trigger: { main: [[{ node: 'Loop', type: 'main' as const, index: 0 }]] },
|
||||
Loop: {
|
||||
main: [
|
||||
// output 0 = "done"
|
||||
[{ node: 'Wait', type: 'main' as const, index: 0 }],
|
||||
// output 1 = "loop"
|
||||
[{ node: 'HTTP Request', type: 'main' as const, index: 0 }],
|
||||
],
|
||||
},
|
||||
'HTTP Request': { main: [[{ node: 'Loop', type: 'main' as const, index: 0 }]] },
|
||||
};
|
||||
|
||||
const nodeTypes = { Loop: 'n8n-nodes-base.splitInBatches' };
|
||||
|
||||
const result = sortNodesByExecutionOrder(
|
||||
[wait, httpRequest, loop, trigger],
|
||||
connections,
|
||||
{},
|
||||
nodeTypes,
|
||||
);
|
||||
|
||||
// Loop body (HTTP Request) should come before done branch (Wait)
|
||||
expect(result.map((n) => n.node.name)).toEqual(['Trigger', 'Loop', 'HTTP Request', 'Wait']);
|
||||
});
|
||||
|
||||
it('should handle loop node with multiple nodes in the loop body', () => {
|
||||
// Trigger → Loop → (done) → Done Node
|
||||
// → (loop) → Step1 → Step2 → (back to Loop)
|
||||
const trigger = makeNode('Trigger', [0, 0], true);
|
||||
const loop = makeNode('Loop', [100, 0]);
|
||||
const doneNode = makeNode('Done', [200, -100]);
|
||||
const step1 = makeNode('Step1', [200, 100]);
|
||||
const step2 = makeNode('Step2', [300, 100]);
|
||||
|
||||
const connections = {
|
||||
Trigger: { main: [[{ node: 'Loop', type: 'main' as const, index: 0 }]] },
|
||||
Loop: {
|
||||
main: [
|
||||
[{ node: 'Done', type: 'main' as const, index: 0 }],
|
||||
[{ node: 'Step1', type: 'main' as const, index: 0 }],
|
||||
],
|
||||
},
|
||||
Step1: { main: [[{ node: 'Step2', type: 'main' as const, index: 0 }]] },
|
||||
Step2: { main: [[{ node: 'Loop', type: 'main' as const, index: 0 }]] },
|
||||
};
|
||||
|
||||
const nodeTypes = { Loop: 'n8n-nodes-base.splitInBatches' };
|
||||
|
||||
const result = sortNodesByExecutionOrder(
|
||||
[doneNode, step2, step1, loop, trigger],
|
||||
connections,
|
||||
{},
|
||||
nodeTypes,
|
||||
);
|
||||
|
||||
expect(result.map((n) => n.node.name)).toEqual(['Trigger', 'Loop', 'Step1', 'Step2', 'Done']);
|
||||
});
|
||||
|
||||
it('should not follow main connections from connectionsByDestinationNode', () => {
|
||||
const trigger = makeNode('Trigger', [0, 0], true);
|
||||
const nodeA = makeNode('A', [100, 0]);
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import type { IWorkflowDb, INodeUi } from '@/Interface';
|
||||
import type { ITag } from '@n8n/rest-api-client/api/tags';
|
||||
import type { IConnections } from 'n8n-workflow';
|
||||
import { SPLIT_IN_BATCHES_NODE_TYPE } from '@/app/constants';
|
||||
|
||||
/**
|
||||
* Converts workflow tags from ITag[] (API response format) to string[] (store format)
|
||||
@@ -66,6 +67,7 @@ export function sortNodesByExecutionOrder<T extends ExecutionOrderItem>(
|
||||
nodes: T[],
|
||||
connectionsBySourceNode: IConnections,
|
||||
connectionsByDestinationNode: IConnections = {},
|
||||
nodeTypes: Record<string, string> = {},
|
||||
): T[] {
|
||||
const triggers = nodes
|
||||
.filter((item) => item.isTrigger)
|
||||
@@ -118,9 +120,23 @@ export function sortNodesByExecutionOrder<T extends ExecutionOrderItem>(
|
||||
// main chain out of order when sub-nodes are shared across multiple agents.
|
||||
const sourceConns = connectionsBySourceNode[name];
|
||||
if (sourceConns?.main) {
|
||||
for (const outputs of sourceConns.main) {
|
||||
for (const conn of outputs ?? []) {
|
||||
dfs(conn.node);
|
||||
const mainOutputs = sourceConns.main;
|
||||
|
||||
// Loop Over Items (SplitInBatches) V3: output 0 = "done", output 1 = "loop".
|
||||
// The execution engine runs the loop body first (repeatedly) and only sends
|
||||
// data to "done" after all iterations complete. Process output 1 before
|
||||
// output 0 so the sorted order matches execution order.
|
||||
if (nodeTypes[name] === SPLIT_IN_BATCHES_NODE_TYPE && mainOutputs.length > 1) {
|
||||
for (const idx of [1, 0]) {
|
||||
for (const conn of mainOutputs[idx] ?? []) {
|
||||
dfs(conn.node);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
for (const outputs of mainOutputs) {
|
||||
for (const conn of outputs ?? []) {
|
||||
dfs(conn.node);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+6
@@ -336,10 +336,16 @@ export const useWorkflowSetupState = (
|
||||
nodeHasTemplateParams(node.name),
|
||||
);
|
||||
|
||||
const allNodeTypes: Record<string, string> = {};
|
||||
for (const node of sourceNodes.value) {
|
||||
allNodeTypes[node.name] = node.type;
|
||||
}
|
||||
|
||||
return sortNodesByExecutionOrder(
|
||||
nodesForSetup,
|
||||
workflowsStore.connectionsBySourceNode,
|
||||
workflowsStore.connectionsByDestinationNode,
|
||||
allNodeTypes,
|
||||
);
|
||||
});
|
||||
|
||||
|
||||
Reference in New Issue
Block a user