-
Notifications
You must be signed in to change notification settings - Fork 10.6k
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
fix(Switch Node): Maintain output connections (#11162)
- Loading branch information
1 parent
f0492bd
commit 9bd79fc
Showing
5 changed files
with
311 additions
and
23 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,162 @@ | ||
import { describe, it, expect, afterAll } from 'vitest'; | ||
import { mock } from 'vitest-mock-extended'; | ||
import type { IConnections, NodeParameterValueType } from 'n8n-workflow'; | ||
import { updateDynamicConnections } from './nodeSettingsUtils'; | ||
import { SWITCH_NODE_TYPE } from '@/constants'; | ||
import type { INodeUi, IUpdateInformation } from '@/Interface'; | ||
|
||
describe('updateDynamicConnections', () => { | ||
afterAll(() => { | ||
vi.clearAllMocks(); | ||
}); | ||
it('should remove extra outputs when the number of outputs decreases', () => { | ||
const node = mock<INodeUi>({ | ||
name: 'TestNode', | ||
type: SWITCH_NODE_TYPE, | ||
parameters: { numberOutputs: 3 }, | ||
}); | ||
|
||
const connections = mock<IConnections>({ | ||
TestNode: { | ||
main: [[{ node: 'Node1' }], [{ node: 'Node2' }], [{ node: 'Node3' }]], | ||
}, | ||
}); | ||
|
||
const parameterData = mock<IUpdateInformation<number>>({ | ||
name: 'parameters.numberOutputs', | ||
value: 2, | ||
}); | ||
|
||
const updatedConnections = updateDynamicConnections(node, connections, parameterData); | ||
|
||
expect(updatedConnections?.TestNode.main).toHaveLength(2); | ||
}); | ||
|
||
it('should splice connections when a rule is removed', () => { | ||
const node = mock<INodeUi>({ | ||
name: 'TestNode', | ||
type: SWITCH_NODE_TYPE, | ||
parameters: { | ||
rules: { values: [{}, {}, {}] }, | ||
options: {}, | ||
}, | ||
}); | ||
|
||
const connections = mock<IConnections>({ | ||
TestNode: { | ||
main: [[{ node: 'Node1' }], [{ node: 'Node2' }], [{ node: 'Node3' }]], | ||
}, | ||
}); | ||
|
||
const parameterData = mock<IUpdateInformation<number>>({ | ||
name: 'parameters.rules.values[1]', | ||
value: undefined, | ||
}); | ||
|
||
const updatedConnections = updateDynamicConnections(node, connections, parameterData); | ||
|
||
expect(updatedConnections?.TestNode.main).toHaveLength(2); | ||
expect(updatedConnections?.TestNode.main[1][0].node).toEqual('Node3'); | ||
}); | ||
|
||
it('should handle fallbackOutput === "extra" and all rules removed', () => { | ||
const node = mock<INodeUi>({ | ||
name: 'TestNode', | ||
type: SWITCH_NODE_TYPE, | ||
parameters: { | ||
options: { fallbackOutput: 'extra' }, | ||
}, | ||
}); | ||
|
||
const connections = mock<IConnections>({ | ||
TestNode: { | ||
main: [[{ node: 'Node1' }], [{ node: 'Node2' }], [{ node: 'Node3' }]], | ||
}, | ||
}); | ||
|
||
const parameterData = mock<IUpdateInformation<number>>({ | ||
name: 'parameters.rules.values', | ||
value: undefined, | ||
}); | ||
|
||
const updatedConnections = updateDynamicConnections(node, connections, parameterData); | ||
|
||
expect(updatedConnections?.TestNode.main).toHaveLength(1); | ||
expect(updatedConnections?.TestNode.main[0][0].node).toEqual('Node3'); | ||
}); | ||
|
||
it('should add a new connection when a rule is added', () => { | ||
const node = mock<INodeUi>({ | ||
name: 'TestNode', | ||
type: SWITCH_NODE_TYPE, | ||
parameters: { | ||
rules: { values: [{}, {}] }, | ||
options: { fallbackOutput: 'none' }, | ||
}, | ||
}); | ||
|
||
const connections = mock<IConnections>({ | ||
TestNode: { | ||
main: [[{ node: 'Node1' }], [{ node: 'Node2' }]], | ||
}, | ||
}); | ||
|
||
const parameterData = mock<IUpdateInformation<NodeParameterValueType>>({ | ||
name: 'parameters.rules.values', | ||
value: [{}, {}, {}], | ||
}); | ||
|
||
const updatedConnections = updateDynamicConnections(node, connections, parameterData); | ||
|
||
expect(updatedConnections?.TestNode.main).toHaveLength(3); | ||
expect(updatedConnections?.TestNode.main[2]).toEqual([]); | ||
}); | ||
|
||
it('should handle extra output when rule is added and fallbackOutput is extra', () => { | ||
const node = mock<INodeUi>({ | ||
name: 'TestNode', | ||
type: SWITCH_NODE_TYPE, | ||
parameters: { | ||
rules: { values: [{}, {}] }, | ||
options: { fallbackOutput: 'extra' }, | ||
}, | ||
}); | ||
|
||
const connections = mock<IConnections>({ | ||
TestNode: { | ||
main: [[{ node: 'Node1' }], [{ node: 'Node2' }], [{ node: 'Node3' }]], | ||
}, | ||
}); | ||
|
||
const parameterData = mock<IUpdateInformation<NodeParameterValueType>>({ | ||
name: 'parameters.rules.values', | ||
value: [{}, {}, {}], | ||
}); | ||
|
||
const updatedConnections = updateDynamicConnections(node, connections, parameterData); | ||
|
||
expect(updatedConnections?.TestNode.main).toHaveLength(4); | ||
expect(updatedConnections?.TestNode.main[2]).toEqual([]); | ||
expect(updatedConnections?.TestNode.main[3][0].node).toEqual('Node3'); | ||
}); | ||
|
||
it('should return null if no conditions are met', () => { | ||
const node = mock<INodeUi>({ | ||
name: 'TestNode', | ||
type: 'otherNodeType', | ||
}); | ||
|
||
const connections = mock<IConnections>({ | ||
TestNode: { main: [] }, | ||
}); | ||
|
||
const parameterData = mock<IUpdateInformation<number>>({ | ||
name: 'parameters.otherParameter', | ||
value: 3, | ||
}); | ||
|
||
const result = updateDynamicConnections(node, connections, parameterData); | ||
|
||
expect(result).toBeNull(); | ||
}); | ||
}); |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,133 @@ | ||
import type { | ||
IConnection, | ||
IConnections, | ||
IDataObject, | ||
NodeInputConnections, | ||
NodeParameterValueType, | ||
} from 'n8n-workflow'; | ||
import type { INodeUi, IUpdateInformation } from '@/Interface'; | ||
import { SWITCH_NODE_TYPE } from '@/constants'; | ||
import { isEqual } from 'lodash-es'; | ||
|
||
import { captureException } from '@sentry/vue'; | ||
|
||
export function updateDynamicConnections( | ||
node: INodeUi, | ||
workflowConnections: IConnections, | ||
parameterData: IUpdateInformation<NodeParameterValueType>, | ||
) { | ||
const connections = { ...workflowConnections }; | ||
|
||
try { | ||
if (parameterData.name.includes('conditions') || !connections[node.name]?.main) return null; | ||
|
||
if (node.type === SWITCH_NODE_TYPE && parameterData.name === 'parameters.numberOutputs') { | ||
const curentNumberOutputs = node.parameters?.numberOutputs as number; | ||
const newNumberOutputs = parameterData.value as number; | ||
|
||
// remove extra outputs | ||
if (newNumberOutputs < curentNumberOutputs) { | ||
connections[node.name].main = connections[node.name].main.slice(0, newNumberOutputs); | ||
return connections; | ||
} | ||
} | ||
|
||
if ( | ||
node.type === SWITCH_NODE_TYPE && | ||
parameterData.name === 'parameters.options.fallbackOutput' | ||
) { | ||
const curentFallbackOutput = (node.parameters?.options as { fallbackOutput: string }) | ||
?.fallbackOutput as string; | ||
if (curentFallbackOutput === 'extra') { | ||
if (!parameterData.value || parameterData.value !== 'extra') { | ||
connections[node.name].main = connections[node.name].main.slice(0, -1); | ||
return connections; | ||
} | ||
} | ||
} | ||
|
||
if (node.type === SWITCH_NODE_TYPE && parameterData.name.includes('parameters.rules.values')) { | ||
const { fallbackOutput } = node.parameters?.options as { fallbackOutput: string }; | ||
|
||
if (parameterData.value === undefined) { | ||
function extractIndex(path: string): number | null { | ||
const match = path.match(/parameters\.rules\.values\[(\d+)\]$/); | ||
return match ? parseInt(match[1], 10) : null; | ||
} | ||
|
||
const index = extractIndex(parameterData.name); | ||
|
||
// rule was removed | ||
if (index !== null) { | ||
connections[node.name].main.splice(index, 1); | ||
return connections; | ||
} | ||
|
||
// all rules were removed | ||
if (parameterData.name === 'parameters.rules.values') { | ||
if (fallbackOutput === 'extra') { | ||
connections[node.name].main = [ | ||
connections[node.name].main[connections[node.name].main.length - 1], | ||
]; | ||
} else { | ||
connections[node.name].main = []; | ||
} | ||
|
||
return connections; | ||
} | ||
} else if (parameterData.name === 'parameters.rules.values') { | ||
const curentRulesvalues = (node.parameters?.rules as { values: IDataObject[] })?.values; | ||
let lastConnection: IConnection[] | undefined = undefined; | ||
if ( | ||
fallbackOutput === 'extra' && | ||
connections[node.name].main.length === curentRulesvalues.length + 1 | ||
) { | ||
lastConnection = connections[node.name].main.pop(); | ||
} | ||
// rule was added | ||
const currentRulesLength = (node.parameters?.rules as { values: IDataObject[] })?.values | ||
?.length; | ||
|
||
const newRulesLength = (parameterData.value as IDataObject[])?.length; | ||
|
||
if (newRulesLength - currentRulesLength === 1) { | ||
connections[node.name].main = [...connections[node.name].main, []]; | ||
|
||
if (lastConnection) { | ||
connections[node.name].main.push(lastConnection); | ||
} | ||
|
||
return connections; | ||
} else { | ||
// order was changed | ||
const newRulesvalues = parameterData.value as IDataObject[]; | ||
const updatedConnectionsIndex: number[] = []; | ||
|
||
for (const rule of curentRulesvalues) { | ||
const index = newRulesvalues.findIndex((newRule) => isEqual(rule, newRule)); | ||
if (index !== -1) { | ||
updatedConnectionsIndex.push(index); | ||
} | ||
} | ||
|
||
const reorderedConnections: NodeInputConnections = []; | ||
|
||
for (const index of updatedConnectionsIndex) { | ||
reorderedConnections.push(connections[node.name].main[index] ?? []); | ||
} | ||
|
||
if (lastConnection) { | ||
reorderedConnections.push(lastConnection); | ||
} | ||
|
||
connections[node.name].main = reorderedConnections; | ||
return connections; | ||
} | ||
} | ||
} | ||
} catch (error) { | ||
captureException(error); | ||
} | ||
|
||
return null; | ||
} |