mirror of
https://github.com/n8n-io/n8n.git
synced 2024-11-14 08:34:07 -08:00
fix(Switch Node): Maintain output connections (#11162)
This commit is contained in:
parent
f0492bd3bb
commit
9bd79fceeb
|
@ -81,7 +81,7 @@ describe('NDV', () => {
|
||||||
ndv.getters.backToCanvas().click();
|
ndv.getters.backToCanvas().click();
|
||||||
workflowPage.actions.executeWorkflow();
|
workflowPage.actions.executeWorkflow();
|
||||||
workflowPage.actions.openNode('Merge');
|
workflowPage.actions.openNode('Merge');
|
||||||
ndv.getters.outputPanel().contains('1 item').should('exist');
|
ndv.getters.outputPanel().contains('2 items').should('exist');
|
||||||
cy.contains('span', 'zero').should('exist');
|
cy.contains('span', 'zero').should('exist');
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
|
@ -22,11 +22,7 @@ import type {
|
||||||
IUpdateInformation,
|
IUpdateInformation,
|
||||||
} from '@/Interface';
|
} from '@/Interface';
|
||||||
|
|
||||||
import {
|
import { COMMUNITY_NODES_INSTALLATION_DOCS_URL, CUSTOM_NODES_DOCS_URL } from '@/constants';
|
||||||
COMMUNITY_NODES_INSTALLATION_DOCS_URL,
|
|
||||||
CUSTOM_NODES_DOCS_URL,
|
|
||||||
SHOULD_CLEAR_NODE_OUTPUTS,
|
|
||||||
} from '@/constants';
|
|
||||||
|
|
||||||
import NodeTitle from '@/components/NodeTitle.vue';
|
import NodeTitle from '@/components/NodeTitle.vue';
|
||||||
import ParameterInputList from '@/components/ParameterInputList.vue';
|
import ParameterInputList from '@/components/ParameterInputList.vue';
|
||||||
|
@ -47,11 +43,11 @@ import { useCredentialsStore } from '@/stores/credentials.store';
|
||||||
import type { EventBus } from 'n8n-design-system';
|
import type { EventBus } from 'n8n-design-system';
|
||||||
import { useExternalHooks } from '@/composables/useExternalHooks';
|
import { useExternalHooks } from '@/composables/useExternalHooks';
|
||||||
import { useNodeHelpers } from '@/composables/useNodeHelpers';
|
import { useNodeHelpers } from '@/composables/useNodeHelpers';
|
||||||
import { useToast } from '@/composables/useToast';
|
|
||||||
import { useI18n } from '@/composables/useI18n';
|
import { useI18n } from '@/composables/useI18n';
|
||||||
import { useTelemetry } from '@/composables/useTelemetry';
|
import { useTelemetry } from '@/composables/useTelemetry';
|
||||||
import { importCurlEventBus, ndvEventBus } from '@/event-bus';
|
import { importCurlEventBus, ndvEventBus } from '@/event-bus';
|
||||||
import { ProjectTypes } from '@/types/projects.types';
|
import { ProjectTypes } from '@/types/projects.types';
|
||||||
|
import { updateDynamicConnections } from '@/utils/nodeSettingsUtils';
|
||||||
|
|
||||||
const props = withDefaults(
|
const props = withDefaults(
|
||||||
defineProps<{
|
defineProps<{
|
||||||
|
@ -94,7 +90,6 @@ const telemetry = useTelemetry();
|
||||||
const nodeHelpers = useNodeHelpers();
|
const nodeHelpers = useNodeHelpers();
|
||||||
const externalHooks = useExternalHooks();
|
const externalHooks = useExternalHooks();
|
||||||
const i18n = useI18n();
|
const i18n = useI18n();
|
||||||
const { showMessage } = useToast();
|
|
||||||
|
|
||||||
const nodeValid = ref(true);
|
const nodeValid = ref(true);
|
||||||
const openPanel = ref<'params' | 'settings'>('params');
|
const openPanel = ref<'params' | 'settings'>('params');
|
||||||
|
@ -483,20 +478,6 @@ const valueChanged = (parameterData: IUpdateInformation) => {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (
|
|
||||||
parameterData.type &&
|
|
||||||
workflowsStore.nodeHasOutputConnection(_node.name) &&
|
|
||||||
SHOULD_CLEAR_NODE_OUTPUTS[nodeType.name]?.eventTypes.includes(parameterData.type) &&
|
|
||||||
SHOULD_CLEAR_NODE_OUTPUTS[nodeType.name]?.parameterPaths.includes(parameterData.name)
|
|
||||||
) {
|
|
||||||
workflowsStore.removeAllNodeConnection(_node, { preserveInputConnections: true });
|
|
||||||
showMessage({
|
|
||||||
type: 'warning',
|
|
||||||
title: i18n.baseText('nodeSettings.outputCleared.title'),
|
|
||||||
message: i18n.baseText('nodeSettings.outputCleared.message'),
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
// Get only the parameters which are different to the defaults
|
// Get only the parameters which are different to the defaults
|
||||||
let nodeParameters = NodeHelpers.getNodeParameters(
|
let nodeParameters = NodeHelpers.getNodeParameters(
|
||||||
nodeType.properties,
|
nodeType.properties,
|
||||||
|
@ -566,6 +547,14 @@ const valueChanged = (parameterData: IUpdateInformation) => {
|
||||||
value: nodeParameters,
|
value: nodeParameters,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const connections = workflowsStore.allConnections;
|
||||||
|
|
||||||
|
const updatedConnections = updateDynamicConnections(_node, connections, parameterData);
|
||||||
|
|
||||||
|
if (updatedConnections) {
|
||||||
|
workflowsStore.setConnections(updatedConnections, true);
|
||||||
|
}
|
||||||
|
|
||||||
workflowsStore.setNodeParameters(updateInformation);
|
workflowsStore.setNodeParameters(updateInformation);
|
||||||
|
|
||||||
void externalHooks.run('nodeSettings.valueChanged', {
|
void externalHooks.run('nodeSettings.valueChanged', {
|
||||||
|
|
|
@ -1054,8 +1054,12 @@ export const useWorkflowsStore = defineStore(STORES.WORKFLOWS, () => {
|
||||||
}, {});
|
}, {});
|
||||||
}
|
}
|
||||||
|
|
||||||
function setConnections(connections: IConnections): void {
|
function setConnections(connections: IConnections, updateWorkflow = false): void {
|
||||||
workflow.value.connections = connections;
|
workflow.value.connections = connections;
|
||||||
|
|
||||||
|
if (updateWorkflow) {
|
||||||
|
updateCachedWorkflow();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function resetAllNodesIssues(): boolean {
|
function resetAllNodesIssues(): boolean {
|
||||||
|
|
162
packages/editor-ui/src/utils/nodeSettingsUtils.test.ts
Normal file
162
packages/editor-ui/src/utils/nodeSettingsUtils.test.ts
Normal file
|
@ -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();
|
||||||
|
});
|
||||||
|
});
|
133
packages/editor-ui/src/utils/nodeSettingsUtils.ts
Normal file
133
packages/editor-ui/src/utils/nodeSettingsUtils.ts
Normal file
|
@ -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;
|
||||||
|
}
|
Loading…
Reference in a new issue