mirror of
https://github.com/n8n-io/n8n.git
synced 2025-02-21 02:56:40 -08:00
fix(editor): Use correct connection index when connecting adjancent nodes after deleting a node (#12973)
Some checks failed
Test Master / install-and-build (push) Waiting to run
Test Master / Unit tests (18.x) (push) Blocked by required conditions
Test Master / Unit tests (20.x) (push) Blocked by required conditions
Test Master / Unit tests (22.4) (push) Blocked by required conditions
Test Master / Lint (push) Blocked by required conditions
Test Master / Notify Slack on failure (push) Blocked by required conditions
Benchmark Docker Image CI / build (push) Has been cancelled
Some checks failed
Test Master / install-and-build (push) Waiting to run
Test Master / Unit tests (18.x) (push) Blocked by required conditions
Test Master / Unit tests (20.x) (push) Blocked by required conditions
Test Master / Unit tests (22.4) (push) Blocked by required conditions
Test Master / Lint (push) Blocked by required conditions
Test Master / Notify Slack on failure (push) Blocked by required conditions
Benchmark Docker Image CI / build (push) Has been cancelled
This commit is contained in:
parent
2774f35969
commit
c7a15d5980
|
@ -2624,6 +2624,204 @@ describe('useCanvasOperations', () => {
|
||||||
expect(workflowsStore.setWorkflowPinData).toHaveBeenCalledWith({});
|
expect(workflowsStore.setWorkflowPinData).toHaveBeenCalledWith({});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
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);
|
||||||
|
|
||||||
|
// Create three nodes in a sequence: A -> B -> C
|
||||||
|
const nodeA = createTestNode({ id: 'A', name: 'Node A', position: [0, 0] });
|
||||||
|
const nodeB = createTestNode({ id: 'B', name: 'Node B', position: [100, 0] });
|
||||||
|
const nodeC = createTestNode({ id: 'C', name: 'Node C', position: [200, 0] });
|
||||||
|
|
||||||
|
const nodeTypeDescription = mockNodeTypeDescription({
|
||||||
|
name: nodeA.type,
|
||||||
|
inputs: [NodeConnectionType.Main],
|
||||||
|
outputs: [NodeConnectionType.Main],
|
||||||
|
});
|
||||||
|
|
||||||
|
nodeTypesStore.getNodeType = vi.fn(() => nodeTypeDescription);
|
||||||
|
|
||||||
|
// Set up the workflow connections A -> B -> C
|
||||||
|
workflowsStore.workflow.nodes = [nodeA, nodeB, nodeC];
|
||||||
|
workflowsStore.workflow.connections = {
|
||||||
|
[nodeA.name]: {
|
||||||
|
main: [[{ node: nodeB.name, type: NodeConnectionType.Main, index: 0 }]],
|
||||||
|
},
|
||||||
|
[nodeB.name]: {
|
||||||
|
main: [[{ node: nodeC.name, type: NodeConnectionType.Main, index: 0 }]],
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
// Mock store methods
|
||||||
|
const workflowObject = createTestWorkflowObject(workflowsStore.workflow);
|
||||||
|
workflowsStore.getCurrentWorkflow.mockReturnValue(workflowObject);
|
||||||
|
workflowsStore.getNodeById.mockImplementation(
|
||||||
|
(id: string) =>
|
||||||
|
({
|
||||||
|
[nodeA.id]: nodeA,
|
||||||
|
[nodeB.id]: nodeB,
|
||||||
|
[nodeC.id]: nodeC,
|
||||||
|
})[id],
|
||||||
|
);
|
||||||
|
workflowsStore.getNodeByName.mockImplementation(
|
||||||
|
(name: string) =>
|
||||||
|
({
|
||||||
|
[nodeA.name]: nodeA,
|
||||||
|
[nodeB.name]: nodeB,
|
||||||
|
[nodeC.name]: nodeC,
|
||||||
|
})[name],
|
||||||
|
);
|
||||||
|
workflowsStore.outgoingConnectionsByNodeName.mockReturnValue({
|
||||||
|
main: [[{ node: nodeC.name, type: NodeConnectionType.Main, index: 0 }]],
|
||||||
|
});
|
||||||
|
workflowsStore.incomingConnectionsByNodeName.mockReturnValue({
|
||||||
|
main: [[{ node: nodeA.name, type: NodeConnectionType.Main, index: 0 }]],
|
||||||
|
});
|
||||||
|
|
||||||
|
const { connectAdjacentNodes } = useCanvasOperations({ router });
|
||||||
|
connectAdjacentNodes(nodeB.id, { trackHistory: true });
|
||||||
|
|
||||||
|
// Check that A was connected directly to C
|
||||||
|
expect(workflowsStore.addConnection).toHaveBeenCalledWith({
|
||||||
|
connection: [
|
||||||
|
{ node: nodeA.name, type: NodeConnectionType.Main, index: 0 },
|
||||||
|
{ node: nodeC.name, type: NodeConnectionType.Main, index: 0 },
|
||||||
|
],
|
||||||
|
});
|
||||||
|
|
||||||
|
// Verify the connection was tracked in history
|
||||||
|
expect(historyStore.pushCommandToUndo).toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
|
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);
|
||||||
|
|
||||||
|
// Create three nodes in a sequence: A -> B -> C
|
||||||
|
const nodeA = createTestNode({ id: 'A', name: 'Node A', position: [0, 0] });
|
||||||
|
const nodeB = createTestNode({ id: 'B', name: 'Node B', position: [100, 0] });
|
||||||
|
const nodeC = createTestNode({ id: 'C', name: 'Node C', position: [200, 0] });
|
||||||
|
|
||||||
|
const nodeTypeDescription = mockNodeTypeDescription({
|
||||||
|
name: nodeA.type,
|
||||||
|
inputs: [NodeConnectionType.Main, NodeConnectionType.Main],
|
||||||
|
outputs: [NodeConnectionType.Main, NodeConnectionType.Main],
|
||||||
|
});
|
||||||
|
|
||||||
|
nodeTypesStore.getNodeType = vi.fn(() => nodeTypeDescription);
|
||||||
|
|
||||||
|
// Set up the workflow connections A -> B -> C
|
||||||
|
workflowsStore.workflow.nodes = [nodeA, nodeB, nodeC];
|
||||||
|
workflowsStore.workflow.connections = {
|
||||||
|
[nodeA.name]: {
|
||||||
|
main: [[{ node: nodeB.name, type: NodeConnectionType.Main, index: 1 }]],
|
||||||
|
},
|
||||||
|
[nodeB.name]: {
|
||||||
|
main: [[{ node: nodeC.name, type: NodeConnectionType.Main, index: 0 }]],
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
// Mock store methods
|
||||||
|
const workflowObject = createTestWorkflowObject(workflowsStore.workflow);
|
||||||
|
workflowsStore.getCurrentWorkflow.mockReturnValue(workflowObject);
|
||||||
|
workflowsStore.getNodeById.mockImplementation(
|
||||||
|
(id: string) =>
|
||||||
|
({
|
||||||
|
[nodeA.id]: nodeA,
|
||||||
|
[nodeB.id]: nodeB,
|
||||||
|
[nodeC.id]: nodeC,
|
||||||
|
})[id],
|
||||||
|
);
|
||||||
|
workflowsStore.getNodeByName.mockImplementation(
|
||||||
|
(name: string) =>
|
||||||
|
({
|
||||||
|
[nodeA.name]: nodeA,
|
||||||
|
[nodeB.name]: nodeB,
|
||||||
|
[nodeC.name]: nodeC,
|
||||||
|
})[name],
|
||||||
|
);
|
||||||
|
workflowsStore.outgoingConnectionsByNodeName.mockReturnValue({
|
||||||
|
main: [[{ node: nodeC.name, type: NodeConnectionType.Main, index: 1 }]],
|
||||||
|
});
|
||||||
|
workflowsStore.incomingConnectionsByNodeName.mockReturnValue({
|
||||||
|
main: [[{ node: nodeA.name, type: NodeConnectionType.Main, index: 0 }]],
|
||||||
|
});
|
||||||
|
|
||||||
|
const { connectAdjacentNodes } = useCanvasOperations({ router });
|
||||||
|
connectAdjacentNodes(nodeB.id, { trackHistory: true });
|
||||||
|
|
||||||
|
// Check that A was connected directly to C
|
||||||
|
expect(workflowsStore.addConnection).toHaveBeenCalledWith({
|
||||||
|
connection: [
|
||||||
|
{ node: nodeA.name, type: NodeConnectionType.Main, index: 0 },
|
||||||
|
{ node: nodeC.name, type: NodeConnectionType.Main, index: 1 },
|
||||||
|
],
|
||||||
|
});
|
||||||
|
|
||||||
|
// Verify the connection was tracked in history
|
||||||
|
expect(historyStore.pushCommandToUndo).toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
|
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: [100, 0] });
|
||||||
|
const nodeC = createTestNode({ id: 'C', name: 'Node C', position: [200, 0] });
|
||||||
|
|
||||||
|
workflowsStore.workflow.nodes = [nodeB, nodeC];
|
||||||
|
workflowsStore.workflow.connections = {
|
||||||
|
[nodeB.name]: {
|
||||||
|
main: [[{ node: nodeC.name, type: NodeConnectionType.Main, index: 0 }]],
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
const workflowObject = createTestWorkflowObject(workflowsStore.workflow);
|
||||||
|
workflowsStore.getCurrentWorkflow.mockReturnValue(workflowObject);
|
||||||
|
workflowsStore.getNodeById.mockReturnValue(nodeB);
|
||||||
|
workflowsStore.outgoingConnectionsByNodeName.mockReturnValue({
|
||||||
|
main: [[{ node: nodeC.name, type: NodeConnectionType.Main, index: 0 }]],
|
||||||
|
});
|
||||||
|
workflowsStore.incomingConnectionsByNodeName.mockReturnValue({});
|
||||||
|
|
||||||
|
const { connectAdjacentNodes } = useCanvasOperations({ router });
|
||||||
|
connectAdjacentNodes(nodeB.id);
|
||||||
|
|
||||||
|
expect(workflowsStore.addConnection).not.toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
|
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: [100, 0] });
|
||||||
|
|
||||||
|
workflowsStore.workflow.nodes = [nodeA, nodeB];
|
||||||
|
workflowsStore.workflow.connections = {
|
||||||
|
[nodeA.name]: {
|
||||||
|
main: [[{ node: nodeB.name, type: NodeConnectionType.Main, index: 0 }]],
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
const workflowObject = createTestWorkflowObject(workflowsStore.workflow);
|
||||||
|
workflowsStore.getCurrentWorkflow.mockReturnValue(workflowObject);
|
||||||
|
workflowsStore.getNodeById.mockReturnValue(nodeB);
|
||||||
|
workflowsStore.outgoingConnectionsByNodeName.mockReturnValue({});
|
||||||
|
workflowsStore.incomingConnectionsByNodeName.mockReturnValue({
|
||||||
|
main: [[{ node: nodeA.name, type: NodeConnectionType.Main, index: 0 }]],
|
||||||
|
});
|
||||||
|
|
||||||
|
const { connectAdjacentNodes } = useCanvasOperations({ router });
|
||||||
|
connectAdjacentNodes(nodeB.id);
|
||||||
|
|
||||||
|
expect(workflowsStore.addConnection).not.toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
function buildImportNodes() {
|
function buildImportNodes() {
|
||||||
|
|
|
@ -281,12 +281,12 @@ export function useCanvasOperations({ router }: { router: ReturnType<typeof useR
|
||||||
{
|
{
|
||||||
node: incomingConnection.node,
|
node: incomingConnection.node,
|
||||||
type,
|
type,
|
||||||
index: 0,
|
index: incomingConnection.index,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
node: outgoingConnection.node,
|
node: outgoingConnection.node,
|
||||||
type,
|
type,
|
||||||
index: 0,
|
index: outgoingConnection.index,
|
||||||
},
|
},
|
||||||
]),
|
]),
|
||||||
);
|
);
|
||||||
|
@ -297,11 +297,13 @@ export function useCanvasOperations({ router }: { router: ReturnType<typeof useR
|
||||||
sourceHandle: createCanvasConnectionHandleString({
|
sourceHandle: createCanvasConnectionHandleString({
|
||||||
mode: CanvasConnectionMode.Output,
|
mode: CanvasConnectionMode.Output,
|
||||||
type,
|
type,
|
||||||
|
index: incomingConnection.index,
|
||||||
}),
|
}),
|
||||||
target: outgoingNodeId,
|
target: outgoingNodeId,
|
||||||
targetHandle: createCanvasConnectionHandleString({
|
targetHandle: createCanvasConnectionHandleString({
|
||||||
mode: CanvasConnectionMode.Input,
|
mode: CanvasConnectionMode.Input,
|
||||||
type,
|
type,
|
||||||
|
index: outgoingConnection.index,
|
||||||
}),
|
}),
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
@ -1972,6 +1974,7 @@ export function useCanvasOperations({ router }: { router: ReturnType<typeof useR
|
||||||
revalidateNodeOutputConnections,
|
revalidateNodeOutputConnections,
|
||||||
isConnectionAllowed,
|
isConnectionAllowed,
|
||||||
filterConnectionsByNodes,
|
filterConnectionsByNodes,
|
||||||
|
connectAdjacentNodes,
|
||||||
importWorkflowData,
|
importWorkflowData,
|
||||||
fetchWorkflowDataFromUrl,
|
fetchWorkflowDataFromUrl,
|
||||||
resetWorkspace,
|
resetWorkspace,
|
||||||
|
|
Loading…
Reference in a new issue