mirror of
https://github.com/Abdulazizzn/n8n-enterprise-unlocked.git
synced 2025-12-16 09:36:44 +00:00
fix: Load workflows with unconnected Switch outputs (#12020)
This commit is contained in:
@@ -76,6 +76,10 @@ export function getCanvasNodes() {
|
||||
);
|
||||
}
|
||||
|
||||
export function getCanvasNodeByName(nodeName: string) {
|
||||
return getCanvasNodes().filter(`:contains(${nodeName})`);
|
||||
}
|
||||
|
||||
export function getSaveButton() {
|
||||
return cy.getByTestId('workflow-save-button');
|
||||
}
|
||||
@@ -194,3 +198,8 @@ export function pasteWorkflow(workflow: object) {
|
||||
export function clickZoomToFit() {
|
||||
getZoomToFitButton().click();
|
||||
}
|
||||
|
||||
export function deleteNode(name: string) {
|
||||
getCanvasNodeByName(name).first().click();
|
||||
cy.get('body').type('{del}');
|
||||
}
|
||||
|
||||
@@ -0,0 +1,17 @@
|
||||
import {
|
||||
deleteNode,
|
||||
getCanvasNodes,
|
||||
navigateToNewWorkflowPage,
|
||||
pasteWorkflow,
|
||||
} from '../composables/workflow';
|
||||
import Workflow from '../fixtures/Switch_node_with_null_connection.json';
|
||||
|
||||
describe('ADO-2929 can load Switch nodes', () => {
|
||||
it('can load workflows with Switch nodes with null at connection index', () => {
|
||||
navigateToNewWorkflowPage();
|
||||
pasteWorkflow(Workflow);
|
||||
getCanvasNodes().should('have.length', 3);
|
||||
deleteNode('Switch');
|
||||
getCanvasNodes().should('have.length', 2);
|
||||
});
|
||||
});
|
||||
85
cypress/fixtures/Switch_node_with_null_connection.json
Normal file
85
cypress/fixtures/Switch_node_with_null_connection.json
Normal file
@@ -0,0 +1,85 @@
|
||||
{
|
||||
"nodes": [
|
||||
{
|
||||
"parameters": {},
|
||||
"id": "418350b8-b402-4d3b-93ba-3794d36c1ad5",
|
||||
"name": "When clicking \"Test workflow\"",
|
||||
"type": "n8n-nodes-base.manualTrigger",
|
||||
"typeVersion": 1,
|
||||
"position": [440, 380]
|
||||
},
|
||||
{
|
||||
"parameters": {
|
||||
"rules": {
|
||||
"values": [
|
||||
{
|
||||
"conditions": {
|
||||
"options": {
|
||||
"caseSensitive": true,
|
||||
"leftValue": "",
|
||||
"typeValidation": "strict"
|
||||
},
|
||||
"conditions": [
|
||||
{
|
||||
"leftValue": "",
|
||||
"rightValue": "",
|
||||
"operator": {
|
||||
"type": "string",
|
||||
"operation": "equals"
|
||||
}
|
||||
}
|
||||
],
|
||||
"combinator": "and"
|
||||
}
|
||||
},
|
||||
{},
|
||||
{}
|
||||
]
|
||||
},
|
||||
"options": {}
|
||||
},
|
||||
"id": "b67ad46f-6b0d-4ff4-b2d2-dfbde44e287c",
|
||||
"name": "Switch",
|
||||
"type": "n8n-nodes-base.switch",
|
||||
"typeVersion": 3,
|
||||
"position": [660, 380]
|
||||
},
|
||||
{
|
||||
"parameters": {
|
||||
"options": {}
|
||||
},
|
||||
"id": "24731c11-e2a4-4854-81a6-277ce72e8a93",
|
||||
"name": "Edit Fields",
|
||||
"type": "n8n-nodes-base.set",
|
||||
"typeVersion": 3.3,
|
||||
"position": [840, 480]
|
||||
}
|
||||
],
|
||||
"connections": {
|
||||
"When clicking \"Test workflow\"": {
|
||||
"main": [
|
||||
[
|
||||
{
|
||||
"node": "Switch",
|
||||
"type": "main",
|
||||
"index": 0
|
||||
}
|
||||
]
|
||||
]
|
||||
},
|
||||
"Switch": {
|
||||
"main": [
|
||||
null,
|
||||
null,
|
||||
[
|
||||
{
|
||||
"node": "Edit Fields",
|
||||
"type": "main",
|
||||
"index": 0
|
||||
}
|
||||
]
|
||||
]
|
||||
}
|
||||
},
|
||||
"pinData": {}
|
||||
}
|
||||
@@ -38,7 +38,7 @@ export class PurgeInvalidWorkflowConnections1675940580449 implements Irreversibl
|
||||
|
||||
// It filters out all connections that are connected to a node that cannot receive input
|
||||
outputConnection.forEach((outputConnectionItem, outputConnectionItemIdx) => {
|
||||
outputConnection[outputConnectionItemIdx] = outputConnectionItem.filter(
|
||||
outputConnection[outputConnectionItemIdx] = (outputConnectionItem ?? []).filter(
|
||||
(outgoingConnections) =>
|
||||
!nodesThatCannotReceiveInput.includes(outgoingConnections.node),
|
||||
);
|
||||
|
||||
@@ -119,7 +119,7 @@ export class InstanceRiskReporter implements RiskReporter {
|
||||
node: WorkflowEntity['nodes'][number];
|
||||
workflow: WorkflowEntity;
|
||||
}) {
|
||||
const childNodeNames = workflow.connections[node.name]?.main[0].map((i) => i.node);
|
||||
const childNodeNames = workflow.connections[node.name]?.main[0]?.map((i) => i.node);
|
||||
|
||||
if (!childNodeNames) return false;
|
||||
|
||||
|
||||
@@ -448,7 +448,7 @@ export class DirectedGraph {
|
||||
|
||||
for (const [outputType, outputs] of Object.entries(iConnection)) {
|
||||
for (const [outputIndex, conns] of outputs.entries()) {
|
||||
for (const conn of conns) {
|
||||
for (const conn of conns ?? []) {
|
||||
// TODO: What's with the input type?
|
||||
const { node: toNodeName, type: _inputType, index: inputIndex } = conn;
|
||||
const to = workflow.getNode(toNodeName);
|
||||
|
||||
@@ -42,6 +42,28 @@ describe('DirectedGraph', () => {
|
||||
);
|
||||
});
|
||||
|
||||
// ┌─────┐ ┌─────┐──► null
|
||||
// │node1├───►│node2| ┌─────┐
|
||||
// └─────┘ └─────┘──►│node3|
|
||||
// └─────┘
|
||||
//
|
||||
test('linear workflow with null connections', () => {
|
||||
// ARRANGE
|
||||
const node1 = createNodeData({ name: 'Node1' });
|
||||
const node2 = createNodeData({ name: 'Node2' });
|
||||
const node3 = createNodeData({ name: 'Node3' });
|
||||
|
||||
// ACT
|
||||
const graph = new DirectedGraph()
|
||||
.addNodes(node1, node2, node3)
|
||||
.addConnections({ from: node1, to: node2 }, { from: node2, to: node3, outputIndex: 1 });
|
||||
|
||||
// ASSERT
|
||||
expect(DirectedGraph.fromWorkflow(graph.toWorkflow({ ...defaultWorkflowParameter }))).toEqual(
|
||||
graph,
|
||||
);
|
||||
});
|
||||
|
||||
describe('getChildren', () => {
|
||||
// ┌─────┐ ┌─────┐ ┌─────┐
|
||||
// │node1├───►│node2├──►│node3│
|
||||
|
||||
@@ -36,6 +36,7 @@ import type {
|
||||
CloseFunction,
|
||||
StartNodeData,
|
||||
NodeExecutionHint,
|
||||
NodeInputConnections,
|
||||
} from 'n8n-workflow';
|
||||
import {
|
||||
LoggerProxy as Logger,
|
||||
@@ -208,6 +209,9 @@ export class WorkflowExecute {
|
||||
// Get the data of the incoming connections
|
||||
incomingSourceData = { main: [] };
|
||||
for (const connections of incomingNodeConnections.main) {
|
||||
if (!connections) {
|
||||
continue;
|
||||
}
|
||||
for (let inputIndex = 0; inputIndex < connections.length; inputIndex++) {
|
||||
connection = connections[inputIndex];
|
||||
|
||||
@@ -249,6 +253,9 @@ export class WorkflowExecute {
|
||||
incomingNodeConnections = workflow.connectionsByDestinationNode[destinationNode];
|
||||
if (incomingNodeConnections !== undefined) {
|
||||
for (const connections of incomingNodeConnections.main) {
|
||||
if (!connections) {
|
||||
continue;
|
||||
}
|
||||
for (let inputIndex = 0; inputIndex < connections.length; inputIndex++) {
|
||||
connection = connections[inputIndex];
|
||||
|
||||
@@ -642,7 +649,7 @@ export class WorkflowExecute {
|
||||
}
|
||||
for (const connectionDataCheck of workflow.connectionsBySourceNode[parentNodeName].main[
|
||||
outputIndexParent
|
||||
]) {
|
||||
] ?? []) {
|
||||
checkOutputNodes.push(connectionDataCheck.node);
|
||||
}
|
||||
}
|
||||
@@ -661,7 +668,7 @@ export class WorkflowExecute {
|
||||
) {
|
||||
for (const inputData of workflow.connectionsByDestinationNode[connectionData.node].main[
|
||||
inputIndex
|
||||
]) {
|
||||
] ?? []) {
|
||||
if (inputData.node === parentNodeName) {
|
||||
// Is the node we come from so its data will be available for sure
|
||||
continue;
|
||||
@@ -681,7 +688,7 @@ export class WorkflowExecute {
|
||||
if (
|
||||
!this.incomingConnectionIsEmpty(
|
||||
this.runExecutionData.resultData.runData,
|
||||
workflow.connectionsByDestinationNode[inputData.node].main[0],
|
||||
workflow.connectionsByDestinationNode[inputData.node].main[0] ?? [],
|
||||
runIndex,
|
||||
)
|
||||
) {
|
||||
@@ -770,7 +777,7 @@ export class WorkflowExecute {
|
||||
} else if (
|
||||
this.incomingConnectionIsEmpty(
|
||||
this.runExecutionData.resultData.runData,
|
||||
workflow.connectionsByDestinationNode[nodeToAdd].main[0],
|
||||
workflow.connectionsByDestinationNode[nodeToAdd].main[0] ?? [],
|
||||
runIndex,
|
||||
)
|
||||
) {
|
||||
@@ -1066,7 +1073,7 @@ export class WorkflowExecute {
|
||||
if (workflow.connectionsByDestinationNode.hasOwnProperty(executionNode.name)) {
|
||||
// Check if the node has incoming connections
|
||||
if (workflow.connectionsByDestinationNode[executionNode.name].hasOwnProperty('main')) {
|
||||
let inputConnections: IConnection[][];
|
||||
let inputConnections: NodeInputConnections;
|
||||
let connectionIndex: number;
|
||||
|
||||
// eslint-disable-next-line prefer-const
|
||||
@@ -1586,7 +1593,7 @@ export class WorkflowExecute {
|
||||
// Iterate over all the different connections of this output
|
||||
for (connectionData of workflow.connectionsBySourceNode[executionNode.name].main[
|
||||
outputIndex
|
||||
]) {
|
||||
] ?? []) {
|
||||
if (!workflow.nodes.hasOwnProperty(connectionData.node)) {
|
||||
throw new ApplicationError('Destination node not found', {
|
||||
extra: {
|
||||
|
||||
@@ -7,6 +7,7 @@
|
||||
"clean": "rimraf dist .turbo",
|
||||
"build": "cross-env VUE_APP_PUBLIC_PATH=\"/{{BASE_PATH}}/\" NODE_OPTIONS=\"--max-old-space-size=8192\" vite build",
|
||||
"typecheck": "vue-tsc --noEmit",
|
||||
"typecheck:watch": "vue-tsc --watch --noEmit",
|
||||
"dev": "pnpm serve",
|
||||
"lint": "eslint src --ext .js,.ts,.vue --quiet",
|
||||
"lintfix": "eslint src --ext .js,.ts,.vue --fix",
|
||||
|
||||
@@ -256,7 +256,7 @@ export function useChatMessaging({
|
||||
];
|
||||
if (!connectedMemoryInputs) return [];
|
||||
|
||||
const memoryConnection = (connectedMemoryInputs ?? []).find((i) => i.length > 0)?.[0];
|
||||
const memoryConnection = (connectedMemoryInputs ?? []).find((i) => (i ?? []).length > 0)?.[0];
|
||||
|
||||
if (!memoryConnection) return [];
|
||||
|
||||
|
||||
@@ -176,7 +176,7 @@ function useJsonFieldCompletions() {
|
||||
if (activeNode) {
|
||||
const workflow = workflowsStore.getCurrentWorkflow();
|
||||
const input = workflow.connectionsByDestinationNode[activeNode.name];
|
||||
return input.main[0][0].node;
|
||||
return input.main[0] ? input.main[0][0].node : null;
|
||||
}
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
|
||||
@@ -86,7 +86,7 @@ function getMultipleNodesText(nodeName: string): string {
|
||||
props.workflow.connectionsByDestinationNode[activeNode.value.name].main || [];
|
||||
// Collect indexes of connected nodes
|
||||
const connectedInputIndexes = activeNodeConnections.reduce((acc: number[], node, index) => {
|
||||
if (node[0] && node[0].node === nodeName) return [...acc, index];
|
||||
if (node?.[0] && node[0].node === nodeName) return [...acc, index];
|
||||
return acc;
|
||||
}, []);
|
||||
|
||||
|
||||
@@ -238,7 +238,7 @@ describe('useCanvasMapping', () => {
|
||||
expect(
|
||||
mappedNodes.value[0]?.data?.connections[CanvasConnectionMode.Output][
|
||||
NodeConnectionType.Main
|
||||
][0][0],
|
||||
][0]?.[0],
|
||||
).toEqual(
|
||||
expect.objectContaining({
|
||||
node: setNode.name,
|
||||
@@ -253,7 +253,7 @@ describe('useCanvasMapping', () => {
|
||||
expect(
|
||||
mappedNodes.value[1]?.data?.connections[CanvasConnectionMode.Input][
|
||||
NodeConnectionType.Main
|
||||
][0][0],
|
||||
][0]?.[0],
|
||||
).toEqual(
|
||||
expect.objectContaining({
|
||||
node: manualTriggerNode.name,
|
||||
|
||||
@@ -802,6 +802,82 @@ describe('useCanvasOperations', () => {
|
||||
expect(workflowsStore.removeNodeExecutionDataById).toHaveBeenCalledWith(nodes[1].id);
|
||||
expect(workflowsStore.removeNodeById).toHaveBeenCalledWith(nodes[1].id);
|
||||
});
|
||||
|
||||
it('should handle nodes with null connections for unconnected indexes', () => {
|
||||
const workflowsStore = mockedStore(useWorkflowsStore);
|
||||
const nodeTypesStore = mockedStore(useNodeTypesStore);
|
||||
|
||||
nodeTypesStore.nodeTypes = {
|
||||
[SET_NODE_TYPE]: { 1: mockNodeTypeDescription({ name: SET_NODE_TYPE }) },
|
||||
};
|
||||
|
||||
const nodes = [
|
||||
createTestNode({
|
||||
id: 'input',
|
||||
type: SET_NODE_TYPE,
|
||||
position: [10, 20],
|
||||
name: 'Input Node',
|
||||
}),
|
||||
createTestNode({
|
||||
id: 'middle',
|
||||
type: SET_NODE_TYPE,
|
||||
position: [10, 20],
|
||||
name: 'Middle Node',
|
||||
}),
|
||||
createTestNode({
|
||||
id: 'output',
|
||||
type: SET_NODE_TYPE,
|
||||
position: [10, 20],
|
||||
name: 'Output Node',
|
||||
}),
|
||||
];
|
||||
|
||||
workflowsStore.getNodeByName = vi
|
||||
.fn()
|
||||
.mockImplementation((name: string) => nodes.find((node) => node.name === name));
|
||||
|
||||
workflowsStore.workflow.nodes = nodes;
|
||||
workflowsStore.workflow.connections = {
|
||||
[nodes[0].name]: {
|
||||
main: [
|
||||
null,
|
||||
[
|
||||
{
|
||||
node: nodes[1].name,
|
||||
type: NodeConnectionType.Main,
|
||||
index: 0,
|
||||
},
|
||||
],
|
||||
],
|
||||
},
|
||||
[nodes[1].name]: {
|
||||
main: [
|
||||
// null here to simulate no connection at index
|
||||
null,
|
||||
[
|
||||
{
|
||||
node: nodes[2].name,
|
||||
type: NodeConnectionType.Main,
|
||||
index: 0,
|
||||
},
|
||||
],
|
||||
],
|
||||
},
|
||||
};
|
||||
|
||||
const workflowObject = createTestWorkflowObject(workflowsStore.workflow);
|
||||
workflowsStore.getCurrentWorkflow.mockReturnValue(workflowObject);
|
||||
workflowsStore.incomingConnectionsByNodeName.mockReturnValue({});
|
||||
|
||||
workflowsStore.getNodeById.mockReturnValue(nodes[1]);
|
||||
|
||||
const { deleteNode } = useCanvasOperations({ router });
|
||||
deleteNode(nodes[1].id);
|
||||
|
||||
expect(workflowsStore.removeNodeById).toHaveBeenCalledWith(nodes[1].id);
|
||||
expect(workflowsStore.removeNodeExecutionDataById).toHaveBeenCalledWith(nodes[1].id);
|
||||
expect(workflowsStore.removeNodeById).toHaveBeenCalledWith(nodes[1].id);
|
||||
});
|
||||
});
|
||||
|
||||
describe('revertDeleteNode', () => {
|
||||
|
||||
@@ -1149,11 +1149,9 @@ export function useCanvasOperations({ router }: { router: ReturnType<typeof useR
|
||||
|
||||
for (const type of Object.keys(connections[nodeName])) {
|
||||
for (const index of Object.keys(connections[nodeName][type])) {
|
||||
for (const connectionIndex of Object.keys(
|
||||
connections[nodeName][type][parseInt(index, 10)],
|
||||
)) {
|
||||
const connectionData =
|
||||
connections[nodeName][type][parseInt(index, 10)][parseInt(connectionIndex, 10)];
|
||||
const connectionsToDelete = connections[nodeName][type][parseInt(index, 10)] ?? [];
|
||||
for (const connectionIndex of Object.keys(connectionsToDelete)) {
|
||||
const connectionData = connectionsToDelete[parseInt(connectionIndex, 10)];
|
||||
if (!connectionData) {
|
||||
continue;
|
||||
}
|
||||
@@ -1490,13 +1488,14 @@ export function useCanvasOperations({ router }: { router: ReturnType<typeof useR
|
||||
sourceIndex++
|
||||
) {
|
||||
const nodeSourceConnections = [];
|
||||
if (currentConnections[sourceNode][type][sourceIndex]) {
|
||||
const connectionsToCheck = currentConnections[sourceNode][type][sourceIndex];
|
||||
if (connectionsToCheck) {
|
||||
for (
|
||||
connectionIndex = 0;
|
||||
connectionIndex < currentConnections[sourceNode][type][sourceIndex].length;
|
||||
connectionIndex < connectionsToCheck.length;
|
||||
connectionIndex++
|
||||
) {
|
||||
connectionData = currentConnections[sourceNode][type][sourceIndex][connectionIndex];
|
||||
connectionData = connectionsToCheck[connectionIndex];
|
||||
if (!createNodeNames.includes(connectionData.node)) {
|
||||
// Node does not get created so skip input connection
|
||||
continue;
|
||||
@@ -1814,7 +1813,7 @@ export function useCanvasOperations({ router }: { router: ReturnType<typeof useR
|
||||
|
||||
for (const [type, typeConnections] of Object.entries(connections)) {
|
||||
const validConnections = typeConnections.map((sourceConnections) =>
|
||||
sourceConnections.filter((connection) => includeNodeNames.has(connection.node)),
|
||||
(sourceConnections ?? []).filter((connection) => includeNodeNames.has(connection.node)),
|
||||
);
|
||||
|
||||
if (validConnections.length) {
|
||||
|
||||
@@ -413,7 +413,7 @@ export function executeData(
|
||||
mainConnections: for (const mainConnections of workflow.connectionsByDestinationNode[
|
||||
currentNode
|
||||
].main) {
|
||||
for (const connection of mainConnections) {
|
||||
for (const connection of mainConnections ?? []) {
|
||||
if (
|
||||
connection.type === NodeConnectionType.Main &&
|
||||
connection.node === parentNodeName
|
||||
|
||||
@@ -156,7 +156,7 @@ export const useNDVStore = defineStore(STORES.NDV, () => {
|
||||
if (!activeNodeConections || activeNodeConections.length < 2) return returnData;
|
||||
|
||||
for (const [index, connection] of activeNodeConections.entries()) {
|
||||
for (const node of connection) {
|
||||
for (const node of connection ?? []) {
|
||||
if (!returnData[node.node]) {
|
||||
returnData[node.node] = [];
|
||||
}
|
||||
|
||||
@@ -891,23 +891,28 @@ export const useWorkflowsStore = defineStore(STORES.WORKFLOWS, () => {
|
||||
let propertyName: keyof IConnection;
|
||||
let connectionExists = false;
|
||||
|
||||
connectionLoop: for (const existingConnection of workflow.value.connections[sourceData.node][
|
||||
sourceData.type
|
||||
][sourceData.index]) {
|
||||
for (propertyName of checkProperties) {
|
||||
if (existingConnection[propertyName] !== destinationData[propertyName]) {
|
||||
continue connectionLoop;
|
||||
const nodeConnections = workflow.value.connections[sourceData.node][sourceData.type];
|
||||
const connectionsToCheck = nodeConnections[sourceData.index];
|
||||
|
||||
if (connectionsToCheck) {
|
||||
connectionLoop: for (const existingConnection of connectionsToCheck) {
|
||||
for (propertyName of checkProperties) {
|
||||
if (existingConnection[propertyName] !== destinationData[propertyName]) {
|
||||
continue connectionLoop;
|
||||
}
|
||||
}
|
||||
connectionExists = true;
|
||||
break;
|
||||
}
|
||||
connectionExists = true;
|
||||
break;
|
||||
}
|
||||
|
||||
// Add the new connection if it does not exist already
|
||||
if (!connectionExists) {
|
||||
workflow.value.connections[sourceData.node][sourceData.type][sourceData.index].push(
|
||||
destinationData,
|
||||
);
|
||||
nodeConnections[sourceData.index] = nodeConnections[sourceData.index] ?? [];
|
||||
const connections = nodeConnections[sourceData.index];
|
||||
if (connections) {
|
||||
connections.push(destinationData);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -934,6 +939,10 @@ export const useWorkflowsStore = defineStore(STORES.WORKFLOWS, () => {
|
||||
|
||||
const connections =
|
||||
workflow.value.connections[sourceData.node][sourceData.type][sourceData.index];
|
||||
if (!connections) {
|
||||
return;
|
||||
}
|
||||
|
||||
for (const index in connections) {
|
||||
if (
|
||||
connections[index].node === destinationData.node &&
|
||||
@@ -979,23 +988,19 @@ export const useWorkflowsStore = defineStore(STORES.WORKFLOWS, () => {
|
||||
for (type of Object.keys(workflow.value.connections[sourceNode])) {
|
||||
for (sourceIndex of Object.keys(workflow.value.connections[sourceNode][type])) {
|
||||
indexesToRemove.length = 0;
|
||||
for (connectionIndex of Object.keys(
|
||||
workflow.value.connections[sourceNode][type][parseInt(sourceIndex, 10)],
|
||||
)) {
|
||||
connectionData =
|
||||
workflow.value.connections[sourceNode][type][parseInt(sourceIndex, 10)][
|
||||
parseInt(connectionIndex, 10)
|
||||
];
|
||||
if (connectionData.node === node.name) {
|
||||
indexesToRemove.push(connectionIndex);
|
||||
const connectionsToRemove =
|
||||
workflow.value.connections[sourceNode][type][parseInt(sourceIndex, 10)];
|
||||
if (connectionsToRemove) {
|
||||
for (connectionIndex of Object.keys(connectionsToRemove)) {
|
||||
connectionData = connectionsToRemove[parseInt(connectionIndex, 10)];
|
||||
if (connectionData.node === node.name) {
|
||||
indexesToRemove.push(connectionIndex);
|
||||
}
|
||||
}
|
||||
indexesToRemove.forEach((index) => {
|
||||
connectionsToRemove.splice(parseInt(index, 10), 1);
|
||||
});
|
||||
}
|
||||
indexesToRemove.forEach((index) => {
|
||||
workflow.value.connections[sourceNode][type][parseInt(sourceIndex, 10)].splice(
|
||||
parseInt(index, 10),
|
||||
1,
|
||||
);
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -56,7 +56,7 @@ describe('updateDynamicConnections', () => {
|
||||
const updatedConnections = updateDynamicConnections(node, connections, parameterData);
|
||||
|
||||
expect(updatedConnections?.TestNode.main).toHaveLength(2);
|
||||
expect(updatedConnections?.TestNode.main[1][0].node).toEqual('Node3');
|
||||
expect(updatedConnections?.TestNode.main[1]?.[0].node).toEqual('Node3');
|
||||
});
|
||||
|
||||
it('should handle fallbackOutput === "extra" and all rules removed', () => {
|
||||
@@ -82,7 +82,7 @@ describe('updateDynamicConnections', () => {
|
||||
const updatedConnections = updateDynamicConnections(node, connections, parameterData);
|
||||
|
||||
expect(updatedConnections?.TestNode.main).toHaveLength(1);
|
||||
expect(updatedConnections?.TestNode.main[0][0].node).toEqual('Node3');
|
||||
expect(updatedConnections?.TestNode.main[0]?.[0].node).toEqual('Node3');
|
||||
});
|
||||
|
||||
it('should add a new connection when a rule is added', () => {
|
||||
@@ -137,7 +137,7 @@ describe('updateDynamicConnections', () => {
|
||||
|
||||
expect(updatedConnections?.TestNode.main).toHaveLength(4);
|
||||
expect(updatedConnections?.TestNode.main[2]).toEqual([]);
|
||||
expect(updatedConnections?.TestNode.main[3][0].node).toEqual('Node3');
|
||||
expect(updatedConnections?.TestNode.main[3]?.[0].node).toEqual('Node3');
|
||||
});
|
||||
|
||||
it('should return null if no conditions are met', () => {
|
||||
|
||||
@@ -77,7 +77,7 @@ export function updateDynamicConnections(
|
||||
}
|
||||
} else if (parameterData.name === 'parameters.rules.values') {
|
||||
const curentRulesvalues = (node.parameters?.rules as { values: IDataObject[] })?.values;
|
||||
let lastConnection: IConnection[] | undefined = undefined;
|
||||
let lastConnection: IConnection[] | null | undefined = undefined;
|
||||
if (
|
||||
fallbackOutput === 'extra' &&
|
||||
connections[node.name].main.length === curentRulesvalues.length + 1
|
||||
|
||||
@@ -1394,7 +1394,11 @@ export default defineComponent({
|
||||
lastSelectedNode.name,
|
||||
);
|
||||
|
||||
if (connections.main === undefined || connections.main.length === 0) {
|
||||
if (
|
||||
connections.main === undefined ||
|
||||
connections.main.length === 0 ||
|
||||
!connections.main[0]
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -1428,7 +1432,11 @@ export default defineComponent({
|
||||
|
||||
const connections = workflow.connectionsByDestinationNode[lastSelectedNode.name];
|
||||
|
||||
if (connections.main === undefined || connections.main.length === 0) {
|
||||
if (
|
||||
connections.main === undefined ||
|
||||
connections.main.length === 0 ||
|
||||
!connections.main[0]
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -1460,7 +1468,11 @@ export default defineComponent({
|
||||
return;
|
||||
}
|
||||
|
||||
const parentNode = connections.main[0][0].node;
|
||||
const parentNode = connections.main[0]?.[0].node;
|
||||
if (!parentNode) {
|
||||
return;
|
||||
}
|
||||
|
||||
const connectionsParent = this.workflowsStore.outgoingConnectionsByNodeName(parentNode);
|
||||
|
||||
if (!Array.isArray(connectionsParent.main) || !connectionsParent.main.length) {
|
||||
@@ -1472,7 +1484,7 @@ export default defineComponent({
|
||||
let lastCheckedNodePosition = e.key === 'ArrowUp' ? -99999999 : 99999999;
|
||||
let nextSelectNode: string | null = null;
|
||||
for (const ouputConnections of connectionsParent.main) {
|
||||
for (const ouputConnection of ouputConnections) {
|
||||
for (const ouputConnection of ouputConnections ?? []) {
|
||||
if (ouputConnection.node === lastSelectedNode.name) {
|
||||
// Ignore current node
|
||||
continue;
|
||||
@@ -3877,13 +3889,10 @@ export default defineComponent({
|
||||
sourceIndex++
|
||||
) {
|
||||
const nodeSourceConnections = [];
|
||||
if (currentConnections[sourceNode][type][sourceIndex]) {
|
||||
for (
|
||||
connectionIndex = 0;
|
||||
connectionIndex < currentConnections[sourceNode][type][sourceIndex].length;
|
||||
connectionIndex++
|
||||
) {
|
||||
connectionData = currentConnections[sourceNode][type][sourceIndex][connectionIndex];
|
||||
const connections = currentConnections[sourceNode][type][sourceIndex];
|
||||
if (connections) {
|
||||
for (connectionIndex = 0; connectionIndex < connections.length; connectionIndex++) {
|
||||
connectionData = connections[connectionIndex];
|
||||
if (!createNodeNames.includes(connectionData.node)) {
|
||||
// Node does not get created so skip input connection
|
||||
continue;
|
||||
@@ -4013,14 +4022,17 @@ export default defineComponent({
|
||||
for (type of Object.keys(connections)) {
|
||||
for (sourceIndex = 0; sourceIndex < connections[type].length; sourceIndex++) {
|
||||
connectionToKeep = [];
|
||||
for (
|
||||
connectionIndex = 0;
|
||||
connectionIndex < connections[type][sourceIndex].length;
|
||||
connectionIndex++
|
||||
) {
|
||||
connectionData = connections[type][sourceIndex][connectionIndex];
|
||||
if (exportNodeNames.indexOf(connectionData.node) !== -1) {
|
||||
connectionToKeep.push(connectionData);
|
||||
const connectionsToCheck = connections[type][sourceIndex];
|
||||
if (connectionsToCheck) {
|
||||
for (
|
||||
connectionIndex = 0;
|
||||
connectionIndex < connectionsToCheck.length;
|
||||
connectionIndex++
|
||||
) {
|
||||
connectionData = connectionsToCheck[connectionIndex];
|
||||
if (exportNodeNames.indexOf(connectionData.node) !== -1) {
|
||||
connectionToKeep.push(connectionData);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -364,7 +364,8 @@ export interface ICredentialDataDecryptedObject {
|
||||
|
||||
// First array index: The output/input-index (if node has multiple inputs/outputs of the same type)
|
||||
// Second array index: The different connections (if one node is connected to multiple nodes)
|
||||
export type NodeInputConnections = IConnection[][];
|
||||
// Any index can be null, for example in a switch node with multiple indexes some of which are not connected
|
||||
export type NodeInputConnections = Array<IConnection[] | null>;
|
||||
|
||||
export interface INodeConnection {
|
||||
sourceIndex: number;
|
||||
|
||||
@@ -194,7 +194,7 @@ export class Workflow {
|
||||
returnConnection[connectionInfo.node][connectionInfo.type].push([]);
|
||||
}
|
||||
|
||||
returnConnection[connectionInfo.node][connectionInfo.type][connectionInfo.index].push({
|
||||
returnConnection[connectionInfo.node][connectionInfo.type][connectionInfo.index]?.push({
|
||||
node: sourceNode,
|
||||
type,
|
||||
index: parseInt(inputIndex, 10),
|
||||
@@ -551,18 +551,18 @@ export class Workflow {
|
||||
let type: string;
|
||||
let sourceIndex: string;
|
||||
let connectionIndex: string;
|
||||
let connectionData: IConnection;
|
||||
let connectionData: IConnection | undefined;
|
||||
for (sourceNode of Object.keys(this.connectionsBySourceNode)) {
|
||||
for (type of Object.keys(this.connectionsBySourceNode[sourceNode])) {
|
||||
for (sourceIndex of Object.keys(this.connectionsBySourceNode[sourceNode][type])) {
|
||||
for (connectionIndex of Object.keys(
|
||||
this.connectionsBySourceNode[sourceNode][type][parseInt(sourceIndex, 10)],
|
||||
this.connectionsBySourceNode[sourceNode][type][parseInt(sourceIndex, 10)] || [],
|
||||
)) {
|
||||
connectionData =
|
||||
this.connectionsBySourceNode[sourceNode][type][parseInt(sourceIndex, 10)][
|
||||
this.connectionsBySourceNode[sourceNode][type][parseInt(sourceIndex, 10)]?.[
|
||||
parseInt(connectionIndex, 10)
|
||||
];
|
||||
if (connectionData.node === currentName) {
|
||||
if (connectionData?.node === currentName) {
|
||||
connectionData.node = newName;
|
||||
}
|
||||
}
|
||||
@@ -615,7 +615,7 @@ export class Workflow {
|
||||
const returnNodes: string[] = [];
|
||||
let addNodes: string[];
|
||||
|
||||
let connectionsByIndex: IConnection[];
|
||||
let connectionsByIndex: IConnection[] | null;
|
||||
for (
|
||||
let connectionIndex = 0;
|
||||
connectionIndex < this.connectionsByDestinationNode[nodeName][type].length;
|
||||
@@ -627,7 +627,7 @@ export class Workflow {
|
||||
}
|
||||
connectionsByIndex = this.connectionsByDestinationNode[nodeName][type][connectionIndex];
|
||||
// eslint-disable-next-line @typescript-eslint/no-loop-func
|
||||
connectionsByIndex.forEach((connection) => {
|
||||
connectionsByIndex?.forEach((connection) => {
|
||||
if (checkedNodes.includes(connection.node)) {
|
||||
// Node got checked already before
|
||||
return;
|
||||
@@ -742,7 +742,7 @@ export class Workflow {
|
||||
checkedNodes.push(nodeName);
|
||||
|
||||
connections[nodeName][type].forEach((connectionsByIndex) => {
|
||||
connectionsByIndex.forEach((connection) => {
|
||||
connectionsByIndex?.forEach((connection) => {
|
||||
if (checkedNodes.includes(connection.node)) {
|
||||
// Node got checked already before
|
||||
return;
|
||||
@@ -839,7 +839,7 @@ export class Workflow {
|
||||
}
|
||||
|
||||
connections[curr.name][type].forEach((connectionsByIndex) => {
|
||||
connectionsByIndex.forEach((connection) => {
|
||||
connectionsByIndex?.forEach((connection) => {
|
||||
queue.push({
|
||||
name: connection.node,
|
||||
indicies: [connection.index],
|
||||
@@ -943,6 +943,10 @@ export class Workflow {
|
||||
|
||||
let outputIndex: INodeConnection | undefined;
|
||||
for (const connectionsByIndex of this.connectionsByDestinationNode[nodeName][type]) {
|
||||
if (!connectionsByIndex) {
|
||||
continue;
|
||||
}
|
||||
|
||||
for (
|
||||
let destinationIndex = 0;
|
||||
destinationIndex < connectionsByIndex.length;
|
||||
|
||||
@@ -990,11 +990,8 @@ function alphanumericId() {
|
||||
|
||||
const chooseRandomly = <T>(array: T[]) => array[randomInt(array.length)];
|
||||
|
||||
function generateTestWorkflowAndRunData(): { workflow: IWorkflowBase; runData: IRunData } {
|
||||
const workflow: IWorkflowBase = {
|
||||
meta: {
|
||||
instanceId: 'a786b722078489c1fa382391a9f3476c2784761624deb2dfb4634827256d51a0',
|
||||
},
|
||||
function generateTestWorkflowAndRunData(): { workflow: Partial<IWorkflowBase>; runData: IRunData } {
|
||||
const workflow: Partial<IWorkflowBase> = {
|
||||
nodes: [
|
||||
{
|
||||
parameters: {},
|
||||
|
||||
Reference in New Issue
Block a user