diff --git a/cypress/e2e/5-ndv.cy.ts b/cypress/e2e/5-ndv.cy.ts index 1aea7035f5cd1..7ce95aba055f1 100644 --- a/cypress/e2e/5-ndv.cy.ts +++ b/cypress/e2e/5-ndv.cy.ts @@ -56,6 +56,26 @@ describe('NDV', () => { cy.shouldNotHaveConsoleErrors(); }); + it('should disconect Switch outputs if rules order was changed', () => { + cy.createFixtureWorkflow('NDV-test-switch_reorder.json', `NDV test switch reorder`); + workflowPage.actions.zoomToFit(); + + workflowPage.actions.executeWorkflow(); + workflowPage.actions.openNode('Merge'); + ndv.getters.outputPanel().contains('2 items').should('exist'); + cy.contains('span', 'first').should('exist'); + ndv.getters.backToCanvas().click(); + + workflowPage.actions.openNode('Switch'); + cy.get('.cm-line').realMouseMove(100, 100); + cy.get('.fa-angle-down').click(); + ndv.getters.backToCanvas().click(); + workflowPage.actions.executeWorkflow(); + workflowPage.actions.openNode('Merge'); + ndv.getters.outputPanel().contains('1 item').should('exist'); + cy.contains('span', 'zero').should('exist'); + }); + it('should show correct validation state for resource locator params', () => { workflowPage.actions.addNodeToCanvas('Typeform', true, true); ndv.getters.container().should('be.visible'); diff --git a/cypress/fixtures/NDV-test-switch_reorder.json b/cypress/fixtures/NDV-test-switch_reorder.json new file mode 100644 index 0000000000000..cf970434f3efb --- /dev/null +++ b/cypress/fixtures/NDV-test-switch_reorder.json @@ -0,0 +1,235 @@ +{ + "name": "switch reorder", + "nodes": [ + { + "parameters": {}, + "id": "b3f0815d-b733-413f-ab3f-74e48277bd3a", + "name": "When clicking \"Test workflow\"", + "type": "n8n-nodes-base.manualTrigger", + "typeVersion": 1, + "position": [ + -20, + 620 + ] + }, + { + "parameters": {}, + "id": "fbc5b12a-6165-4cab-80a1-9fd6e4fbe39f", + "name": "One", + "type": "n8n-nodes-base.noOp", + "typeVersion": 1, + "position": [ + 620, + 720 + ] + }, + { + "parameters": { + "duplicateItem": true, + "duplicateCount": 1, + "assignments": { + "assignments": [ + { + "id": "ec6c1d1d-a17a-4537-8135-d474df7fded1", + "name": "entry", + "value": "first", + "type": "string" + } + ] + }, + "options": {} + }, + "id": "8c5a72a5-17ef-40e0-8477-764f24770174", + "name": "Edit Fields", + "type": "n8n-nodes-base.set", + "typeVersion": 3.3, + "position": [ + 160, + 740 + ] + }, + { + "parameters": { + "assignments": { + "assignments": [ + { + "id": "d8ec7c46-d02f-4bf5-931e-5ec2fb8bea22", + "name": "entry", + "value": "zero", + "type": "string" + } + ] + }, + "options": {} + }, + "id": "bc3fb81d-2ddf-4b28-a93d-762a48e8fd6b", + "name": "Edit Fields1", + "type": "n8n-nodes-base.set", + "typeVersion": 3.3, + "position": [ + 160, + 500 + ] + }, + { + "parameters": { + "rules": { + "values": [ + { + "conditions": { + "options": { + "caseSensitive": true, + "leftValue": "", + "typeValidation": "strict" + }, + "conditions": [ + { + "leftValue": "={{ $json.entry }}", + "rightValue": "first", + "operator": { + "type": "string", + "operation": "equals" + } + } + ], + "combinator": "and" + }, + "renameOutput": true, + "outputKey": "1" + }, + { + "conditions": { + "options": { + "caseSensitive": true, + "leftValue": "", + "typeValidation": "strict" + }, + "conditions": [ + { + "id": "ffa570ef-fc16-49ec-87be-56159f14a44b", + "leftValue": "={{ $json.entry }}", + "rightValue": "=second", + "operator": { + "type": "string", + "operation": "equals" + } + } + ], + "combinator": "and" + }, + "renameOutput": true, + "outputKey": "2" + } + ] + }, + "options": {} + }, + "id": "296ba553-c6c5-4c84-89fb-9056b24bab30", + "name": "Switch", + "type": "n8n-nodes-base.switch", + "typeVersion": 3, + "position": [ + 360, + 740 + ] + }, + { + "parameters": {}, + "id": "da787dd6-8e85-4dd5-8326-198705b4ae4b", + "name": "Merge", + "type": "n8n-nodes-base.merge", + "typeVersion": 2.1, + "position": [ + 880, + 520 + ] + } + ], + "pinData": { + "Edit Fields": [ + { + "json": { + "entry": "first" + } + }, + { + "json": { + "entry": "second" + } + } + ] + }, + "connections": { + "When clicking \"Test workflow\"": { + "main": [ + [ + { + "node": "Edit Fields", + "type": "main", + "index": 0 + }, + { + "node": "Edit Fields1", + "type": "main", + "index": 0 + } + ] + ] + }, + "Edit Fields": { + "main": [ + [ + { + "node": "Switch", + "type": "main", + "index": 0 + } + ] + ] + }, + "One": { + "main": [ + [ + { + "node": "Merge", + "type": "main", + "index": 1 + } + ] + ] + }, + "Edit Fields1": { + "main": [ + [ + { + "node": "Merge", + "type": "main", + "index": 0 + } + ] + ] + }, + "Switch": { + "main": [ + [ + { + "node": "One", + "type": "main", + "index": 0 + } + ] + ] + } + }, + "active": false, + "settings": { + "executionOrder": "v1" + }, + "versionId": "ce5db792-5e38-4d54-895b-88d85f2545d0", + "meta": { + "templateCredsSetupCompleted": true, + "instanceId": "be251a83c052a9862eeac953816fbb1464f89dfbf79d7ac490a8e336a8cc8bfd" + }, + "id": "uMpL0bN7t1NYZDJS", + "tags": [] +} diff --git a/packages/editor-ui/src/Interface.ts b/packages/editor-ui/src/Interface.ts index a5c0202c4be2f..7fa606bca4871 100644 --- a/packages/editor-ui/src/Interface.ts +++ b/packages/editor-ui/src/Interface.ts @@ -139,6 +139,7 @@ export interface IUpdateInformation { | INodeParameters; // with null makes problems in NodeSettings.vue node?: string; oldValue?: string | number; + type?: 'optionsOrderChanged'; } export interface INodeUpdatePropertiesInformation { diff --git a/packages/editor-ui/src/components/FixedCollectionParameter.vue b/packages/editor-ui/src/components/FixedCollectionParameter.vue index e1545f9e9efa2..6fabba8532868 100644 --- a/packages/editor-ui/src/components/FixedCollectionParameter.vue +++ b/packages/editor-ui/src/components/FixedCollectionParameter.vue @@ -254,6 +254,7 @@ export default defineComponent({ const parameterData = { name: this.getPropertyPath(optionName), value: this.mutableValues[optionName], + type: 'optionsOrderChanged', }; this.$emit('valueChanged', parameterData); @@ -270,6 +271,7 @@ export default defineComponent({ const parameterData = { name: this.getPropertyPath(optionName), value: this.mutableValues[optionName], + type: 'optionsOrderChanged', }; this.$emit('valueChanged', parameterData); diff --git a/packages/editor-ui/src/components/NodeSettings.vue b/packages/editor-ui/src/components/NodeSettings.vue index bd225c178545a..3b464f9dc9bb3 100644 --- a/packages/editor-ui/src/components/NodeSettings.vue +++ b/packages/editor-ui/src/components/NodeSettings.vue @@ -200,6 +200,7 @@ import { CUSTOM_NODES_DOCS_URL, MAIN_NODE_PANEL_WIDTH, IMPORT_CURL_MODAL_KEY, + SHOULD_CLEAR_NODE_OUTPUTS, } from '@/constants'; import NodeTitle from '@/components/NodeTitle.vue'; @@ -223,6 +224,7 @@ import { useCredentialsStore } from '@/stores/credentials.store'; import type { EventBus } from 'n8n-design-system'; import { useExternalHooks } from '@/composables/useExternalHooks'; import { useNodeHelpers } from '@/composables/useNodeHelpers'; +import { useToast } from '@/composables/useToast'; export default defineComponent({ name: 'NodeSettings', @@ -238,10 +240,12 @@ export default defineComponent({ setup() { const nodeHelpers = useNodeHelpers(); const externalHooks = useExternalHooks(); + const { showMessage } = useToast(); return { externalHooks, nodeHelpers, + showMessage, }; }, computed: { @@ -853,6 +857,20 @@ export default defineComponent({ return; } + if ( + parameterData.type && + this.workflowsStore.nodeHasOutputConnection(node.name) && + SHOULD_CLEAR_NODE_OUTPUTS[nodeType.name]?.eventTypes.includes(parameterData.type) && + SHOULD_CLEAR_NODE_OUTPUTS[nodeType.name]?.parameterPaths.includes(parameterData.name) + ) { + this.workflowsStore.removeAllNodeConnection(node, { preserveInputConnections: true }); + this.showMessage({ + type: 'warning', + title: this.$locale.baseText('nodeSettings.outputCleared.title'), + message: this.$locale.baseText('nodeSettings.outputCleared.message'), + }); + } + // Get only the parameters which are different to the defaults let nodeParameters = NodeHelpers.getNodeParameters( nodeType.properties, diff --git a/packages/editor-ui/src/constants.ts b/packages/editor-ui/src/constants.ts index 44dbab412bf82..728ee7588c41e 100644 --- a/packages/editor-ui/src/constants.ts +++ b/packages/editor-ui/src/constants.ts @@ -653,6 +653,20 @@ export const MFA_AUTHENTICATION_RECOVERY_CODE_INPUT_MAX_LENGTH = 36; export const NODE_TYPES_EXCLUDED_FROM_OUTPUT_NAME_APPEND = [FILTER_NODE_TYPE, SWITCH_NODE_TYPE]; +type ClearOutgoingConnectonsEvents = { + [nodeName: string]: { + parameterPaths: string[]; + eventTypes: string[]; + }; +}; + +export const SHOULD_CLEAR_NODE_OUTPUTS: ClearOutgoingConnectonsEvents = { + [SWITCH_NODE_TYPE]: { + parameterPaths: ['parameters.rules.values'], + eventTypes: ['optionsOrderChanged'], + }, +}; + export const ALLOWED_HTML_ATTRIBUTES = ['href', 'name', 'target', 'title', 'class', 'id', 'style']; export const ALLOWED_HTML_TAGS = [ diff --git a/packages/editor-ui/src/plugins/i18n/locales/en.json b/packages/editor-ui/src/plugins/i18n/locales/en.json index ad5d7e0ade023..1c07021e8f9db 100644 --- a/packages/editor-ui/src/plugins/i18n/locales/en.json +++ b/packages/editor-ui/src/plugins/i18n/locales/en.json @@ -1093,6 +1093,8 @@ "nodeSettings.latest": "Latest", "nodeSettings.deprecated": "Deprecated", "nodeSettings.latestVersion": "Latest version: {version}", + "nodeSettings.outputCleared.title": "Parameters changed", + "nodeSettings.outputCleared.message": "Order of parameters changed, outgoing connections were cleared", "nodeSettings.nodeVersion": "{node} node version {version}", "nodeView.addNode": "Add node", "nodeView.openNodesPanel": "Open nodes panel", diff --git a/packages/editor-ui/src/stores/workflows.store.ts b/packages/editor-ui/src/stores/workflows.store.ts index 6e19b8d8f331a..8eed4d08fc3ff 100644 --- a/packages/editor-ui/src/stores/workflows.store.ts +++ b/packages/editor-ui/src/stores/workflows.store.ts @@ -200,6 +200,12 @@ export const useWorkflowsStore = defineStore(STORES.WORKFLOWS, { return {}; }; }, + nodeHasOutputConnection() { + return (nodeName: string): boolean => { + if (this.workflow.connections.hasOwnProperty(nodeName)) return true; + return false; + }; + }, isNodeInOutgoingNodeConnections() { return (firstNode: string, secondNode: string): boolean => { const firstNodeConnections = this.outgoingConnectionsByNodeName(firstNode); @@ -841,15 +847,19 @@ export const useWorkflowsStore = defineStore(STORES.WORKFLOWS, { this.workflow.connections = {}; }, - removeAllNodeConnection(node: INodeUi): void { + removeAllNodeConnection( + node: INodeUi, + { preserveInputConnections = false, preserveOutputConnections = false } = {}, + ): void { const uiStore = useUIStore(); uiStore.stateIsDirty = true; // Remove all source connections - if (this.workflow.connections.hasOwnProperty(node.name)) { + if (!preserveOutputConnections && this.workflow.connections.hasOwnProperty(node.name)) { delete this.workflow.connections[node.name]; } // Remove all destination connections + if (preserveInputConnections) return; const indexesToRemove = []; let sourceNode: string, type: string,