mirror of
https://github.com/n8n-io/n8n.git
synced 2024-11-09 22:24:05 -08:00
feat(Switch Node): Add support for infinite Switch outputs (#7499)
Github issue / Community forum post (link here to close automatically): https://community.n8n.io/t/add-more-outputs-to-switch-node/3864 --------- Signed-off-by: Oleg Ivaniv <me@olegivaniv.com>
This commit is contained in:
parent
6f45298d3d
commit
2febc61ec9
|
@ -43,7 +43,9 @@ describe('Undo/Redo', () => {
|
||||||
WorkflowPage.actions.zoomToFit();
|
WorkflowPage.actions.zoomToFit();
|
||||||
WorkflowPage.getters
|
WorkflowPage.getters
|
||||||
.canvasNodeByName('Code')
|
.canvasNodeByName('Code')
|
||||||
.should('have.attr', 'style', 'left: 860px; top: 220px;');
|
.should('have.css', 'left', '860px')
|
||||||
|
.should('have.css', 'top', '220px')
|
||||||
|
|
||||||
WorkflowPage.actions.hitUndo();
|
WorkflowPage.actions.hitUndo();
|
||||||
WorkflowPage.getters.canvasNodes().should('have.have.length', 2);
|
WorkflowPage.getters.canvasNodes().should('have.have.length', 2);
|
||||||
WorkflowPage.getters.nodeConnections().should('have.length', 1);
|
WorkflowPage.getters.nodeConnections().should('have.length', 1);
|
||||||
|
@ -59,7 +61,8 @@ describe('Undo/Redo', () => {
|
||||||
// Last node should be added back to original position
|
// Last node should be added back to original position
|
||||||
WorkflowPage.getters
|
WorkflowPage.getters
|
||||||
.canvasNodeByName('Code')
|
.canvasNodeByName('Code')
|
||||||
.should('have.attr', 'style', 'left: 860px; top: 220px;');
|
.should('have.css', 'left', '860px')
|
||||||
|
.should('have.css', 'top', '220px')
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should undo/redo deleting node using delete button', () => {
|
it('should undo/redo deleting node using delete button', () => {
|
||||||
|
@ -133,15 +136,19 @@ describe('Undo/Redo', () => {
|
||||||
cy.drag('[data-test-id="canvas-node"].jtk-drag-selected', [50, 150], { clickToFinish: true });
|
cy.drag('[data-test-id="canvas-node"].jtk-drag-selected', [50, 150], { clickToFinish: true });
|
||||||
WorkflowPage.getters
|
WorkflowPage.getters
|
||||||
.canvasNodeByName('Code')
|
.canvasNodeByName('Code')
|
||||||
.should('have.attr', 'style', 'left: 740px; top: 320px;');
|
.should('have.css', 'left', '740px')
|
||||||
|
.should('have.css', 'top', '320px')
|
||||||
|
|
||||||
WorkflowPage.actions.hitUndo();
|
WorkflowPage.actions.hitUndo();
|
||||||
WorkflowPage.getters
|
WorkflowPage.getters
|
||||||
.canvasNodeByName('Code')
|
.canvasNodeByName('Code')
|
||||||
.should('have.attr', 'style', 'left: 640px; top: 220px;');
|
.should('have.css', 'left', '640px')
|
||||||
|
.should('have.css', 'top', '220px')
|
||||||
WorkflowPage.actions.hitRedo();
|
WorkflowPage.actions.hitRedo();
|
||||||
WorkflowPage.getters
|
WorkflowPage.getters
|
||||||
.canvasNodeByName('Code')
|
.canvasNodeByName('Code')
|
||||||
.should('have.attr', 'style', 'left: 740px; top: 320px;');
|
.should('have.css', 'left', '740px')
|
||||||
|
.should('have.css', 'top', '320px')
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should undo/redo deleting a connection by pressing delete button', () => {
|
it('should undo/redo deleting a connection by pressing delete button', () => {
|
||||||
|
@ -269,8 +276,8 @@ describe('Undo/Redo', () => {
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should undo/redo multiple steps', () => {
|
it('should undo/redo multiple steps', () => {
|
||||||
const initialPosition = 'left: 420px; top: 220px;';
|
const initialPosition = {left: '420px', top: '220px'};
|
||||||
const movedPosition = 'left: 540px; top: 360px;';
|
const movedPosition = {left: '540px', top: '360px'};
|
||||||
|
|
||||||
WorkflowPage.actions.addNodeToCanvas(SCHEDULE_TRIGGER_NODE_NAME);
|
WorkflowPage.actions.addNodeToCanvas(SCHEDULE_TRIGGER_NODE_NAME);
|
||||||
WorkflowPage.actions.addNodeToCanvas(CODE_NODE_NAME);
|
WorkflowPage.actions.addNodeToCanvas(CODE_NODE_NAME);
|
||||||
|
@ -283,10 +290,17 @@ describe('Undo/Redo', () => {
|
||||||
WorkflowPage.getters.canvasNodes().last().click();
|
WorkflowPage.getters.canvasNodes().last().click();
|
||||||
WorkflowPage.actions.hitDisableNodeShortcut();
|
WorkflowPage.actions.hitDisableNodeShortcut();
|
||||||
// Move first one
|
// Move first one
|
||||||
WorkflowPage.getters.canvasNodes().first().should('have.attr', 'style', initialPosition);
|
WorkflowPage.getters.canvasNodes()
|
||||||
|
.first()
|
||||||
|
.should('have.css', 'left', initialPosition.left)
|
||||||
|
.should('have.css', 'top', initialPosition.top)
|
||||||
|
|
||||||
WorkflowPage.getters.canvasNodes().first().click();
|
WorkflowPage.getters.canvasNodes().first().click();
|
||||||
cy.drag('[data-test-id="canvas-node"].jtk-drag-selected', [50, 150], { clickToFinish: true });
|
cy.drag('[data-test-id="canvas-node"].jtk-drag-selected', [50, 150], { clickToFinish: true });
|
||||||
WorkflowPage.getters.canvasNodes().first().should('have.attr', 'style', movedPosition);
|
WorkflowPage.getters.canvasNodes()
|
||||||
|
.first()
|
||||||
|
.should('have.css', 'left', movedPosition.left)
|
||||||
|
.should('have.css', 'top', movedPosition.top)
|
||||||
// Delete the set node
|
// Delete the set node
|
||||||
WorkflowPage.getters.canvasNodeByName(EDIT_FIELDS_SET_NODE_NAME).click().click();
|
WorkflowPage.getters.canvasNodeByName(EDIT_FIELDS_SET_NODE_NAME).click().click();
|
||||||
cy.get('body').type('{backspace}');
|
cy.get('body').type('{backspace}');
|
||||||
|
@ -297,7 +311,10 @@ describe('Undo/Redo', () => {
|
||||||
WorkflowPage.getters.nodeConnections().should('have.length', 3);
|
WorkflowPage.getters.nodeConnections().should('have.length', 3);
|
||||||
// Second undo: Should move first node to it's original position
|
// Second undo: Should move first node to it's original position
|
||||||
WorkflowPage.actions.hitUndo();
|
WorkflowPage.actions.hitUndo();
|
||||||
WorkflowPage.getters.canvasNodes().first().should('have.attr', 'style', initialPosition);
|
WorkflowPage.getters.canvasNodes()
|
||||||
|
.first()
|
||||||
|
.should('have.css', 'left', initialPosition.left)
|
||||||
|
.should('have.css', 'top', initialPosition.top)
|
||||||
// Third undo: Should enable last node
|
// Third undo: Should enable last node
|
||||||
WorkflowPage.actions.hitUndo();
|
WorkflowPage.actions.hitUndo();
|
||||||
WorkflowPage.getters.disabledNodes().should('have.length', 0);
|
WorkflowPage.getters.disabledNodes().should('have.length', 0);
|
||||||
|
@ -307,7 +324,10 @@ describe('Undo/Redo', () => {
|
||||||
WorkflowPage.getters.disabledNodes().should('have.length', 1);
|
WorkflowPage.getters.disabledNodes().should('have.length', 1);
|
||||||
// Second redo: Should move the first node
|
// Second redo: Should move the first node
|
||||||
WorkflowPage.actions.hitRedo();
|
WorkflowPage.actions.hitRedo();
|
||||||
WorkflowPage.getters.canvasNodes().first().should('have.attr', 'style', movedPosition);
|
WorkflowPage.getters.canvasNodes()
|
||||||
|
.first()
|
||||||
|
.should('have.css', 'left', movedPosition.left)
|
||||||
|
.should('have.css', 'top', movedPosition.top)
|
||||||
// Third redo: Should delete the Set node
|
// Third redo: Should delete the Set node
|
||||||
WorkflowPage.actions.hitRedo();
|
WorkflowPage.actions.hitRedo();
|
||||||
WorkflowPage.getters.canvasNodes().should('have.length', 3);
|
WorkflowPage.getters.canvasNodes().should('have.length', 3);
|
||||||
|
|
|
@ -133,7 +133,8 @@ describe('Canvas Actions', () => {
|
||||||
WorkflowPage.getters
|
WorkflowPage.getters
|
||||||
.canvasNodes()
|
.canvasNodes()
|
||||||
.last()
|
.last()
|
||||||
.should('have.attr', 'style', 'left: 860px; top: 220px;');
|
.should('have.css', 'left', '860px')
|
||||||
|
.should('have.css', 'top', '220px')
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should delete connections by pressing the delete button', () => {
|
it('should delete connections by pressing the delete button', () => {
|
||||||
|
|
|
@ -8,11 +8,11 @@ import {
|
||||||
MERGE_NODE_NAME,
|
MERGE_NODE_NAME,
|
||||||
} from './../constants';
|
} from './../constants';
|
||||||
import { WorkflowPage as WorkflowPageClass } from '../pages/workflow';
|
import { WorkflowPage as WorkflowPageClass } from '../pages/workflow';
|
||||||
import { WorkflowExecutionsTab } from '../pages';
|
import { NDV, WorkflowExecutionsTab } from '../pages';
|
||||||
|
|
||||||
const WorkflowPage = new WorkflowPageClass();
|
const WorkflowPage = new WorkflowPageClass();
|
||||||
const ExecutionsTab = new WorkflowExecutionsTab();
|
const ExecutionsTab = new WorkflowExecutionsTab();
|
||||||
|
const NDVDialog = new NDV();
|
||||||
const DEFAULT_ZOOM_FACTOR = 1;
|
const DEFAULT_ZOOM_FACTOR = 1;
|
||||||
const ZOOM_IN_X1_FACTOR = 1.25; // Zoom in factor after one click
|
const ZOOM_IN_X1_FACTOR = 1.25; // Zoom in factor after one click
|
||||||
const ZOOM_IN_X2_FACTOR = 1.5625; // Zoom in factor after two clicks
|
const ZOOM_IN_X2_FACTOR = 1.5625; // Zoom in factor after two clicks
|
||||||
|
@ -29,10 +29,15 @@ describe('Canvas Node Manipulation and Navigation', () => {
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should add switch node and test connections', () => {
|
it('should add switch node and test connections', () => {
|
||||||
WorkflowPage.actions.addNodeToCanvas(SWITCH_NODE_NAME, true);
|
const desiredOutputs = 4;
|
||||||
|
WorkflowPage.actions.addNodeToCanvas(SWITCH_NODE_NAME, true, true);
|
||||||
|
|
||||||
// Switch has 4 output endpoints
|
for (let i = 0; i < desiredOutputs; i++) {
|
||||||
for (let i = 0; i < 4; i++) {
|
cy.contains('Add Routing Rule').click()
|
||||||
|
}
|
||||||
|
|
||||||
|
NDVDialog.actions.close()
|
||||||
|
for (let i = 0; i < desiredOutputs; i++) {
|
||||||
WorkflowPage.getters.canvasNodePlusEndpointByName(SWITCH_NODE_NAME, i).click({ force: true });
|
WorkflowPage.getters.canvasNodePlusEndpointByName(SWITCH_NODE_NAME, i).click({ force: true });
|
||||||
WorkflowPage.getters.nodeCreatorSearchBar().should('be.visible');
|
WorkflowPage.getters.nodeCreatorSearchBar().should('be.visible');
|
||||||
WorkflowPage.actions.addNodeToCanvas(EDIT_FIELDS_SET_NODE_NAME, false);
|
WorkflowPage.actions.addNodeToCanvas(EDIT_FIELDS_SET_NODE_NAME, false);
|
||||||
|
@ -42,7 +47,7 @@ describe('Canvas Node Manipulation and Navigation', () => {
|
||||||
cy.reload();
|
cy.reload();
|
||||||
cy.waitForLoad();
|
cy.waitForLoad();
|
||||||
// Make sure all connections are there after reload
|
// Make sure all connections are there after reload
|
||||||
for (let i = 0; i < 4; i++) {
|
for (let i = 0; i < desiredOutputs; i++) {
|
||||||
const setName = `${EDIT_FIELDS_SET_NODE_NAME}${i > 0 ? i : ''}`;
|
const setName = `${EDIT_FIELDS_SET_NODE_NAME}${i > 0 ? i : ''}`;
|
||||||
WorkflowPage.getters
|
WorkflowPage.getters
|
||||||
.canvasNodeInputEndpointByName(setName)
|
.canvasNodeInputEndpointByName(setName)
|
||||||
|
@ -167,7 +172,8 @@ describe('Canvas Node Manipulation and Navigation', () => {
|
||||||
WorkflowPage.getters
|
WorkflowPage.getters
|
||||||
.canvasNodes()
|
.canvasNodes()
|
||||||
.last()
|
.last()
|
||||||
.should('have.attr', 'style', 'left: 740px; top: 320px;');
|
.should('have.css', 'left', '740px')
|
||||||
|
.should('have.css', 'top', '320px')
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should zoom in', () => {
|
it('should zoom in', () => {
|
||||||
|
|
|
@ -184,7 +184,14 @@ import { nodeHelpers } from '@/mixins/nodeHelpers';
|
||||||
import { workflowHelpers } from '@/mixins/workflowHelpers';
|
import { workflowHelpers } from '@/mixins/workflowHelpers';
|
||||||
import { pinData } from '@/mixins/pinData';
|
import { pinData } from '@/mixins/pinData';
|
||||||
|
|
||||||
import type { IExecutionsSummary, INodeTypeDescription, ITaskData } from 'n8n-workflow';
|
import type {
|
||||||
|
ConnectionTypes,
|
||||||
|
IExecutionsSummary,
|
||||||
|
INodeInputConfiguration,
|
||||||
|
INodeOutputConfiguration,
|
||||||
|
INodeTypeDescription,
|
||||||
|
ITaskData,
|
||||||
|
} from 'n8n-workflow';
|
||||||
import { NodeConnectionType, NodeHelpers } from 'n8n-workflow';
|
import { NodeConnectionType, NodeHelpers } from 'n8n-workflow';
|
||||||
|
|
||||||
import NodeIcon from '@/components/NodeIcon.vue';
|
import NodeIcon from '@/components/NodeIcon.vue';
|
||||||
|
@ -357,9 +364,15 @@ export default defineComponent({
|
||||||
top: this.position[1] + 'px',
|
top: this.position[1] + 'px',
|
||||||
};
|
};
|
||||||
|
|
||||||
const nonMainInputs = this.inputs.filter((input) => input !== NodeConnectionType.Main);
|
const workflow = this.workflowsStore.getCurrentWorkflow();
|
||||||
|
const inputs =
|
||||||
|
NodeHelpers.getNodeInputs(workflow, this.node, this.nodeType) ||
|
||||||
|
([] as Array<ConnectionTypes | INodeInputConfiguration>);
|
||||||
|
const inputTypes = NodeHelpers.getConnectionTypes(inputs);
|
||||||
|
|
||||||
|
const nonMainInputs = inputTypes.filter((input) => input !== NodeConnectionType.Main);
|
||||||
if (nonMainInputs.length) {
|
if (nonMainInputs.length) {
|
||||||
const requiredNonMainInputs = this.inputs.filter(
|
const requiredNonMainInputs = inputs.filter(
|
||||||
(input) => typeof input !== 'string' && input.required,
|
(input) => typeof input !== 'string' && input.required,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
@ -373,6 +386,15 @@ export default defineComponent({
|
||||||
styles['--configurable-node-input-count'] = nonMainInputs.length + spacerCount;
|
styles['--configurable-node-input-count'] = nonMainInputs.length + spacerCount;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const outputs =
|
||||||
|
NodeHelpers.getNodeOutputs(workflow, this.node, this.nodeType) ||
|
||||||
|
([] as Array<ConnectionTypes | INodeOutputConfiguration>);
|
||||||
|
|
||||||
|
const outputTypes = NodeHelpers.getConnectionTypes(outputs);
|
||||||
|
|
||||||
|
const mainOutputs = outputTypes.filter((output) => output === NodeConnectionType.Main);
|
||||||
|
styles['--node-main-output-count'] = mainOutputs.length;
|
||||||
|
|
||||||
return styles;
|
return styles;
|
||||||
},
|
},
|
||||||
nodeClass(): object {
|
nodeClass(): object {
|
||||||
|
@ -698,7 +720,12 @@ export default defineComponent({
|
||||||
<style lang="scss" scoped>
|
<style lang="scss" scoped>
|
||||||
.node-wrapper {
|
.node-wrapper {
|
||||||
--node-width: 100px;
|
--node-width: 100px;
|
||||||
--node-height: 100px;
|
/*
|
||||||
|
Set the node height to 100px as a base.
|
||||||
|
Increase height by 20px for each output beyond the 4th one.
|
||||||
|
max(0, var(--node-main-output-count, 1) - 4) ensures that we only start counting after the 4th output.
|
||||||
|
*/
|
||||||
|
--node-height: calc(100px + max(0, var(--node-main-output-count, 1) - 4) * 20px);
|
||||||
|
|
||||||
--configurable-node-min-input-count: 4;
|
--configurable-node-min-input-count: 4;
|
||||||
--configurable-node-input-width: 65px;
|
--configurable-node-input-width: 65px;
|
||||||
|
@ -1158,6 +1185,7 @@ export default defineComponent({
|
||||||
--endpoint-size-small: 14px;
|
--endpoint-size-small: 14px;
|
||||||
--endpoint-size-medium: 18px;
|
--endpoint-size-medium: 18px;
|
||||||
--stalk-size: 40px;
|
--stalk-size: 40px;
|
||||||
|
--stalk-switch-size: 60px;
|
||||||
--stalk-success-size: 87px;
|
--stalk-success-size: 87px;
|
||||||
--stalk-success-size-without-label: 40px;
|
--stalk-success-size-without-label: 40px;
|
||||||
--stalk-long-size: 127px;
|
--stalk-long-size: 127px;
|
||||||
|
@ -1405,6 +1433,15 @@ export default defineComponent({
|
||||||
margin-top: calc(var(--spacing-l) * -1);
|
margin-top: calc(var(--spacing-l) * -1);
|
||||||
margin-left: 0;
|
margin-left: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Switch node allows for dynamic connection labels
|
||||||
|
// so we need to make sure the label does not overflow
|
||||||
|
&[data-endpoint-node-type='n8n-nodes-base.switch'] {
|
||||||
|
max-width: calc(var(--stalk-size) - (var(--endpoint-size-small)));
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
margin-left: calc(var(--endpoint-size-small) + var(--spacing-2xs) + 10px);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.node-input-endpoint-label {
|
.node-input-endpoint-label {
|
||||||
|
@ -1446,6 +1483,7 @@ export default defineComponent({
|
||||||
opacity: 1;
|
opacity: 1;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.long-stalk {
|
.long-stalk {
|
||||||
--stalk-size: var(--stalk-long-size);
|
--stalk-size: var(--stalk-long-size);
|
||||||
}
|
}
|
||||||
|
@ -1455,4 +1493,7 @@ export default defineComponent({
|
||||||
.ep-success--without-label {
|
.ep-success--without-label {
|
||||||
--stalk-size: var(--stalk-success-size-without-label);
|
--stalk-size: var(--stalk-success-size-without-label);
|
||||||
}
|
}
|
||||||
|
[data-endpoint-node-type='n8n-nodes-base.switch'] {
|
||||||
|
--stalk-size: var(--stalk-switch-size);
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|
|
@ -502,6 +502,7 @@ import type {
|
||||||
IBinaryKeyData,
|
IBinaryKeyData,
|
||||||
IDataObject,
|
IDataObject,
|
||||||
INodeExecutionData,
|
INodeExecutionData,
|
||||||
|
INodeOutputConfiguration,
|
||||||
INodeTypeDescription,
|
INodeTypeDescription,
|
||||||
IRunData,
|
IRunData,
|
||||||
IRunExecutionData,
|
IRunExecutionData,
|
||||||
|
@ -543,6 +544,7 @@ import { useWorkflowsStore } from '@/stores/workflows.store';
|
||||||
import { useNDVStore } from '@/stores/ndv.store';
|
import { useNDVStore } from '@/stores/ndv.store';
|
||||||
import { useNodeTypesStore } from '@/stores/nodeTypes.store';
|
import { useNodeTypesStore } from '@/stores/nodeTypes.store';
|
||||||
import { useToast } from '@/composables';
|
import { useToast } from '@/composables';
|
||||||
|
import { isObject } from 'lodash-es';
|
||||||
|
|
||||||
const RunDataTable = defineAsyncComponent(async () => import('@/components/RunDataTable.vue'));
|
const RunDataTable = defineAsyncComponent(async () => import('@/components/RunDataTable.vue'));
|
||||||
const RunDataJson = defineAsyncComponent(async () => import('@/components/RunDataJson.vue'));
|
const RunDataJson = defineAsyncComponent(async () => import('@/components/RunDataJson.vue'));
|
||||||
|
@ -891,9 +893,8 @@ export default defineComponent({
|
||||||
return this.outputIndex;
|
return this.outputIndex;
|
||||||
},
|
},
|
||||||
branches(): ITab[] {
|
branches(): ITab[] {
|
||||||
function capitalize(name: string) {
|
const capitalize = (name: string) => name.charAt(0).toLocaleUpperCase() + name.slice(1);
|
||||||
return name.charAt(0).toLocaleUpperCase() + name.slice(1);
|
|
||||||
}
|
|
||||||
const branches: ITab[] = [];
|
const branches: ITab[] = [];
|
||||||
|
|
||||||
for (let i = 0; i <= this.maxOutputIndex; i++) {
|
for (let i = 0; i <= this.maxOutputIndex; i++) {
|
||||||
|
@ -903,6 +904,7 @@ export default defineComponent({
|
||||||
const itemsCount = this.getDataCount(this.runIndex, i);
|
const itemsCount = this.getDataCount(this.runIndex, i);
|
||||||
const items = this.$locale.baseText('ndv.output.items', { adjustToNumber: itemsCount });
|
const items = this.$locale.baseText('ndv.output.items', { adjustToNumber: itemsCount });
|
||||||
let outputName = this.getOutputName(i);
|
let outputName = this.getOutputName(i);
|
||||||
|
|
||||||
if (`${outputName}` === `${i}`) {
|
if (`${outputName}` === `${i}`) {
|
||||||
outputName = `${this.$locale.baseText('ndv.output')} ${outputName}`;
|
outputName = `${this.$locale.baseText('ndv.output')} ${outputName}`;
|
||||||
} else {
|
} else {
|
||||||
|
@ -936,6 +938,18 @@ export default defineComponent({
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
|
getResolvedNodeOutputs() {
|
||||||
|
if (this.node && this.nodeType) {
|
||||||
|
const workflow = this.workflowsStore.getCurrentWorkflow();
|
||||||
|
const workflowNode = workflow.getNode(this.node.name);
|
||||||
|
|
||||||
|
if (workflowNode) {
|
||||||
|
const outputs = NodeHelpers.getNodeOutputs(workflow, workflowNode, this.nodeType);
|
||||||
|
return outputs;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return [];
|
||||||
|
},
|
||||||
onItemHover(itemIndex: number | null) {
|
onItemHover(itemIndex: number | null) {
|
||||||
if (itemIndex === null) {
|
if (itemIndex === null) {
|
||||||
this.$emit('itemHover', null);
|
this.$emit('itemHover', null);
|
||||||
|
@ -1287,9 +1301,7 @@ export default defineComponent({
|
||||||
this.closeBinaryDataDisplay();
|
this.closeBinaryDataDisplay();
|
||||||
let outputTypes: ConnectionTypes[] = [];
|
let outputTypes: ConnectionTypes[] = [];
|
||||||
if (this.nodeType !== null && this.node !== null) {
|
if (this.nodeType !== null && this.node !== null) {
|
||||||
const workflow = this.workflowsStore.getCurrentWorkflow();
|
const outputs = this.getResolvedNodeOutputs();
|
||||||
const workflowNode = workflow.getNode(this.node.name);
|
|
||||||
const outputs = NodeHelpers.getNodeOutputs(workflow, workflowNode, this.nodeType);
|
|
||||||
outputTypes = NodeHelpers.getConnectionTypes(outputs);
|
outputTypes = NodeHelpers.getConnectionTypes(outputs);
|
||||||
}
|
}
|
||||||
this.connectionType = outputTypes.length === 0 ? NodeConnectionType.Main : outputTypes[0];
|
this.connectionType = outputTypes.length === 0 ? NodeConnectionType.Main : outputTypes[0];
|
||||||
|
@ -1367,6 +1379,12 @@ export default defineComponent({
|
||||||
}
|
}
|
||||||
|
|
||||||
const nodeType = this.nodeType;
|
const nodeType = this.nodeType;
|
||||||
|
const outputs = this.getResolvedNodeOutputs();
|
||||||
|
const outputConfiguration = outputs?.[outputIndex] as INodeOutputConfiguration;
|
||||||
|
|
||||||
|
if (outputConfiguration && isObject(outputConfiguration)) {
|
||||||
|
return outputConfiguration?.displayName;
|
||||||
|
}
|
||||||
if (!nodeType?.outputNames || nodeType.outputNames.length <= outputIndex) {
|
if (!nodeType?.outputNames || nodeType.outputNames.length <= outputIndex) {
|
||||||
return outputIndex + 1;
|
return outputIndex + 1;
|
||||||
}
|
}
|
||||||
|
|
|
@ -595,7 +595,7 @@ export const MFA_AUTHENTICATION_TOKEN_INPUT_MAX_LENGTH = 6;
|
||||||
|
|
||||||
export const MFA_AUTHENTICATION_RECOVERY_CODE_INPUT_MAX_LENGTH = 36;
|
export const MFA_AUTHENTICATION_RECOVERY_CODE_INPUT_MAX_LENGTH = 36;
|
||||||
|
|
||||||
export const NODE_TYPES_EXCLUDED_FROM_OUTPUT_NAME_APPEND = [FILTER_NODE_TYPE];
|
export const NODE_TYPES_EXCLUDED_FROM_OUTPUT_NAME_APPEND = [FILTER_NODE_TYPE, SWITCH_NODE_TYPE];
|
||||||
|
|
||||||
export const ALLOWED_HTML_ATTRIBUTES = ['href', 'name', 'target', 'title', 'class', 'id', 'style'];
|
export const ALLOWED_HTML_ATTRIBUTES = ['href', 'name', 'target', 'title', 'class', 'id', 'style'];
|
||||||
|
|
||||||
|
|
|
@ -261,6 +261,7 @@ export const nodeBase = defineComponent({
|
||||||
nodeId: this.nodeId,
|
nodeId: this.nodeId,
|
||||||
index: typeIndex,
|
index: typeIndex,
|
||||||
totalEndpoints: inputsOfSameRootType.length,
|
totalEndpoints: inputsOfSameRootType.length,
|
||||||
|
nodeType: node.type,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -423,6 +424,7 @@ export const nodeBase = defineComponent({
|
||||||
this.$refs[this.data.name] as Element,
|
this.$refs[this.data.name] as Element,
|
||||||
newEndpointData,
|
newEndpointData,
|
||||||
);
|
);
|
||||||
|
|
||||||
this.__addEndpointTestingData(endpoint, 'output', typeIndex);
|
this.__addEndpointTestingData(endpoint, 'output', typeIndex);
|
||||||
if (outputConfiguration.displayName || nodeTypeData.outputNames?.[i]) {
|
if (outputConfiguration.displayName || nodeTypeData.outputNames?.[i]) {
|
||||||
// Apply output names if they got set
|
// Apply output names if they got set
|
||||||
|
@ -439,6 +441,7 @@ export const nodeBase = defineComponent({
|
||||||
nodeId: this.nodeId,
|
nodeId: this.nodeId,
|
||||||
index: typeIndex,
|
index: typeIndex,
|
||||||
totalEndpoints: outputsOfSameRootType.length,
|
totalEndpoints: outputsOfSameRootType.length,
|
||||||
|
nodeType: node.type,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -454,6 +457,7 @@ export const nodeBase = defineComponent({
|
||||||
connectedEndpoint: endpoint,
|
connectedEndpoint: endpoint,
|
||||||
showOutputLabel: outputs.length === 1,
|
showOutputLabel: outputs.length === 1,
|
||||||
size: outputs.length >= 3 ? 'small' : 'medium',
|
size: outputs.length >= 3 ? 'small' : 'medium',
|
||||||
|
nodeType: node.type,
|
||||||
hoverMessage: this.$locale.baseText('nodeBase.clickToAddNodeOrDragToConnect'),
|
hoverMessage: this.$locale.baseText('nodeBase.clickToAddNodeOrDragToConnect'),
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
@ -486,6 +490,7 @@ export const nodeBase = defineComponent({
|
||||||
nodeName: node.name,
|
nodeName: node.name,
|
||||||
nodeId: this.nodeId,
|
nodeId: this.nodeId,
|
||||||
index: typeIndex,
|
index: typeIndex,
|
||||||
|
nodeType: node.type,
|
||||||
totalEndpoints: outputsOfSameRootType.length,
|
totalEndpoints: outputsOfSameRootType.length,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
|
@ -14,6 +14,7 @@ interface N8nPlusEndpointParams extends EndpointRepresentationParams {
|
||||||
dimensions: number;
|
dimensions: number;
|
||||||
connectedEndpoint: Endpoint;
|
connectedEndpoint: Endpoint;
|
||||||
hoverMessage: string;
|
hoverMessage: string;
|
||||||
|
nodeType: string;
|
||||||
size: 'small' | 'medium';
|
size: 'small' | 'medium';
|
||||||
showOutputLabel: boolean;
|
showOutputLabel: boolean;
|
||||||
}
|
}
|
||||||
|
@ -23,15 +24,17 @@ export const N8nPlusEndpointType = 'N8nPlus';
|
||||||
export const EVENT_PLUS_ENDPOINT_CLICK = 'eventPlusEndpointClick';
|
export const EVENT_PLUS_ENDPOINT_CLICK = 'eventPlusEndpointClick';
|
||||||
export class N8nPlusEndpoint extends EndpointRepresentation<ComputedN8nPlusEndpoint> {
|
export class N8nPlusEndpoint extends EndpointRepresentation<ComputedN8nPlusEndpoint> {
|
||||||
params: N8nPlusEndpointParams;
|
params: N8nPlusEndpointParams;
|
||||||
|
|
||||||
label: string;
|
label: string;
|
||||||
|
|
||||||
stalkOverlay: Overlay | null;
|
stalkOverlay: Overlay | null;
|
||||||
|
|
||||||
messageOverlay: Overlay | null;
|
messageOverlay: Overlay | null;
|
||||||
|
|
||||||
constructor(endpoint: Endpoint, params: N8nPlusEndpointParams) {
|
constructor(endpoint: Endpoint, params: N8nPlusEndpointParams) {
|
||||||
super(endpoint, params);
|
super(endpoint, params);
|
||||||
|
|
||||||
this.params = params;
|
this.params = params;
|
||||||
|
|
||||||
this.label = '';
|
this.label = '';
|
||||||
this.stalkOverlay = null;
|
this.stalkOverlay = null;
|
||||||
this.messageOverlay = null;
|
this.messageOverlay = null;
|
||||||
|
@ -41,6 +44,7 @@ export class N8nPlusEndpoint extends EndpointRepresentation<ComputedN8nPlusEndpo
|
||||||
}
|
}
|
||||||
|
|
||||||
static type = N8nPlusEndpointType;
|
static type = N8nPlusEndpointType;
|
||||||
|
|
||||||
type = N8nPlusEndpoint.type;
|
type = N8nPlusEndpoint.type;
|
||||||
|
|
||||||
setupOverlays() {
|
setupOverlays() {
|
||||||
|
@ -50,6 +54,9 @@ export class N8nPlusEndpoint extends EndpointRepresentation<ComputedN8nPlusEndpo
|
||||||
type: 'Custom',
|
type: 'Custom',
|
||||||
options: {
|
options: {
|
||||||
id: PlusStalkOverlay,
|
id: PlusStalkOverlay,
|
||||||
|
attributes: {
|
||||||
|
'data-endpoint-node-type': this.params.nodeType,
|
||||||
|
},
|
||||||
create: () => {
|
create: () => {
|
||||||
const stalk = createElement('div', {}, `${PlusStalkOverlay} ${this.params.size}`);
|
const stalk = createElement('div', {}, `${PlusStalkOverlay} ${this.params.size}`);
|
||||||
return stalk;
|
return stalk;
|
||||||
|
@ -61,6 +68,9 @@ export class N8nPlusEndpoint extends EndpointRepresentation<ComputedN8nPlusEndpo
|
||||||
options: {
|
options: {
|
||||||
id: HoverMessageOverlay,
|
id: HoverMessageOverlay,
|
||||||
location: 0.5,
|
location: 0.5,
|
||||||
|
attributes: {
|
||||||
|
'data-endpoint-node-type': this.params.nodeType,
|
||||||
|
},
|
||||||
create: () => {
|
create: () => {
|
||||||
const hoverMessage = createElement('p', {}, `${HoverMessageOverlay} ${this.params.size}`);
|
const hoverMessage = createElement('p', {}, `${HoverMessageOverlay} ${this.params.size}`);
|
||||||
hoverMessage.innerHTML = this.params.hoverMessage;
|
hoverMessage.innerHTML = this.params.hoverMessage;
|
||||||
|
@ -70,23 +80,27 @@ export class N8nPlusEndpoint extends EndpointRepresentation<ComputedN8nPlusEndpo
|
||||||
});
|
});
|
||||||
this.endpoint.instance.setSuspendDrawing(false);
|
this.endpoint.instance.setSuspendDrawing(false);
|
||||||
}
|
}
|
||||||
|
|
||||||
bindEvents() {
|
bindEvents() {
|
||||||
this.instance.bind(EVENT_ENDPOINT_MOUSEOVER, this.setHoverMessageVisible);
|
this.instance.bind(EVENT_ENDPOINT_MOUSEOVER, this.setHoverMessageVisible);
|
||||||
this.instance.bind(EVENT_ENDPOINT_MOUSEOUT, this.unsetHoverMessageVisible);
|
this.instance.bind(EVENT_ENDPOINT_MOUSEOUT, this.unsetHoverMessageVisible);
|
||||||
this.instance.bind(EVENT_ENDPOINT_CLICK, this.fireClickEvent);
|
this.instance.bind(EVENT_ENDPOINT_CLICK, this.fireClickEvent);
|
||||||
this.instance.bind(EVENT_CONNECTION_ABORT, this.setStalkLabels);
|
this.instance.bind(EVENT_CONNECTION_ABORT, this.setStalkLabels);
|
||||||
}
|
}
|
||||||
|
|
||||||
unbindEvents() {
|
unbindEvents() {
|
||||||
this.instance.unbind(EVENT_ENDPOINT_MOUSEOVER, this.setHoverMessageVisible);
|
this.instance.unbind(EVENT_ENDPOINT_MOUSEOVER, this.setHoverMessageVisible);
|
||||||
this.instance.unbind(EVENT_ENDPOINT_MOUSEOUT, this.unsetHoverMessageVisible);
|
this.instance.unbind(EVENT_ENDPOINT_MOUSEOUT, this.unsetHoverMessageVisible);
|
||||||
this.instance.unbind(EVENT_ENDPOINT_CLICK, this.fireClickEvent);
|
this.instance.unbind(EVENT_ENDPOINT_CLICK, this.fireClickEvent);
|
||||||
this.instance.unbind(EVENT_CONNECTION_ABORT, this.setStalkLabels);
|
this.instance.unbind(EVENT_CONNECTION_ABORT, this.setStalkLabels);
|
||||||
}
|
}
|
||||||
|
|
||||||
setStalkLabels = () => {
|
setStalkLabels = () => {
|
||||||
if (!this.endpoint) return;
|
if (!this.endpoint) return;
|
||||||
|
|
||||||
const stalkOverlay = this.endpoint.getOverlay(PlusStalkOverlay);
|
const stalkOverlay = this.endpoint.getOverlay(PlusStalkOverlay);
|
||||||
const messageOverlay = this.endpoint.getOverlay(HoverMessageOverlay);
|
const messageOverlay = this.endpoint.getOverlay(HoverMessageOverlay);
|
||||||
|
|
||||||
if (stalkOverlay && messageOverlay) {
|
if (stalkOverlay && messageOverlay) {
|
||||||
// Increase the size of the stalk overlay if the label is too long
|
// Increase the size of the stalk overlay if the label is too long
|
||||||
const fnKey = this.label.length > 10 ? 'add' : 'remove';
|
const fnKey = this.label.length > 10 ? 'add' : 'remove';
|
||||||
|
@ -100,21 +114,25 @@ export class N8nPlusEndpoint extends EndpointRepresentation<ComputedN8nPlusEndpo
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
fireClickEvent = (endpoint: Endpoint) => {
|
fireClickEvent = (endpoint: Endpoint) => {
|
||||||
if (endpoint === this.endpoint) {
|
if (endpoint === this.endpoint) {
|
||||||
this.instance.fire(EVENT_PLUS_ENDPOINT_CLICK, this.endpoint);
|
this.instance.fire(EVENT_PLUS_ENDPOINT_CLICK, this.endpoint);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
setHoverMessageVisible = (endpoint: Endpoint) => {
|
setHoverMessageVisible = (endpoint: Endpoint) => {
|
||||||
if (endpoint === this.endpoint && this.messageOverlay) {
|
if (endpoint === this.endpoint && this.messageOverlay) {
|
||||||
this.instance.addOverlayClass(this.messageOverlay, 'visible');
|
this.instance.addOverlayClass(this.messageOverlay, 'visible');
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
unsetHoverMessageVisible = (endpoint: Endpoint) => {
|
unsetHoverMessageVisible = (endpoint: Endpoint) => {
|
||||||
if (endpoint === this.endpoint && this.messageOverlay) {
|
if (endpoint === this.endpoint && this.messageOverlay) {
|
||||||
this.instance.removeOverlayClass(this.messageOverlay, 'visible');
|
this.instance.removeOverlayClass(this.messageOverlay, 'visible');
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
clearOverlays() {
|
clearOverlays() {
|
||||||
Object.keys(this.endpoint.getOverlays()).forEach((key) => {
|
Object.keys(this.endpoint.getOverlays()).forEach((key) => {
|
||||||
this.endpoint.removeOverlay(key);
|
this.endpoint.removeOverlay(key);
|
||||||
|
@ -122,6 +140,7 @@ export class N8nPlusEndpoint extends EndpointRepresentation<ComputedN8nPlusEndpo
|
||||||
this.stalkOverlay = null;
|
this.stalkOverlay = null;
|
||||||
this.messageOverlay = null;
|
this.messageOverlay = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
getConnections() {
|
getConnections() {
|
||||||
const connections = [
|
const connections = [
|
||||||
...this.endpoint.connections,
|
...this.endpoint.connections,
|
||||||
|
@ -130,6 +149,7 @@ export class N8nPlusEndpoint extends EndpointRepresentation<ComputedN8nPlusEndpo
|
||||||
|
|
||||||
return connections;
|
return connections;
|
||||||
}
|
}
|
||||||
|
|
||||||
setIsVisible(visible: boolean) {
|
setIsVisible(visible: boolean) {
|
||||||
this.instance.setSuspendDrawing(true);
|
this.instance.setSuspendDrawing(true);
|
||||||
Object.keys(this.endpoint.getOverlays()).forEach((overlay) => {
|
Object.keys(this.endpoint.getOverlays()).forEach((overlay) => {
|
||||||
|
@ -179,6 +199,7 @@ export const N8nPlusEndpointHandler: EndpointHandler<N8nPlusEndpoint, ComputedN8
|
||||||
ep.w = w;
|
ep.w = w;
|
||||||
ep.h = h;
|
ep.h = h;
|
||||||
|
|
||||||
|
ep.canvas?.setAttribute('data-endpoint-node-type', ep.params.nodeType);
|
||||||
ep.addClass('plus-endpoint');
|
ep.addClass('plus-endpoint');
|
||||||
return [x, y, w, h, ep.params.dimensions];
|
return [x, y, w, h, ep.params.dimensions];
|
||||||
},
|
},
|
||||||
|
|
|
@ -181,46 +181,18 @@ export const getAnchorPosition = (
|
||||||
spacerIndexes: number[] = [],
|
spacerIndexes: number[] = [],
|
||||||
): ArrayAnchorSpec[] => {
|
): ArrayAnchorSpec[] => {
|
||||||
if (connectionType === NodeConnectionType.Main) {
|
if (connectionType === NodeConnectionType.Main) {
|
||||||
const positions = {
|
const anchors: ArrayAnchorSpec[] = [];
|
||||||
input: {
|
const x = type === 'input' ? 0.01 : 0.99;
|
||||||
1: [[0.01, 0.5, -1, 0]],
|
const ox = type === 'input' ? -1 : 1;
|
||||||
2: [
|
const oy = 0;
|
||||||
[0.01, 0.3, -1, 0],
|
const stepSize = 1 / (amount + 1); // +1 to not touch the node boundaries
|
||||||
[0.01, 0.7, -1, 0],
|
|
||||||
],
|
|
||||||
3: [
|
|
||||||
[0.01, 0.25, -1, 0],
|
|
||||||
[0.01, 0.5, -1, 0],
|
|
||||||
[0.01, 0.75, -1, 0],
|
|
||||||
],
|
|
||||||
4: [
|
|
||||||
[0.01, 0.2, -1, 0],
|
|
||||||
[0.01, 0.4, -1, 0],
|
|
||||||
[0.01, 0.6, -1, 0],
|
|
||||||
[0.01, 0.8, -1, 0],
|
|
||||||
],
|
|
||||||
},
|
|
||||||
output: {
|
|
||||||
1: [[0.99, 0.5, 1, 0]],
|
|
||||||
2: [
|
|
||||||
[0.99, 0.3, 1, 0],
|
|
||||||
[0.99, 0.7, 1, 0],
|
|
||||||
],
|
|
||||||
3: [
|
|
||||||
[0.99, 0.25, 1, 0],
|
|
||||||
[0.99, 0.5, 1, 0],
|
|
||||||
[0.99, 0.75, 1, 0],
|
|
||||||
],
|
|
||||||
4: [
|
|
||||||
[0.99, 0.2, 1, 0],
|
|
||||||
[0.99, 0.4, 1, 0],
|
|
||||||
[0.99, 0.6, 1, 0],
|
|
||||||
[0.99, 0.8, 1, 0],
|
|
||||||
],
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
return positions[type][amount] as ArrayAnchorSpec[];
|
for (let i = 1; i <= amount; i++) {
|
||||||
|
const y = stepSize * i; // Multiply by index to set position
|
||||||
|
anchors.push([x, y, ox, oy]);
|
||||||
|
}
|
||||||
|
|
||||||
|
return anchors;
|
||||||
}
|
}
|
||||||
|
|
||||||
const y = type === 'input' ? 0.99 : 0.01;
|
const y = type === 'input' ? 0.99 : 0.01;
|
||||||
|
@ -317,15 +289,20 @@ export const getOutputEndpointStyle = (
|
||||||
outlineStroke: 'none',
|
outlineStroke: 'none',
|
||||||
});
|
});
|
||||||
|
|
||||||
export const getOutputNameOverlay = (labelText: string, outputName: string): OverlaySpec => ({
|
export const getOutputNameOverlay = (
|
||||||
|
labelText: string,
|
||||||
|
outputName: ConnectionTypes,
|
||||||
|
): OverlaySpec => ({
|
||||||
type: 'Custom',
|
type: 'Custom',
|
||||||
options: {
|
options: {
|
||||||
id: OVERLAY_OUTPUT_NAME_LABEL,
|
id: OVERLAY_OUTPUT_NAME_LABEL,
|
||||||
visible: true,
|
visible: true,
|
||||||
create: (component: Endpoint) => {
|
create: (ep: Endpoint) => {
|
||||||
const label = document.createElement('div');
|
const label = document.createElement('div');
|
||||||
label.innerHTML = labelText;
|
label.innerHTML = labelText;
|
||||||
label.classList.add('node-output-endpoint-label');
|
label.classList.add('node-output-endpoint-label');
|
||||||
|
|
||||||
|
label.setAttribute('data-endpoint-node-type', ep?.__meta?.nodeType);
|
||||||
if (outputName !== NodeConnectionType.Main) {
|
if (outputName !== NodeConnectionType.Main) {
|
||||||
label.classList.add('node-output-endpoint-label--data');
|
label.classList.add('node-output-endpoint-label--data');
|
||||||
label.classList.add(`node-connection-type-${getScope(outputName)}`);
|
label.classList.add(`node-connection-type-${getScope(outputName)}`);
|
||||||
|
|
|
@ -1,686 +1,25 @@
|
||||||
import type {
|
import type { INodeTypeBaseDescription, IVersionedNodeType } from 'n8n-workflow';
|
||||||
IExecuteFunctions,
|
import { VersionedNodeType } from 'n8n-workflow';
|
||||||
INodeExecutionData,
|
|
||||||
INodeParameters,
|
|
||||||
INodeType,
|
|
||||||
INodeTypeDescription,
|
|
||||||
NodeParameterValue,
|
|
||||||
} from 'n8n-workflow';
|
|
||||||
import { NodeOperationError } from 'n8n-workflow';
|
|
||||||
|
|
||||||
export class Switch implements INodeType {
|
import { SwitchV1 } from './V1/SwitchV1.node';
|
||||||
description: INodeTypeDescription = {
|
import { SwitchV2 } from './V2/SwitchV2.node';
|
||||||
displayName: 'Switch',
|
|
||||||
name: 'switch',
|
|
||||||
icon: 'fa:map-signs',
|
|
||||||
group: ['transform'],
|
|
||||||
version: 1,
|
|
||||||
description: 'Route items depending on defined expression or rules',
|
|
||||||
defaults: {
|
|
||||||
name: 'Switch',
|
|
||||||
color: '#506000',
|
|
||||||
},
|
|
||||||
inputs: ['main'],
|
|
||||||
// eslint-disable-next-line n8n-nodes-base/node-class-description-outputs-wrong
|
|
||||||
outputs: ['main', 'main', 'main', 'main'],
|
|
||||||
outputNames: ['0', '1', '2', '3'],
|
|
||||||
properties: [
|
|
||||||
{
|
|
||||||
displayName: 'Mode',
|
|
||||||
name: 'mode',
|
|
||||||
type: 'options',
|
|
||||||
options: [
|
|
||||||
{
|
|
||||||
name: 'Expression',
|
|
||||||
value: 'expression',
|
|
||||||
description: 'Expression decides how to route data',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'Rules',
|
|
||||||
value: 'rules',
|
|
||||||
description: 'Rules decide how to route data',
|
|
||||||
},
|
|
||||||
],
|
|
||||||
default: 'rules',
|
|
||||||
description: 'How data should be routed',
|
|
||||||
},
|
|
||||||
|
|
||||||
// ----------------------------------
|
export class Switch extends VersionedNodeType {
|
||||||
// mode:expression
|
constructor() {
|
||||||
// ----------------------------------
|
const baseDescription: INodeTypeBaseDescription = {
|
||||||
{
|
displayName: 'Switch',
|
||||||
displayName: 'Output',
|
name: 'switch',
|
||||||
name: 'output',
|
icon: 'fa:map-signs',
|
||||||
type: 'number',
|
group: ['transform'],
|
||||||
typeOptions: {
|
description: 'Route items depending on defined expression or rules',
|
||||||
minValue: 0,
|
defaultVersion: 2,
|
||||||
maxValue: 3,
|
|
||||||
},
|
|
||||||
displayOptions: {
|
|
||||||
show: {
|
|
||||||
mode: ['expression'],
|
|
||||||
},
|
|
||||||
},
|
|
||||||
default: 0,
|
|
||||||
description: 'The index of output to which to send data to',
|
|
||||||
},
|
|
||||||
|
|
||||||
// ----------------------------------
|
|
||||||
// mode:rules
|
|
||||||
// ----------------------------------
|
|
||||||
{
|
|
||||||
displayName: 'Data Type',
|
|
||||||
name: 'dataType',
|
|
||||||
type: 'options',
|
|
||||||
displayOptions: {
|
|
||||||
show: {
|
|
||||||
mode: ['rules'],
|
|
||||||
},
|
|
||||||
},
|
|
||||||
options: [
|
|
||||||
{
|
|
||||||
name: 'Boolean',
|
|
||||||
value: 'boolean',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'Date & Time',
|
|
||||||
value: 'dateTime',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'Number',
|
|
||||||
value: 'number',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'String',
|
|
||||||
value: 'string',
|
|
||||||
},
|
|
||||||
],
|
|
||||||
default: 'number',
|
|
||||||
description: 'The type of data to route on',
|
|
||||||
},
|
|
||||||
|
|
||||||
// ----------------------------------
|
|
||||||
// dataType:boolean
|
|
||||||
// ----------------------------------
|
|
||||||
{
|
|
||||||
displayName: 'Value 1',
|
|
||||||
name: 'value1',
|
|
||||||
type: 'boolean',
|
|
||||||
displayOptions: {
|
|
||||||
show: {
|
|
||||||
dataType: ['boolean'],
|
|
||||||
mode: ['rules'],
|
|
||||||
},
|
|
||||||
},
|
|
||||||
default: false,
|
|
||||||
// eslint-disable-next-line n8n-nodes-base/node-param-description-boolean-without-whether
|
|
||||||
description: 'The value to compare with the first one',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
displayName: 'Routing Rules',
|
|
||||||
name: 'rules',
|
|
||||||
placeholder: 'Add Routing Rule',
|
|
||||||
type: 'fixedCollection',
|
|
||||||
typeOptions: {
|
|
||||||
multipleValues: true,
|
|
||||||
},
|
|
||||||
displayOptions: {
|
|
||||||
show: {
|
|
||||||
dataType: ['boolean'],
|
|
||||||
mode: ['rules'],
|
|
||||||
},
|
|
||||||
},
|
|
||||||
default: {},
|
|
||||||
options: [
|
|
||||||
{
|
|
||||||
name: 'rules',
|
|
||||||
displayName: 'Boolean',
|
|
||||||
values: [
|
|
||||||
// eslint-disable-next-line n8n-nodes-base/node-param-operation-without-no-data-expression
|
|
||||||
{
|
|
||||||
displayName: 'Operation',
|
|
||||||
name: 'operation',
|
|
||||||
type: 'options',
|
|
||||||
options: [
|
|
||||||
{
|
|
||||||
name: 'Equal',
|
|
||||||
value: 'equal',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'Not Equal',
|
|
||||||
value: 'notEqual',
|
|
||||||
},
|
|
||||||
],
|
|
||||||
default: 'equal',
|
|
||||||
description: 'Operation to decide where the the data should be mapped to',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
displayName: 'Value 2',
|
|
||||||
name: 'value2',
|
|
||||||
type: 'boolean',
|
|
||||||
default: false,
|
|
||||||
// eslint-disable-next-line n8n-nodes-base/node-param-description-boolean-without-whether
|
|
||||||
description: 'The value to compare with the first one',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
displayName: 'Output',
|
|
||||||
name: 'output',
|
|
||||||
type: 'number',
|
|
||||||
typeOptions: {
|
|
||||||
minValue: 0,
|
|
||||||
maxValue: 3,
|
|
||||||
},
|
|
||||||
default: 0,
|
|
||||||
description: 'The index of output to which to send data to if rule matches',
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
|
|
||||||
// ----------------------------------
|
|
||||||
// dataType:dateTime
|
|
||||||
// ----------------------------------
|
|
||||||
{
|
|
||||||
displayName: 'Value 1',
|
|
||||||
name: 'value1',
|
|
||||||
type: 'dateTime',
|
|
||||||
displayOptions: {
|
|
||||||
show: {
|
|
||||||
dataType: ['dateTime'],
|
|
||||||
mode: ['rules'],
|
|
||||||
},
|
|
||||||
},
|
|
||||||
default: '',
|
|
||||||
description: 'The value to compare with the second one',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
displayName: 'Routing Rules',
|
|
||||||
name: 'rules',
|
|
||||||
placeholder: 'Add Routing Rule',
|
|
||||||
type: 'fixedCollection',
|
|
||||||
typeOptions: {
|
|
||||||
multipleValues: true,
|
|
||||||
},
|
|
||||||
displayOptions: {
|
|
||||||
show: {
|
|
||||||
dataType: ['dateTime'],
|
|
||||||
mode: ['rules'],
|
|
||||||
},
|
|
||||||
},
|
|
||||||
default: {},
|
|
||||||
options: [
|
|
||||||
{
|
|
||||||
name: 'rules',
|
|
||||||
displayName: 'Dates',
|
|
||||||
values: [
|
|
||||||
// eslint-disable-next-line n8n-nodes-base/node-param-operation-without-no-data-expression
|
|
||||||
{
|
|
||||||
displayName: 'Operation',
|
|
||||||
name: 'operation',
|
|
||||||
type: 'options',
|
|
||||||
options: [
|
|
||||||
{
|
|
||||||
name: 'Occurred After',
|
|
||||||
value: 'after',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'Occurred Before',
|
|
||||||
value: 'before',
|
|
||||||
},
|
|
||||||
],
|
|
||||||
default: 'after',
|
|
||||||
description: 'Operation to decide where the the data should be mapped to',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
displayName: 'Value 2',
|
|
||||||
name: 'value2',
|
|
||||||
type: 'dateTime',
|
|
||||||
default: 0,
|
|
||||||
description: 'The value to compare with the first one',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
displayName: 'Output',
|
|
||||||
name: 'output',
|
|
||||||
type: 'number',
|
|
||||||
typeOptions: {
|
|
||||||
minValue: 0,
|
|
||||||
maxValue: 3,
|
|
||||||
},
|
|
||||||
default: 0,
|
|
||||||
description: 'The index of output to which to send data to if rule matches',
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
|
|
||||||
// ----------------------------------
|
|
||||||
// dataType:number
|
|
||||||
// ----------------------------------
|
|
||||||
{
|
|
||||||
displayName: 'Value 1',
|
|
||||||
name: 'value1',
|
|
||||||
type: 'number',
|
|
||||||
displayOptions: {
|
|
||||||
show: {
|
|
||||||
dataType: ['number'],
|
|
||||||
mode: ['rules'],
|
|
||||||
},
|
|
||||||
},
|
|
||||||
default: 0,
|
|
||||||
description: 'The value to compare with the second one',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
displayName: 'Routing Rules',
|
|
||||||
name: 'rules',
|
|
||||||
placeholder: 'Add Routing Rule',
|
|
||||||
type: 'fixedCollection',
|
|
||||||
typeOptions: {
|
|
||||||
multipleValues: true,
|
|
||||||
},
|
|
||||||
displayOptions: {
|
|
||||||
show: {
|
|
||||||
dataType: ['number'],
|
|
||||||
mode: ['rules'],
|
|
||||||
},
|
|
||||||
},
|
|
||||||
default: {},
|
|
||||||
options: [
|
|
||||||
{
|
|
||||||
name: 'rules',
|
|
||||||
displayName: 'Numbers',
|
|
||||||
values: [
|
|
||||||
// eslint-disable-next-line n8n-nodes-base/node-param-operation-without-no-data-expression
|
|
||||||
{
|
|
||||||
displayName: 'Operation',
|
|
||||||
name: 'operation',
|
|
||||||
type: 'options',
|
|
||||||
// eslint-disable-next-line n8n-nodes-base/node-param-options-type-unsorted-items
|
|
||||||
options: [
|
|
||||||
{
|
|
||||||
name: 'Smaller',
|
|
||||||
value: 'smaller',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'Smaller Equal',
|
|
||||||
value: 'smallerEqual',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'Equal',
|
|
||||||
value: 'equal',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'Not Equal',
|
|
||||||
value: 'notEqual',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'Larger',
|
|
||||||
value: 'larger',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'Larger Equal',
|
|
||||||
value: 'largerEqual',
|
|
||||||
},
|
|
||||||
],
|
|
||||||
default: 'smaller',
|
|
||||||
description: 'Operation to decide where the the data should be mapped to',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
displayName: 'Value 2',
|
|
||||||
name: 'value2',
|
|
||||||
type: 'number',
|
|
||||||
default: 0,
|
|
||||||
description: 'The value to compare with the first one',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
displayName: 'Output',
|
|
||||||
name: 'output',
|
|
||||||
type: 'number',
|
|
||||||
typeOptions: {
|
|
||||||
minValue: 0,
|
|
||||||
maxValue: 3,
|
|
||||||
},
|
|
||||||
default: 0,
|
|
||||||
description: 'The index of output to which to send data to if rule matches',
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
|
|
||||||
// ----------------------------------
|
|
||||||
// dataType:string
|
|
||||||
// ----------------------------------
|
|
||||||
{
|
|
||||||
displayName: 'Value 1',
|
|
||||||
name: 'value1',
|
|
||||||
type: 'string',
|
|
||||||
displayOptions: {
|
|
||||||
show: {
|
|
||||||
dataType: ['string'],
|
|
||||||
mode: ['rules'],
|
|
||||||
},
|
|
||||||
},
|
|
||||||
default: '',
|
|
||||||
description: 'The value to compare with the second one',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
displayName: 'Routing Rules',
|
|
||||||
name: 'rules',
|
|
||||||
placeholder: 'Add Routing Rule',
|
|
||||||
type: 'fixedCollection',
|
|
||||||
typeOptions: {
|
|
||||||
multipleValues: true,
|
|
||||||
},
|
|
||||||
displayOptions: {
|
|
||||||
show: {
|
|
||||||
dataType: ['string'],
|
|
||||||
mode: ['rules'],
|
|
||||||
},
|
|
||||||
},
|
|
||||||
default: {},
|
|
||||||
options: [
|
|
||||||
{
|
|
||||||
name: 'rules',
|
|
||||||
displayName: 'Strings',
|
|
||||||
values: [
|
|
||||||
// eslint-disable-next-line n8n-nodes-base/node-param-operation-without-no-data-expression
|
|
||||||
{
|
|
||||||
displayName: 'Operation',
|
|
||||||
name: 'operation',
|
|
||||||
type: 'options',
|
|
||||||
// eslint-disable-next-line n8n-nodes-base/node-param-options-type-unsorted-items
|
|
||||||
options: [
|
|
||||||
{
|
|
||||||
name: 'Contains',
|
|
||||||
value: 'contains',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'Not Contains',
|
|
||||||
value: 'notContains',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'Ends With',
|
|
||||||
value: 'endsWith',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'Not Ends With',
|
|
||||||
value: 'notEndsWith',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'Equal',
|
|
||||||
value: 'equal',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'Not Equal',
|
|
||||||
value: 'notEqual',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'Regex Match',
|
|
||||||
value: 'regex',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'Regex Not Match',
|
|
||||||
value: 'notRegex',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'Starts With',
|
|
||||||
value: 'startsWith',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'Not Starts With',
|
|
||||||
value: 'notStartsWith',
|
|
||||||
},
|
|
||||||
],
|
|
||||||
default: 'equal',
|
|
||||||
description: 'Operation to decide where the the data should be mapped to',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
displayName: 'Value 2',
|
|
||||||
name: 'value2',
|
|
||||||
type: 'string',
|
|
||||||
displayOptions: {
|
|
||||||
hide: {
|
|
||||||
operation: ['regex', 'notRegex'],
|
|
||||||
},
|
|
||||||
},
|
|
||||||
default: '',
|
|
||||||
description: 'The value to compare with the first one',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
displayName: 'Regex',
|
|
||||||
name: 'value2',
|
|
||||||
type: 'string',
|
|
||||||
displayOptions: {
|
|
||||||
show: {
|
|
||||||
operation: ['regex', 'notRegex'],
|
|
||||||
},
|
|
||||||
},
|
|
||||||
default: '',
|
|
||||||
placeholder: '/text/i',
|
|
||||||
description: 'The regex which has to match',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
displayName: 'Output',
|
|
||||||
name: 'output',
|
|
||||||
type: 'number',
|
|
||||||
typeOptions: {
|
|
||||||
minValue: 0,
|
|
||||||
maxValue: 3,
|
|
||||||
},
|
|
||||||
default: 0,
|
|
||||||
description: 'The index of output to which to send data to if rule matches',
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
|
|
||||||
{
|
|
||||||
displayName: 'Fallback Output',
|
|
||||||
name: 'fallbackOutput',
|
|
||||||
type: 'options',
|
|
||||||
displayOptions: {
|
|
||||||
show: {
|
|
||||||
mode: ['rules'],
|
|
||||||
},
|
|
||||||
},
|
|
||||||
// eslint-disable-next-line n8n-nodes-base/node-param-options-type-unsorted-items
|
|
||||||
options: [
|
|
||||||
{
|
|
||||||
name: 'None',
|
|
||||||
value: -1,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: '0',
|
|
||||||
value: 0,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: '1',
|
|
||||||
value: 1,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: '2',
|
|
||||||
value: 2,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: '3',
|
|
||||||
value: 3,
|
|
||||||
},
|
|
||||||
],
|
|
||||||
default: -1,
|
|
||||||
description: 'The output to which to route all items which do not match any of the rules',
|
|
||||||
},
|
|
||||||
],
|
|
||||||
};
|
|
||||||
|
|
||||||
async execute(this: IExecuteFunctions): Promise<INodeExecutionData[][]> {
|
|
||||||
const returnData: INodeExecutionData[][] = [[], [], [], []];
|
|
||||||
|
|
||||||
const items = this.getInputData();
|
|
||||||
|
|
||||||
let compareOperationResult: boolean;
|
|
||||||
let item: INodeExecutionData;
|
|
||||||
let mode: string;
|
|
||||||
let outputIndex: number;
|
|
||||||
let ruleData: INodeParameters;
|
|
||||||
|
|
||||||
// The compare operations
|
|
||||||
const compareOperationFunctions: {
|
|
||||||
[key: string]: (value1: NodeParameterValue, value2: NodeParameterValue) => boolean;
|
|
||||||
} = {
|
|
||||||
after: (value1: NodeParameterValue, value2: NodeParameterValue) =>
|
|
||||||
(value1 || 0) > (value2 || 0),
|
|
||||||
before: (value1: NodeParameterValue, value2: NodeParameterValue) =>
|
|
||||||
(value1 || 0) < (value2 || 0),
|
|
||||||
contains: (value1: NodeParameterValue, value2: NodeParameterValue) =>
|
|
||||||
(value1 || '').toString().includes((value2 || '').toString()),
|
|
||||||
notContains: (value1: NodeParameterValue, value2: NodeParameterValue) =>
|
|
||||||
!(value1 || '').toString().includes((value2 || '').toString()),
|
|
||||||
endsWith: (value1: NodeParameterValue, value2: NodeParameterValue) =>
|
|
||||||
(value1 as string).endsWith(value2 as string),
|
|
||||||
notEndsWith: (value1: NodeParameterValue, value2: NodeParameterValue) =>
|
|
||||||
!(value1 as string).endsWith(value2 as string),
|
|
||||||
equal: (value1: NodeParameterValue, value2: NodeParameterValue) => value1 === value2,
|
|
||||||
notEqual: (value1: NodeParameterValue, value2: NodeParameterValue) => value1 !== value2,
|
|
||||||
larger: (value1: NodeParameterValue, value2: NodeParameterValue) =>
|
|
||||||
(value1 || 0) > (value2 || 0),
|
|
||||||
largerEqual: (value1: NodeParameterValue, value2: NodeParameterValue) =>
|
|
||||||
(value1 || 0) >= (value2 || 0),
|
|
||||||
smaller: (value1: NodeParameterValue, value2: NodeParameterValue) =>
|
|
||||||
(value1 || 0) < (value2 || 0),
|
|
||||||
smallerEqual: (value1: NodeParameterValue, value2: NodeParameterValue) =>
|
|
||||||
(value1 || 0) <= (value2 || 0),
|
|
||||||
startsWith: (value1: NodeParameterValue, value2: NodeParameterValue) =>
|
|
||||||
(value1 as string).startsWith(value2 as string),
|
|
||||||
notStartsWith: (value1: NodeParameterValue, value2: NodeParameterValue) =>
|
|
||||||
!(value1 as string).startsWith(value2 as string),
|
|
||||||
regex: (value1: NodeParameterValue, value2: NodeParameterValue) => {
|
|
||||||
const regexMatch = (value2 || '').toString().match(new RegExp('^/(.*?)/([gimusy]*)$'));
|
|
||||||
|
|
||||||
let regex: RegExp;
|
|
||||||
if (!regexMatch) {
|
|
||||||
regex = new RegExp((value2 || '').toString());
|
|
||||||
} else if (regexMatch.length === 1) {
|
|
||||||
regex = new RegExp(regexMatch[1]);
|
|
||||||
} else {
|
|
||||||
regex = new RegExp(regexMatch[1], regexMatch[2]);
|
|
||||||
}
|
|
||||||
|
|
||||||
return !!(value1 || '').toString().match(regex);
|
|
||||||
},
|
|
||||||
notRegex: (value1: NodeParameterValue, value2: NodeParameterValue) => {
|
|
||||||
const regexMatch = (value2 || '').toString().match(new RegExp('^/(.*?)/([gimusy]*)$'));
|
|
||||||
|
|
||||||
let regex: RegExp;
|
|
||||||
if (!regexMatch) {
|
|
||||||
regex = new RegExp((value2 || '').toString());
|
|
||||||
} else if (regexMatch.length === 1) {
|
|
||||||
regex = new RegExp(regexMatch[1]);
|
|
||||||
} else {
|
|
||||||
regex = new RegExp(regexMatch[1], regexMatch[2]);
|
|
||||||
}
|
|
||||||
|
|
||||||
return !(value1 || '').toString().match(regex);
|
|
||||||
},
|
|
||||||
};
|
};
|
||||||
|
|
||||||
// Converts the input data of a dateTime into a number for easy compare
|
const nodeVersions: IVersionedNodeType['nodeVersions'] = {
|
||||||
const convertDateTime = (value: NodeParameterValue): number => {
|
1: new SwitchV1(baseDescription),
|
||||||
let returnValue: number | undefined = undefined;
|
2: new SwitchV2(baseDescription),
|
||||||
if (typeof value === 'string') {
|
|
||||||
returnValue = new Date(value).getTime();
|
|
||||||
} else if (typeof value === 'number') {
|
|
||||||
returnValue = value;
|
|
||||||
}
|
|
||||||
if ((value as unknown as object) instanceof Date) {
|
|
||||||
returnValue = (value as unknown as Date).getTime();
|
|
||||||
}
|
|
||||||
|
|
||||||
if (returnValue === undefined || isNaN(returnValue)) {
|
|
||||||
throw new NodeOperationError(
|
|
||||||
this.getNode(),
|
|
||||||
`The value "${value}" is not a valid DateTime.`,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
return returnValue;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const checkIndexRange = (index: number) => {
|
super(nodeVersions, baseDescription);
|
||||||
if (index < 0 || index >= returnData.length) {
|
|
||||||
throw new NodeOperationError(
|
|
||||||
this.getNode(),
|
|
||||||
`The ouput ${index} is not allowed. It has to be between 0 and ${returnData.length - 1}!`,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
// Iterate over all items to check to which output they should be routed to
|
|
||||||
itemLoop: for (let itemIndex = 0; itemIndex < items.length; itemIndex++) {
|
|
||||||
try {
|
|
||||||
item = items[itemIndex];
|
|
||||||
mode = this.getNodeParameter('mode', itemIndex) as string;
|
|
||||||
|
|
||||||
if (mode === 'expression') {
|
|
||||||
// One expression decides how to route item
|
|
||||||
|
|
||||||
outputIndex = this.getNodeParameter('output', itemIndex) as number;
|
|
||||||
checkIndexRange(outputIndex);
|
|
||||||
|
|
||||||
returnData[outputIndex].push(item);
|
|
||||||
} else if (mode === 'rules') {
|
|
||||||
// Rules decide how to route item
|
|
||||||
|
|
||||||
const dataType = this.getNodeParameter('dataType', 0) as string;
|
|
||||||
|
|
||||||
let value1 = this.getNodeParameter('value1', itemIndex) as NodeParameterValue;
|
|
||||||
if (dataType === 'dateTime') {
|
|
||||||
value1 = convertDateTime(value1);
|
|
||||||
}
|
|
||||||
|
|
||||||
for (ruleData of this.getNodeParameter(
|
|
||||||
'rules.rules',
|
|
||||||
itemIndex,
|
|
||||||
[],
|
|
||||||
) as INodeParameters[]) {
|
|
||||||
// Check if the values passes
|
|
||||||
|
|
||||||
let value2 = ruleData.value2 as NodeParameterValue;
|
|
||||||
if (dataType === 'dateTime') {
|
|
||||||
value2 = convertDateTime(value2);
|
|
||||||
}
|
|
||||||
|
|
||||||
compareOperationResult = compareOperationFunctions[ruleData.operation as string](
|
|
||||||
value1,
|
|
||||||
value2,
|
|
||||||
);
|
|
||||||
|
|
||||||
if (compareOperationResult) {
|
|
||||||
// If rule matches add it to the correct output and continue with next item
|
|
||||||
checkIndexRange(ruleData.output as number);
|
|
||||||
returnData[ruleData.output as number].push(item);
|
|
||||||
continue itemLoop;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check if a fallback output got defined and route accordingly
|
|
||||||
outputIndex = this.getNodeParameter('fallbackOutput', itemIndex) as number;
|
|
||||||
if (outputIndex !== -1) {
|
|
||||||
checkIndexRange(outputIndex);
|
|
||||||
returnData[outputIndex].push(item);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
if (this.continueOnFail()) {
|
|
||||||
returnData[0].push({ json: { error: error.message } });
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
throw error;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return returnData;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
686
packages/nodes-base/nodes/Switch/V1/SwitchV1.node.ts
Normal file
686
packages/nodes-base/nodes/Switch/V1/SwitchV1.node.ts
Normal file
|
@ -0,0 +1,686 @@
|
||||||
|
import type {
|
||||||
|
IExecuteFunctions,
|
||||||
|
INodeExecutionData,
|
||||||
|
INodeParameters,
|
||||||
|
INodeType,
|
||||||
|
INodeTypeBaseDescription,
|
||||||
|
INodeTypeDescription,
|
||||||
|
NodeParameterValue,
|
||||||
|
} from 'n8n-workflow';
|
||||||
|
import { NodeOperationError } from 'n8n-workflow';
|
||||||
|
|
||||||
|
export class SwitchV1 implements INodeType {
|
||||||
|
description: INodeTypeDescription;
|
||||||
|
|
||||||
|
constructor(baseDescription: INodeTypeBaseDescription) {
|
||||||
|
this.description = {
|
||||||
|
...baseDescription,
|
||||||
|
version: [1],
|
||||||
|
defaults: {
|
||||||
|
name: 'Switch',
|
||||||
|
color: '#506000',
|
||||||
|
},
|
||||||
|
inputs: ['main'],
|
||||||
|
outputs: ['main', 'main', 'main', 'main'],
|
||||||
|
outputNames: ['0', '1', '2', '3'],
|
||||||
|
properties: [
|
||||||
|
{
|
||||||
|
displayName: 'Mode',
|
||||||
|
name: 'mode',
|
||||||
|
type: 'options',
|
||||||
|
options: [
|
||||||
|
{
|
||||||
|
name: 'Expression',
|
||||||
|
value: 'expression',
|
||||||
|
description: 'Expression decides how to route data',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'Rules',
|
||||||
|
value: 'rules',
|
||||||
|
description: 'Rules decide how to route data',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
default: 'rules',
|
||||||
|
description: 'How data should be routed',
|
||||||
|
},
|
||||||
|
|
||||||
|
// ----------------------------------
|
||||||
|
// mode:expression
|
||||||
|
// ----------------------------------
|
||||||
|
{
|
||||||
|
displayName: 'Output',
|
||||||
|
name: 'output',
|
||||||
|
type: 'number',
|
||||||
|
typeOptions: {
|
||||||
|
minValue: 0,
|
||||||
|
maxValue: 3,
|
||||||
|
},
|
||||||
|
displayOptions: {
|
||||||
|
show: {
|
||||||
|
mode: ['expression'],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
default: 0,
|
||||||
|
description: 'The index of output to which to send data to',
|
||||||
|
},
|
||||||
|
|
||||||
|
// ----------------------------------
|
||||||
|
// mode:rules
|
||||||
|
// ----------------------------------
|
||||||
|
{
|
||||||
|
displayName: 'Data Type',
|
||||||
|
name: 'dataType',
|
||||||
|
type: 'options',
|
||||||
|
displayOptions: {
|
||||||
|
show: {
|
||||||
|
mode: ['rules'],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
options: [
|
||||||
|
{
|
||||||
|
name: 'Boolean',
|
||||||
|
value: 'boolean',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'Date & Time',
|
||||||
|
value: 'dateTime',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'Number',
|
||||||
|
value: 'number',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'String',
|
||||||
|
value: 'string',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
default: 'number',
|
||||||
|
description: 'The type of data to route on',
|
||||||
|
},
|
||||||
|
|
||||||
|
// ----------------------------------
|
||||||
|
// dataType:boolean
|
||||||
|
// ----------------------------------
|
||||||
|
{
|
||||||
|
displayName: 'Value 1',
|
||||||
|
name: 'value1',
|
||||||
|
type: 'boolean',
|
||||||
|
displayOptions: {
|
||||||
|
show: {
|
||||||
|
dataType: ['boolean'],
|
||||||
|
mode: ['rules'],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
default: false,
|
||||||
|
// eslint-disable-next-line n8n-nodes-base/node-param-description-boolean-without-whether
|
||||||
|
description: 'The value to compare with the first one',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
displayName: 'Routing Rules',
|
||||||
|
name: 'rules',
|
||||||
|
placeholder: 'Add Routing Rule',
|
||||||
|
type: 'fixedCollection',
|
||||||
|
typeOptions: {
|
||||||
|
multipleValues: true,
|
||||||
|
},
|
||||||
|
displayOptions: {
|
||||||
|
show: {
|
||||||
|
dataType: ['boolean'],
|
||||||
|
mode: ['rules'],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
default: {},
|
||||||
|
options: [
|
||||||
|
{
|
||||||
|
name: 'rules',
|
||||||
|
displayName: 'Boolean',
|
||||||
|
values: [
|
||||||
|
// eslint-disable-next-line n8n-nodes-base/node-param-operation-without-no-data-expression
|
||||||
|
{
|
||||||
|
displayName: 'Operation',
|
||||||
|
name: 'operation',
|
||||||
|
type: 'options',
|
||||||
|
options: [
|
||||||
|
{
|
||||||
|
name: 'Equal',
|
||||||
|
value: 'equal',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'Not Equal',
|
||||||
|
value: 'notEqual',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
default: 'equal',
|
||||||
|
description: 'Operation to decide where the the data should be mapped to',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
displayName: 'Value 2',
|
||||||
|
name: 'value2',
|
||||||
|
type: 'boolean',
|
||||||
|
default: false,
|
||||||
|
// eslint-disable-next-line n8n-nodes-base/node-param-description-boolean-without-whether
|
||||||
|
description: 'The value to compare with the first one',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
displayName: 'Output',
|
||||||
|
name: 'output',
|
||||||
|
type: 'number',
|
||||||
|
typeOptions: {
|
||||||
|
minValue: 0,
|
||||||
|
maxValue: 3,
|
||||||
|
},
|
||||||
|
default: 0,
|
||||||
|
description: 'The index of output to which to send data to if rule matches',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
|
||||||
|
// ----------------------------------
|
||||||
|
// dataType:dateTime
|
||||||
|
// ----------------------------------
|
||||||
|
{
|
||||||
|
displayName: 'Value 1',
|
||||||
|
name: 'value1',
|
||||||
|
type: 'dateTime',
|
||||||
|
displayOptions: {
|
||||||
|
show: {
|
||||||
|
dataType: ['dateTime'],
|
||||||
|
mode: ['rules'],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
default: '',
|
||||||
|
description: 'The value to compare with the second one',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
displayName: 'Routing Rules',
|
||||||
|
name: 'rules',
|
||||||
|
placeholder: 'Add Routing Rule',
|
||||||
|
type: 'fixedCollection',
|
||||||
|
typeOptions: {
|
||||||
|
multipleValues: true,
|
||||||
|
},
|
||||||
|
displayOptions: {
|
||||||
|
show: {
|
||||||
|
dataType: ['dateTime'],
|
||||||
|
mode: ['rules'],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
default: {},
|
||||||
|
options: [
|
||||||
|
{
|
||||||
|
name: 'rules',
|
||||||
|
displayName: 'Dates',
|
||||||
|
values: [
|
||||||
|
// eslint-disable-next-line n8n-nodes-base/node-param-operation-without-no-data-expression
|
||||||
|
{
|
||||||
|
displayName: 'Operation',
|
||||||
|
name: 'operation',
|
||||||
|
type: 'options',
|
||||||
|
options: [
|
||||||
|
{
|
||||||
|
name: 'Occurred After',
|
||||||
|
value: 'after',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'Occurred Before',
|
||||||
|
value: 'before',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
default: 'after',
|
||||||
|
description: 'Operation to decide where the the data should be mapped to',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
displayName: 'Value 2',
|
||||||
|
name: 'value2',
|
||||||
|
type: 'dateTime',
|
||||||
|
default: 0,
|
||||||
|
description: 'The value to compare with the first one',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
displayName: 'Output',
|
||||||
|
name: 'output',
|
||||||
|
type: 'number',
|
||||||
|
typeOptions: {
|
||||||
|
minValue: 0,
|
||||||
|
maxValue: 3,
|
||||||
|
},
|
||||||
|
default: 0,
|
||||||
|
description: 'The index of output to which to send data to if rule matches',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
|
||||||
|
// ----------------------------------
|
||||||
|
// dataType:number
|
||||||
|
// ----------------------------------
|
||||||
|
{
|
||||||
|
displayName: 'Value 1',
|
||||||
|
name: 'value1',
|
||||||
|
type: 'number',
|
||||||
|
displayOptions: {
|
||||||
|
show: {
|
||||||
|
dataType: ['number'],
|
||||||
|
mode: ['rules'],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
default: 0,
|
||||||
|
description: 'The value to compare with the second one',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
displayName: 'Routing Rules',
|
||||||
|
name: 'rules',
|
||||||
|
placeholder: 'Add Routing Rule',
|
||||||
|
type: 'fixedCollection',
|
||||||
|
typeOptions: {
|
||||||
|
multipleValues: true,
|
||||||
|
},
|
||||||
|
displayOptions: {
|
||||||
|
show: {
|
||||||
|
dataType: ['number'],
|
||||||
|
mode: ['rules'],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
default: {},
|
||||||
|
options: [
|
||||||
|
{
|
||||||
|
name: 'rules',
|
||||||
|
displayName: 'Numbers',
|
||||||
|
values: [
|
||||||
|
// eslint-disable-next-line n8n-nodes-base/node-param-operation-without-no-data-expression
|
||||||
|
{
|
||||||
|
displayName: 'Operation',
|
||||||
|
name: 'operation',
|
||||||
|
type: 'options',
|
||||||
|
// eslint-disable-next-line n8n-nodes-base/node-param-options-type-unsorted-items
|
||||||
|
options: [
|
||||||
|
{
|
||||||
|
name: 'Smaller',
|
||||||
|
value: 'smaller',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'Smaller Equal',
|
||||||
|
value: 'smallerEqual',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'Equal',
|
||||||
|
value: 'equal',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'Not Equal',
|
||||||
|
value: 'notEqual',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'Larger',
|
||||||
|
value: 'larger',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'Larger Equal',
|
||||||
|
value: 'largerEqual',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
default: 'smaller',
|
||||||
|
description: 'Operation to decide where the the data should be mapped to',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
displayName: 'Value 2',
|
||||||
|
name: 'value2',
|
||||||
|
type: 'number',
|
||||||
|
default: 0,
|
||||||
|
description: 'The value to compare with the first one',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
displayName: 'Output',
|
||||||
|
name: 'output',
|
||||||
|
type: 'number',
|
||||||
|
typeOptions: {
|
||||||
|
minValue: 0,
|
||||||
|
maxValue: 3,
|
||||||
|
},
|
||||||
|
default: 0,
|
||||||
|
description: 'The index of output to which to send data to if rule matches',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
|
||||||
|
// ----------------------------------
|
||||||
|
// dataType:string
|
||||||
|
// ----------------------------------
|
||||||
|
{
|
||||||
|
displayName: 'Value 1',
|
||||||
|
name: 'value1',
|
||||||
|
type: 'string',
|
||||||
|
displayOptions: {
|
||||||
|
show: {
|
||||||
|
dataType: ['string'],
|
||||||
|
mode: ['rules'],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
default: '',
|
||||||
|
description: 'The value to compare with the second one',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
displayName: 'Routing Rules',
|
||||||
|
name: 'rules',
|
||||||
|
placeholder: 'Add Routing Rule',
|
||||||
|
type: 'fixedCollection',
|
||||||
|
typeOptions: {
|
||||||
|
multipleValues: true,
|
||||||
|
},
|
||||||
|
displayOptions: {
|
||||||
|
show: {
|
||||||
|
dataType: ['string'],
|
||||||
|
mode: ['rules'],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
default: {},
|
||||||
|
options: [
|
||||||
|
{
|
||||||
|
name: 'rules',
|
||||||
|
displayName: 'Strings',
|
||||||
|
values: [
|
||||||
|
// eslint-disable-next-line n8n-nodes-base/node-param-operation-without-no-data-expression
|
||||||
|
{
|
||||||
|
displayName: 'Operation',
|
||||||
|
name: 'operation',
|
||||||
|
type: 'options',
|
||||||
|
// eslint-disable-next-line n8n-nodes-base/node-param-options-type-unsorted-items
|
||||||
|
options: [
|
||||||
|
{
|
||||||
|
name: 'Contains',
|
||||||
|
value: 'contains',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'Not Contains',
|
||||||
|
value: 'notContains',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'Ends With',
|
||||||
|
value: 'endsWith',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'Not Ends With',
|
||||||
|
value: 'notEndsWith',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'Equal',
|
||||||
|
value: 'equal',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'Not Equal',
|
||||||
|
value: 'notEqual',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'Regex Match',
|
||||||
|
value: 'regex',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'Regex Not Match',
|
||||||
|
value: 'notRegex',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'Starts With',
|
||||||
|
value: 'startsWith',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'Not Starts With',
|
||||||
|
value: 'notStartsWith',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
default: 'equal',
|
||||||
|
description: 'Operation to decide where the the data should be mapped to',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
displayName: 'Value 2',
|
||||||
|
name: 'value2',
|
||||||
|
type: 'string',
|
||||||
|
displayOptions: {
|
||||||
|
hide: {
|
||||||
|
operation: ['regex', 'notRegex'],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
default: '',
|
||||||
|
description: 'The value to compare with the first one',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
displayName: 'Regex',
|
||||||
|
name: 'value2',
|
||||||
|
type: 'string',
|
||||||
|
displayOptions: {
|
||||||
|
show: {
|
||||||
|
operation: ['regex', 'notRegex'],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
default: '',
|
||||||
|
placeholder: '/text/i',
|
||||||
|
description: 'The regex which has to match',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
displayName: 'Output',
|
||||||
|
name: 'output',
|
||||||
|
type: 'number',
|
||||||
|
typeOptions: {
|
||||||
|
minValue: 0,
|
||||||
|
maxValue: 3,
|
||||||
|
},
|
||||||
|
default: 0,
|
||||||
|
description: 'The index of output to which to send data to if rule matches',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
|
||||||
|
{
|
||||||
|
displayName: 'Fallback Output',
|
||||||
|
name: 'fallbackOutput',
|
||||||
|
type: 'options',
|
||||||
|
displayOptions: {
|
||||||
|
show: {
|
||||||
|
mode: ['rules'],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
// eslint-disable-next-line n8n-nodes-base/node-param-options-type-unsorted-items
|
||||||
|
options: [
|
||||||
|
{
|
||||||
|
name: 'None',
|
||||||
|
value: -1,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: '0',
|
||||||
|
value: 0,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: '1',
|
||||||
|
value: 1,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: '2',
|
||||||
|
value: 2,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: '3',
|
||||||
|
value: 3,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
default: -1,
|
||||||
|
description: 'The output to which to route all items which do not match any of the rules',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
async execute(this: IExecuteFunctions): Promise<INodeExecutionData[][]> {
|
||||||
|
const returnData: INodeExecutionData[][] = [[], [], [], []];
|
||||||
|
|
||||||
|
const items = this.getInputData();
|
||||||
|
|
||||||
|
let compareOperationResult: boolean;
|
||||||
|
let item: INodeExecutionData;
|
||||||
|
let mode: string;
|
||||||
|
let outputIndex: number;
|
||||||
|
let ruleData: INodeParameters;
|
||||||
|
|
||||||
|
// The compare operations
|
||||||
|
const compareOperationFunctions: {
|
||||||
|
[key: string]: (value1: NodeParameterValue, value2: NodeParameterValue) => boolean;
|
||||||
|
} = {
|
||||||
|
after: (value1: NodeParameterValue, value2: NodeParameterValue) =>
|
||||||
|
(value1 || 0) > (value2 || 0),
|
||||||
|
before: (value1: NodeParameterValue, value2: NodeParameterValue) =>
|
||||||
|
(value1 || 0) < (value2 || 0),
|
||||||
|
contains: (value1: NodeParameterValue, value2: NodeParameterValue) =>
|
||||||
|
(value1 || '').toString().includes((value2 || '').toString()),
|
||||||
|
notContains: (value1: NodeParameterValue, value2: NodeParameterValue) =>
|
||||||
|
!(value1 || '').toString().includes((value2 || '').toString()),
|
||||||
|
endsWith: (value1: NodeParameterValue, value2: NodeParameterValue) =>
|
||||||
|
(value1 as string).endsWith(value2 as string),
|
||||||
|
notEndsWith: (value1: NodeParameterValue, value2: NodeParameterValue) =>
|
||||||
|
!(value1 as string).endsWith(value2 as string),
|
||||||
|
equal: (value1: NodeParameterValue, value2: NodeParameterValue) => value1 === value2,
|
||||||
|
notEqual: (value1: NodeParameterValue, value2: NodeParameterValue) => value1 !== value2,
|
||||||
|
larger: (value1: NodeParameterValue, value2: NodeParameterValue) =>
|
||||||
|
(value1 || 0) > (value2 || 0),
|
||||||
|
largerEqual: (value1: NodeParameterValue, value2: NodeParameterValue) =>
|
||||||
|
(value1 || 0) >= (value2 || 0),
|
||||||
|
smaller: (value1: NodeParameterValue, value2: NodeParameterValue) =>
|
||||||
|
(value1 || 0) < (value2 || 0),
|
||||||
|
smallerEqual: (value1: NodeParameterValue, value2: NodeParameterValue) =>
|
||||||
|
(value1 || 0) <= (value2 || 0),
|
||||||
|
startsWith: (value1: NodeParameterValue, value2: NodeParameterValue) =>
|
||||||
|
(value1 as string).startsWith(value2 as string),
|
||||||
|
notStartsWith: (value1: NodeParameterValue, value2: NodeParameterValue) =>
|
||||||
|
!(value1 as string).startsWith(value2 as string),
|
||||||
|
regex: (value1: NodeParameterValue, value2: NodeParameterValue) => {
|
||||||
|
const regexMatch = (value2 || '').toString().match(new RegExp('^/(.*?)/([gimusy]*)$'));
|
||||||
|
|
||||||
|
let regex: RegExp;
|
||||||
|
if (!regexMatch) {
|
||||||
|
regex = new RegExp((value2 || '').toString());
|
||||||
|
} else if (regexMatch.length === 1) {
|
||||||
|
regex = new RegExp(regexMatch[1]);
|
||||||
|
} else {
|
||||||
|
regex = new RegExp(regexMatch[1], regexMatch[2]);
|
||||||
|
}
|
||||||
|
|
||||||
|
return !!(value1 || '').toString().match(regex);
|
||||||
|
},
|
||||||
|
notRegex: (value1: NodeParameterValue, value2: NodeParameterValue) => {
|
||||||
|
const regexMatch = (value2 || '').toString().match(new RegExp('^/(.*?)/([gimusy]*)$'));
|
||||||
|
|
||||||
|
let regex: RegExp;
|
||||||
|
if (!regexMatch) {
|
||||||
|
regex = new RegExp((value2 || '').toString());
|
||||||
|
} else if (regexMatch.length === 1) {
|
||||||
|
regex = new RegExp(regexMatch[1]);
|
||||||
|
} else {
|
||||||
|
regex = new RegExp(regexMatch[1], regexMatch[2]);
|
||||||
|
}
|
||||||
|
|
||||||
|
return !(value1 || '').toString().match(regex);
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
// Converts the input data of a dateTime into a number for easy compare
|
||||||
|
const convertDateTime = (value: NodeParameterValue): number => {
|
||||||
|
let returnValue: number | undefined = undefined;
|
||||||
|
if (typeof value === 'string') {
|
||||||
|
returnValue = new Date(value).getTime();
|
||||||
|
} else if (typeof value === 'number') {
|
||||||
|
returnValue = value;
|
||||||
|
}
|
||||||
|
if ((value as unknown as object) instanceof Date) {
|
||||||
|
returnValue = (value as unknown as Date).getTime();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (returnValue === undefined || isNaN(returnValue)) {
|
||||||
|
throw new NodeOperationError(
|
||||||
|
this.getNode(),
|
||||||
|
`The value "${value}" is not a valid DateTime.`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return returnValue;
|
||||||
|
};
|
||||||
|
|
||||||
|
const checkIndexRange = (index: number) => {
|
||||||
|
if (index < 0 || index >= returnData.length) {
|
||||||
|
throw new NodeOperationError(
|
||||||
|
this.getNode(),
|
||||||
|
`The ouput ${index} is not allowed. It has to be between 0 and ${returnData.length - 1}!`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Iterate over all items to check to which output they should be routed to
|
||||||
|
itemLoop: for (let itemIndex = 0; itemIndex < items.length; itemIndex++) {
|
||||||
|
try {
|
||||||
|
item = items[itemIndex];
|
||||||
|
mode = this.getNodeParameter('mode', itemIndex) as string;
|
||||||
|
|
||||||
|
if (mode === 'expression') {
|
||||||
|
// One expression decides how to route item
|
||||||
|
|
||||||
|
outputIndex = this.getNodeParameter('output', itemIndex) as number;
|
||||||
|
checkIndexRange(outputIndex);
|
||||||
|
|
||||||
|
returnData[outputIndex].push(item);
|
||||||
|
} else if (mode === 'rules') {
|
||||||
|
// Rules decide how to route item
|
||||||
|
|
||||||
|
const dataType = this.getNodeParameter('dataType', 0) as string;
|
||||||
|
|
||||||
|
let value1 = this.getNodeParameter('value1', itemIndex) as NodeParameterValue;
|
||||||
|
if (dataType === 'dateTime') {
|
||||||
|
value1 = convertDateTime(value1);
|
||||||
|
}
|
||||||
|
|
||||||
|
for (ruleData of this.getNodeParameter(
|
||||||
|
'rules.rules',
|
||||||
|
itemIndex,
|
||||||
|
[],
|
||||||
|
) as INodeParameters[]) {
|
||||||
|
// Check if the values passes
|
||||||
|
|
||||||
|
let value2 = ruleData.value2 as NodeParameterValue;
|
||||||
|
if (dataType === 'dateTime') {
|
||||||
|
value2 = convertDateTime(value2);
|
||||||
|
}
|
||||||
|
|
||||||
|
compareOperationResult = compareOperationFunctions[ruleData.operation as string](
|
||||||
|
value1,
|
||||||
|
value2,
|
||||||
|
);
|
||||||
|
|
||||||
|
if (compareOperationResult) {
|
||||||
|
// If rule matches add it to the correct output and continue with next item
|
||||||
|
checkIndexRange(ruleData.output as number);
|
||||||
|
returnData[ruleData.output as number].push(item);
|
||||||
|
continue itemLoop;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if a fallback output got defined and route accordingly
|
||||||
|
outputIndex = this.getNodeParameter('fallbackOutput', itemIndex) as number;
|
||||||
|
if (outputIndex !== -1) {
|
||||||
|
checkIndexRange(outputIndex);
|
||||||
|
returnData[outputIndex].push(item);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
if (this.continueOnFail()) {
|
||||||
|
returnData[0].push({ json: { error: error.message } });
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return returnData;
|
||||||
|
}
|
||||||
|
}
|
706
packages/nodes-base/nodes/Switch/V2/SwitchV2.node.ts
Normal file
706
packages/nodes-base/nodes/Switch/V2/SwitchV2.node.ts
Normal file
|
@ -0,0 +1,706 @@
|
||||||
|
import type {
|
||||||
|
IExecuteFunctions,
|
||||||
|
ILoadOptionsFunctions,
|
||||||
|
INodeExecutionData,
|
||||||
|
INodeParameters,
|
||||||
|
INodePropertyOptions,
|
||||||
|
INodeType,
|
||||||
|
INodeTypeBaseDescription,
|
||||||
|
INodeTypeDescription,
|
||||||
|
NodeParameterValue,
|
||||||
|
} from 'n8n-workflow';
|
||||||
|
import { NodeConnectionType, NodeOperationError } from 'n8n-workflow';
|
||||||
|
|
||||||
|
export class SwitchV2 implements INodeType {
|
||||||
|
description: INodeTypeDescription;
|
||||||
|
|
||||||
|
constructor(baseDescription: INodeTypeBaseDescription) {
|
||||||
|
this.description = {
|
||||||
|
...baseDescription,
|
||||||
|
version: [2],
|
||||||
|
defaults: {
|
||||||
|
name: 'Switch',
|
||||||
|
color: '#506000',
|
||||||
|
},
|
||||||
|
inputs: ['main'],
|
||||||
|
// eslint-disable-next-line prettier/prettier
|
||||||
|
outputs: `={{
|
||||||
|
((parameters) => {
|
||||||
|
const rules = parameters.rules?.rules ?? [];
|
||||||
|
const mode = parameters.mode;
|
||||||
|
|
||||||
|
if (mode === 'expression') {
|
||||||
|
return Array
|
||||||
|
.from(
|
||||||
|
{ length: parameters.outputsAmount },
|
||||||
|
(_, i) => ({ type: "${NodeConnectionType.Main}", displayName: i.toString() })
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
return rules.map(value => {
|
||||||
|
return { type: "${NodeConnectionType.Main}", displayName: value.outputKey }
|
||||||
|
})
|
||||||
|
})($parameter)
|
||||||
|
}}`,
|
||||||
|
|
||||||
|
properties: [
|
||||||
|
{
|
||||||
|
displayName: 'Mode',
|
||||||
|
name: 'mode',
|
||||||
|
type: 'options',
|
||||||
|
options: [
|
||||||
|
{
|
||||||
|
name: 'Expression',
|
||||||
|
value: 'expression',
|
||||||
|
description: 'Expression decides how to route data',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'Rules',
|
||||||
|
value: 'rules',
|
||||||
|
description: 'Rules decide how to route data',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
default: 'rules',
|
||||||
|
description: 'How data should be routed',
|
||||||
|
},
|
||||||
|
|
||||||
|
// ----------------------------------
|
||||||
|
// mode:expression
|
||||||
|
// ----------------------------------
|
||||||
|
{
|
||||||
|
displayName: 'Output',
|
||||||
|
name: 'output',
|
||||||
|
type: 'string',
|
||||||
|
displayOptions: {
|
||||||
|
show: {
|
||||||
|
mode: ['expression'],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
default: '',
|
||||||
|
description: 'The index of output to which to send data to',
|
||||||
|
},
|
||||||
|
|
||||||
|
{
|
||||||
|
displayName: 'Outputs Amount',
|
||||||
|
name: 'outputsAmount',
|
||||||
|
type: 'number',
|
||||||
|
displayOptions: {
|
||||||
|
show: {
|
||||||
|
mode: ['expression'],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
default: 4,
|
||||||
|
description: 'Amount of outputs to create',
|
||||||
|
},
|
||||||
|
|
||||||
|
// ----------------------------------
|
||||||
|
// mode:rules
|
||||||
|
// ----------------------------------
|
||||||
|
{
|
||||||
|
displayName: 'Data Type',
|
||||||
|
name: 'dataType',
|
||||||
|
type: 'options',
|
||||||
|
displayOptions: {
|
||||||
|
show: {
|
||||||
|
mode: ['rules'],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
options: [
|
||||||
|
{
|
||||||
|
name: 'Boolean',
|
||||||
|
value: 'boolean',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'Date & Time',
|
||||||
|
value: 'dateTime',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'Number',
|
||||||
|
value: 'number',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'String',
|
||||||
|
value: 'string',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
default: 'number',
|
||||||
|
description: 'The type of data to route on',
|
||||||
|
},
|
||||||
|
|
||||||
|
// ----------------------------------
|
||||||
|
// dataType:boolean
|
||||||
|
// ----------------------------------
|
||||||
|
{
|
||||||
|
displayName: 'Value 1',
|
||||||
|
name: 'value1',
|
||||||
|
type: 'boolean',
|
||||||
|
displayOptions: {
|
||||||
|
show: {
|
||||||
|
dataType: ['boolean'],
|
||||||
|
mode: ['rules'],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
default: false,
|
||||||
|
// eslint-disable-next-line n8n-nodes-base/node-param-description-boolean-without-whether
|
||||||
|
description: 'The value to compare with the first one',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
displayName: 'Routing Rules',
|
||||||
|
name: 'rules',
|
||||||
|
placeholder: 'Add Routing Rule',
|
||||||
|
type: 'fixedCollection',
|
||||||
|
typeOptions: {
|
||||||
|
multipleValues: true,
|
||||||
|
},
|
||||||
|
displayOptions: {
|
||||||
|
show: {
|
||||||
|
dataType: ['boolean'],
|
||||||
|
mode: ['rules'],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
default: {},
|
||||||
|
options: [
|
||||||
|
{
|
||||||
|
name: 'rules',
|
||||||
|
displayName: 'Boolean',
|
||||||
|
values: [
|
||||||
|
// eslint-disable-next-line n8n-nodes-base/node-param-operation-without-no-data-expression
|
||||||
|
{
|
||||||
|
displayName: 'Operation',
|
||||||
|
name: 'operation',
|
||||||
|
type: 'options',
|
||||||
|
options: [
|
||||||
|
{
|
||||||
|
name: 'Equal',
|
||||||
|
value: 'equal',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'Not Equal',
|
||||||
|
value: 'notEqual',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
default: 'equal',
|
||||||
|
description: 'Operation to decide where the the data should be mapped to',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
displayName: 'Value 2',
|
||||||
|
name: 'value2',
|
||||||
|
type: 'boolean',
|
||||||
|
default: false,
|
||||||
|
// eslint-disable-next-line n8n-nodes-base/node-param-description-boolean-without-whether
|
||||||
|
description: 'The value to compare with the first one',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
displayName: 'Output Key',
|
||||||
|
name: 'outputKey',
|
||||||
|
type: 'string',
|
||||||
|
default: '',
|
||||||
|
description: 'The label of output to which to send data to if rule matches',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
|
||||||
|
// ----------------------------------
|
||||||
|
// dataType:dateTime
|
||||||
|
// ----------------------------------
|
||||||
|
{
|
||||||
|
displayName: 'Value 1',
|
||||||
|
name: 'value1',
|
||||||
|
type: 'dateTime',
|
||||||
|
displayOptions: {
|
||||||
|
show: {
|
||||||
|
dataType: ['dateTime'],
|
||||||
|
mode: ['rules'],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
default: '',
|
||||||
|
description: 'The value to compare with the second one',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
displayName: 'Routing Rules',
|
||||||
|
name: 'rules',
|
||||||
|
placeholder: 'Add Routing Rule',
|
||||||
|
type: 'fixedCollection',
|
||||||
|
typeOptions: {
|
||||||
|
multipleValues: true,
|
||||||
|
},
|
||||||
|
displayOptions: {
|
||||||
|
show: {
|
||||||
|
dataType: ['dateTime'],
|
||||||
|
mode: ['rules'],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
default: {},
|
||||||
|
options: [
|
||||||
|
{
|
||||||
|
name: 'rules',
|
||||||
|
displayName: 'Dates',
|
||||||
|
values: [
|
||||||
|
// eslint-disable-next-line n8n-nodes-base/node-param-operation-without-no-data-expression
|
||||||
|
{
|
||||||
|
displayName: 'Operation',
|
||||||
|
name: 'operation',
|
||||||
|
type: 'options',
|
||||||
|
options: [
|
||||||
|
{
|
||||||
|
name: 'Occurred After',
|
||||||
|
value: 'after',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'Occurred Before',
|
||||||
|
value: 'before',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
default: 'after',
|
||||||
|
description: 'Operation to decide where the the data should be mapped to',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
displayName: 'Value 2',
|
||||||
|
name: 'value2',
|
||||||
|
type: 'dateTime',
|
||||||
|
default: 0,
|
||||||
|
description: 'The value to compare with the first one',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
displayName: 'Output Key',
|
||||||
|
name: 'outputKey',
|
||||||
|
type: 'string',
|
||||||
|
default: '',
|
||||||
|
description: 'The label of output to which to send data to if rule matches',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
|
||||||
|
// ----------------------------------
|
||||||
|
// dataType:number
|
||||||
|
// ----------------------------------
|
||||||
|
{
|
||||||
|
displayName: 'Value 1',
|
||||||
|
name: 'value1',
|
||||||
|
type: 'number',
|
||||||
|
displayOptions: {
|
||||||
|
show: {
|
||||||
|
dataType: ['number'],
|
||||||
|
mode: ['rules'],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
default: 0,
|
||||||
|
description: 'The value to compare with the second one',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
displayName: 'Routing Rules',
|
||||||
|
name: 'rules',
|
||||||
|
placeholder: 'Add Routing Rule',
|
||||||
|
type: 'fixedCollection',
|
||||||
|
typeOptions: {
|
||||||
|
multipleValues: true,
|
||||||
|
},
|
||||||
|
displayOptions: {
|
||||||
|
show: {
|
||||||
|
dataType: ['number'],
|
||||||
|
mode: ['rules'],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
default: {},
|
||||||
|
options: [
|
||||||
|
{
|
||||||
|
name: 'rules',
|
||||||
|
displayName: 'Numbers',
|
||||||
|
values: [
|
||||||
|
// eslint-disable-next-line n8n-nodes-base/node-param-operation-without-no-data-expression
|
||||||
|
{
|
||||||
|
displayName: 'Operation',
|
||||||
|
name: 'operation',
|
||||||
|
type: 'options',
|
||||||
|
// eslint-disable-next-line n8n-nodes-base/node-param-options-type-unsorted-items
|
||||||
|
options: [
|
||||||
|
{
|
||||||
|
name: 'Smaller',
|
||||||
|
value: 'smaller',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'Smaller Equal',
|
||||||
|
value: 'smallerEqual',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'Equal',
|
||||||
|
value: 'equal',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'Not Equal',
|
||||||
|
value: 'notEqual',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'Larger',
|
||||||
|
value: 'larger',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'Larger Equal',
|
||||||
|
value: 'largerEqual',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
default: 'smaller',
|
||||||
|
description: 'Operation to decide where the the data should be mapped to',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
displayName: 'Value 2',
|
||||||
|
name: 'value2',
|
||||||
|
type: 'number',
|
||||||
|
default: 0,
|
||||||
|
description: 'The value to compare with the first one',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
displayName: 'Output Key',
|
||||||
|
name: 'outputKey',
|
||||||
|
type: 'string',
|
||||||
|
default: '',
|
||||||
|
description: 'The label of output to which to send data to if rule matches',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
|
||||||
|
// ----------------------------------
|
||||||
|
// dataType:string
|
||||||
|
// ----------------------------------
|
||||||
|
{
|
||||||
|
displayName: 'Value 1',
|
||||||
|
name: 'value1',
|
||||||
|
type: 'string',
|
||||||
|
displayOptions: {
|
||||||
|
show: {
|
||||||
|
dataType: ['string'],
|
||||||
|
mode: ['rules'],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
default: '',
|
||||||
|
description: 'The value to compare with the second one',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
displayName: 'Routing Rules',
|
||||||
|
name: 'rules',
|
||||||
|
placeholder: 'Add Routing Rule',
|
||||||
|
type: 'fixedCollection',
|
||||||
|
typeOptions: {
|
||||||
|
multipleValues: true,
|
||||||
|
},
|
||||||
|
displayOptions: {
|
||||||
|
show: {
|
||||||
|
dataType: ['string'],
|
||||||
|
mode: ['rules'],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
default: {},
|
||||||
|
options: [
|
||||||
|
{
|
||||||
|
name: 'rules',
|
||||||
|
displayName: 'Strings',
|
||||||
|
values: [
|
||||||
|
// eslint-disable-next-line n8n-nodes-base/node-param-operation-without-no-data-expression
|
||||||
|
{
|
||||||
|
displayName: 'Operation',
|
||||||
|
name: 'operation',
|
||||||
|
type: 'options',
|
||||||
|
// eslint-disable-next-line n8n-nodes-base/node-param-options-type-unsorted-items
|
||||||
|
options: [
|
||||||
|
{
|
||||||
|
name: 'Contains',
|
||||||
|
value: 'contains',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'Not Contains',
|
||||||
|
value: 'notContains',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'Ends With',
|
||||||
|
value: 'endsWith',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'Not Ends With',
|
||||||
|
value: 'notEndsWith',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'Equal',
|
||||||
|
value: 'equal',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'Not Equal',
|
||||||
|
value: 'notEqual',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'Regex Match',
|
||||||
|
value: 'regex',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'Regex Not Match',
|
||||||
|
value: 'notRegex',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'Starts With',
|
||||||
|
value: 'startsWith',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'Not Starts With',
|
||||||
|
value: 'notStartsWith',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
default: 'equal',
|
||||||
|
description: 'Operation to decide where the the data should be mapped to',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
displayName: 'Value 2',
|
||||||
|
name: 'value2',
|
||||||
|
type: 'string',
|
||||||
|
displayOptions: {
|
||||||
|
hide: {
|
||||||
|
operation: ['regex', 'notRegex'],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
default: '',
|
||||||
|
description: 'The value to compare with the first one',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
displayName: 'Regex',
|
||||||
|
name: 'value2',
|
||||||
|
type: 'string',
|
||||||
|
displayOptions: {
|
||||||
|
show: {
|
||||||
|
operation: ['regex', 'notRegex'],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
default: '',
|
||||||
|
placeholder: '/text/i',
|
||||||
|
description: 'The regex which has to match',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
displayName: 'Output Key',
|
||||||
|
name: 'outputKey',
|
||||||
|
type: 'string',
|
||||||
|
default: '',
|
||||||
|
description: 'The label of output to which to send data to if rule matches',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
|
||||||
|
{
|
||||||
|
displayName: 'Fallback Output Name or ID',
|
||||||
|
name: 'fallbackOutput',
|
||||||
|
type: 'options',
|
||||||
|
displayOptions: {
|
||||||
|
show: {
|
||||||
|
mode: ['rules'],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
// eslint-disable-next-line n8n-nodes-base/node-param-options-type-unsorted-items
|
||||||
|
typeOptions: {
|
||||||
|
loadOptionsDependsOn: ['rules.rules'],
|
||||||
|
loadOptionsMethod: 'getFallbackOutputOptions',
|
||||||
|
},
|
||||||
|
default: -1,
|
||||||
|
description:
|
||||||
|
'The output to which to route all items which do not match any of the rules. Choose from the list, or specify an ID using an <a href="https://docs.n8n.io/code-examples/expressions/">expression</a>.',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
methods = {
|
||||||
|
loadOptions: {
|
||||||
|
async getFallbackOutputOptions(this: ILoadOptionsFunctions): Promise<INodePropertyOptions[]> {
|
||||||
|
const rules = (this.getCurrentNodeParameter('rules.rules') as INodeParameters[]) ?? [];
|
||||||
|
const options = rules.map((rule, index) => ({
|
||||||
|
name: `${index} ${rule.outputKey as string}`,
|
||||||
|
value: index,
|
||||||
|
}));
|
||||||
|
|
||||||
|
options.unshift({
|
||||||
|
name: 'None',
|
||||||
|
value: -1,
|
||||||
|
});
|
||||||
|
|
||||||
|
return options;
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
async execute(this: IExecuteFunctions): Promise<INodeExecutionData[][]> {
|
||||||
|
let returnData: INodeExecutionData[][] = [];
|
||||||
|
|
||||||
|
const items = this.getInputData();
|
||||||
|
|
||||||
|
let compareOperationResult: boolean;
|
||||||
|
let item: INodeExecutionData;
|
||||||
|
let mode: string;
|
||||||
|
let outputIndex: number;
|
||||||
|
let ruleData: INodeParameters;
|
||||||
|
|
||||||
|
// The compare operations
|
||||||
|
const compareOperationFunctions: {
|
||||||
|
[key: string]: (value1: NodeParameterValue, value2: NodeParameterValue) => boolean;
|
||||||
|
} = {
|
||||||
|
after: (value1: NodeParameterValue, value2: NodeParameterValue) =>
|
||||||
|
(value1 || 0) > (value2 || 0),
|
||||||
|
before: (value1: NodeParameterValue, value2: NodeParameterValue) =>
|
||||||
|
(value1 || 0) < (value2 || 0),
|
||||||
|
contains: (value1: NodeParameterValue, value2: NodeParameterValue) =>
|
||||||
|
(value1 || '').toString().includes((value2 || '').toString()),
|
||||||
|
notContains: (value1: NodeParameterValue, value2: NodeParameterValue) =>
|
||||||
|
!(value1 || '').toString().includes((value2 || '').toString()),
|
||||||
|
endsWith: (value1: NodeParameterValue, value2: NodeParameterValue) =>
|
||||||
|
(value1 as string).endsWith(value2 as string),
|
||||||
|
notEndsWith: (value1: NodeParameterValue, value2: NodeParameterValue) =>
|
||||||
|
!(value1 as string).endsWith(value2 as string),
|
||||||
|
equal: (value1: NodeParameterValue, value2: NodeParameterValue) => value1 === value2,
|
||||||
|
notEqual: (value1: NodeParameterValue, value2: NodeParameterValue) => value1 !== value2,
|
||||||
|
larger: (value1: NodeParameterValue, value2: NodeParameterValue) =>
|
||||||
|
(value1 || 0) > (value2 || 0),
|
||||||
|
largerEqual: (value1: NodeParameterValue, value2: NodeParameterValue) =>
|
||||||
|
(value1 || 0) >= (value2 || 0),
|
||||||
|
smaller: (value1: NodeParameterValue, value2: NodeParameterValue) =>
|
||||||
|
(value1 || 0) < (value2 || 0),
|
||||||
|
smallerEqual: (value1: NodeParameterValue, value2: NodeParameterValue) =>
|
||||||
|
(value1 || 0) <= (value2 || 0),
|
||||||
|
startsWith: (value1: NodeParameterValue, value2: NodeParameterValue) =>
|
||||||
|
(value1 as string).startsWith(value2 as string),
|
||||||
|
notStartsWith: (value1: NodeParameterValue, value2: NodeParameterValue) =>
|
||||||
|
!(value1 as string).startsWith(value2 as string),
|
||||||
|
regex: (value1: NodeParameterValue, value2: NodeParameterValue) => {
|
||||||
|
const regexMatch = (value2 || '').toString().match(new RegExp('^/(.*?)/([gimusy]*)$'));
|
||||||
|
|
||||||
|
let regex: RegExp;
|
||||||
|
if (!regexMatch) {
|
||||||
|
regex = new RegExp((value2 || '').toString());
|
||||||
|
} else if (regexMatch.length === 1) {
|
||||||
|
regex = new RegExp(regexMatch[1]);
|
||||||
|
} else {
|
||||||
|
regex = new RegExp(regexMatch[1], regexMatch[2]);
|
||||||
|
}
|
||||||
|
|
||||||
|
return !!(value1 || '').toString().match(regex);
|
||||||
|
},
|
||||||
|
notRegex: (value1: NodeParameterValue, value2: NodeParameterValue) => {
|
||||||
|
const regexMatch = (value2 || '').toString().match(new RegExp('^/(.*?)/([gimusy]*)$'));
|
||||||
|
|
||||||
|
let regex: RegExp;
|
||||||
|
if (!regexMatch) {
|
||||||
|
regex = new RegExp((value2 || '').toString());
|
||||||
|
} else if (regexMatch.length === 1) {
|
||||||
|
regex = new RegExp(regexMatch[1]);
|
||||||
|
} else {
|
||||||
|
regex = new RegExp(regexMatch[1], regexMatch[2]);
|
||||||
|
}
|
||||||
|
|
||||||
|
return !(value1 || '').toString().match(regex);
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
// Converts the input data of a dateTime into a number for easy compare
|
||||||
|
const convertDateTime = (value: NodeParameterValue): number => {
|
||||||
|
let returnValue: number | undefined = undefined;
|
||||||
|
if (typeof value === 'string') {
|
||||||
|
returnValue = new Date(value).getTime();
|
||||||
|
} else if (typeof value === 'number') {
|
||||||
|
returnValue = value;
|
||||||
|
}
|
||||||
|
if ((value as unknown as object) instanceof Date) {
|
||||||
|
returnValue = (value as unknown as Date).getTime();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (returnValue === undefined || isNaN(returnValue)) {
|
||||||
|
throw new NodeOperationError(
|
||||||
|
this.getNode(),
|
||||||
|
`The value "${value}" is not a valid DateTime.`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return returnValue;
|
||||||
|
};
|
||||||
|
|
||||||
|
const checkIndexRange = (index: number) => {
|
||||||
|
if (index < 0 || index >= returnData.length) {
|
||||||
|
throw new NodeOperationError(
|
||||||
|
this.getNode(),
|
||||||
|
`The ouput ${index} is not allowed. It has to be between 0 and ${returnData.length - 1}!`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// Iterate over all items to check to which output they should be routed to
|
||||||
|
itemLoop: for (let itemIndex = 0; itemIndex < items.length; itemIndex++) {
|
||||||
|
try {
|
||||||
|
item = items[itemIndex];
|
||||||
|
const rules = this.getNodeParameter('rules.rules', itemIndex, []) as INodeParameters[];
|
||||||
|
mode = this.getNodeParameter('mode', itemIndex) as string;
|
||||||
|
|
||||||
|
if (mode === 'expression') {
|
||||||
|
const outputsAmount = this.getNodeParameter('outputsAmount', itemIndex) as number;
|
||||||
|
if (itemIndex === 0) {
|
||||||
|
returnData = new Array(outputsAmount).fill(0).map(() => []);
|
||||||
|
}
|
||||||
|
// One expression decides how to route item
|
||||||
|
outputIndex = this.getNodeParameter('output', itemIndex) as number;
|
||||||
|
checkIndexRange(outputIndex);
|
||||||
|
|
||||||
|
returnData[outputIndex].push(item);
|
||||||
|
} else if (mode === 'rules') {
|
||||||
|
// Rules decide how to route item
|
||||||
|
if (itemIndex === 0) {
|
||||||
|
returnData = new Array(rules.length).fill(0).map(() => []);
|
||||||
|
}
|
||||||
|
const dataType = this.getNodeParameter('dataType', 0) as string;
|
||||||
|
|
||||||
|
let value1 = this.getNodeParameter('value1', itemIndex) as NodeParameterValue;
|
||||||
|
if (dataType === 'dateTime') {
|
||||||
|
value1 = convertDateTime(value1);
|
||||||
|
}
|
||||||
|
|
||||||
|
for (ruleData of rules) {
|
||||||
|
// Check if the values passes
|
||||||
|
|
||||||
|
let value2 = ruleData.value2 as NodeParameterValue;
|
||||||
|
if (dataType === 'dateTime') {
|
||||||
|
value2 = convertDateTime(value2);
|
||||||
|
}
|
||||||
|
|
||||||
|
compareOperationResult = compareOperationFunctions[ruleData.operation as string](
|
||||||
|
value1,
|
||||||
|
value2,
|
||||||
|
);
|
||||||
|
|
||||||
|
if (compareOperationResult) {
|
||||||
|
// If rule matches add it to the correct output and continue with next item
|
||||||
|
checkIndexRange(ruleData.output as number);
|
||||||
|
|
||||||
|
const ruleIndex = rules.indexOf(ruleData);
|
||||||
|
returnData[ruleIndex].push(item);
|
||||||
|
continue itemLoop;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if a fallback output got defined and route accordingly
|
||||||
|
outputIndex = this.getNodeParameter('fallbackOutput', itemIndex) as number;
|
||||||
|
if (outputIndex !== -1) {
|
||||||
|
checkIndexRange(outputIndex);
|
||||||
|
returnData[outputIndex].push(item);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
if (this.continueOnFail()) {
|
||||||
|
returnData[0].push({ json: { error: error.message } });
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return returnData;
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,171 @@
|
||||||
|
{
|
||||||
|
"name": "My workflow 109",
|
||||||
|
"nodes": [
|
||||||
|
{
|
||||||
|
"parameters": {},
|
||||||
|
"id": "7ae16f96-5c2c-44a3-9f96-167e426336f9",
|
||||||
|
"name": "When clicking \"Execute Workflow\"",
|
||||||
|
"type": "n8n-nodes-base.manualTrigger",
|
||||||
|
"typeVersion": 1,
|
||||||
|
"position": [
|
||||||
|
620,
|
||||||
|
720
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"parameters": {
|
||||||
|
"jsCode": "return [{\n \"output\": \"third\",\n \"text\": \"third output text\"\n}, {\n \"output\": \"fourth\",\n \"text\": \"fourth output text\"\n}, {\n \"output\": \"first\",\n \"text\": \"first output text\"\n}, {\n \"output\": \"second\",\n \"text\": \"second output text\"\n}]"
|
||||||
|
},
|
||||||
|
"id": "31e9aada-7aa2-4c62-8e15-0cecb91788e4",
|
||||||
|
"name": "Code",
|
||||||
|
"type": "n8n-nodes-base.code",
|
||||||
|
"typeVersion": 2,
|
||||||
|
"position": [
|
||||||
|
840,
|
||||||
|
720
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"parameters": {},
|
||||||
|
"id": "cf10b4c7-16a6-4c16-a17c-7b83f954f7b9",
|
||||||
|
"name": "No Operation, do nothing",
|
||||||
|
"type": "n8n-nodes-base.noOp",
|
||||||
|
"typeVersion": 1,
|
||||||
|
"position": [
|
||||||
|
1380,
|
||||||
|
560
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"parameters": {},
|
||||||
|
"id": "3e7e7f4a-bff9-4ce1-a5e5-58505853260f",
|
||||||
|
"name": "No Operation, do nothing1",
|
||||||
|
"type": "n8n-nodes-base.noOp",
|
||||||
|
"typeVersion": 1,
|
||||||
|
"position": [
|
||||||
|
1380,
|
||||||
|
720
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"parameters": {},
|
||||||
|
"id": "205f59d6-52f5-4412-9511-b680a91d0be2",
|
||||||
|
"name": "No Operation, do nothing2",
|
||||||
|
"type": "n8n-nodes-base.noOp",
|
||||||
|
"typeVersion": 1,
|
||||||
|
"position": [
|
||||||
|
1380,
|
||||||
|
880
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"parameters": {
|
||||||
|
"mode": "expression",
|
||||||
|
"output": "={{ Math.max(0, ['first', 'second', 'third'].indexOf( $json.output)) }}",
|
||||||
|
"outputsAmount": 3
|
||||||
|
},
|
||||||
|
"id": "9c3dc163-0103-45c2-8455-e6ab3e84679c",
|
||||||
|
"name": "Switch1",
|
||||||
|
"type": "n8n-nodes-base.switch",
|
||||||
|
"typeVersion": 2,
|
||||||
|
"position": [
|
||||||
|
1100,
|
||||||
|
720
|
||||||
|
]
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"pinData": {
|
||||||
|
"No Operation, do nothing2": [
|
||||||
|
{
|
||||||
|
"json": {
|
||||||
|
"output": "third",
|
||||||
|
"text": "third output text"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"No Operation, do nothing1": [
|
||||||
|
{
|
||||||
|
"json": {
|
||||||
|
"output": "second",
|
||||||
|
"text": "second output text"
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"No Operation, do nothing": [
|
||||||
|
{
|
||||||
|
"json": {
|
||||||
|
"output": "fourth",
|
||||||
|
"text": "fourth output text"
|
||||||
|
}
|
||||||
|
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"json": {
|
||||||
|
"output": "first",
|
||||||
|
"text": "first output text"
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"connections": {
|
||||||
|
"When clicking \"Execute Workflow\"": {
|
||||||
|
"main": [
|
||||||
|
[
|
||||||
|
{
|
||||||
|
"node": "Code",
|
||||||
|
"type": "main",
|
||||||
|
"index": 0
|
||||||
|
}
|
||||||
|
]
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"Code": {
|
||||||
|
"main": [
|
||||||
|
[
|
||||||
|
{
|
||||||
|
"node": "Switch1",
|
||||||
|
"type": "main",
|
||||||
|
"index": 0
|
||||||
|
}
|
||||||
|
]
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"Switch1": {
|
||||||
|
"main": [
|
||||||
|
[
|
||||||
|
{
|
||||||
|
"node": "No Operation, do nothing",
|
||||||
|
"type": "main",
|
||||||
|
"index": 0
|
||||||
|
}
|
||||||
|
],
|
||||||
|
[
|
||||||
|
{
|
||||||
|
"node": "No Operation, do nothing1",
|
||||||
|
"type": "main",
|
||||||
|
"index": 0
|
||||||
|
}
|
||||||
|
],
|
||||||
|
[
|
||||||
|
{
|
||||||
|
"node": "No Operation, do nothing2",
|
||||||
|
"type": "main",
|
||||||
|
"index": 0
|
||||||
|
}
|
||||||
|
]
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"active": false,
|
||||||
|
"settings": {
|
||||||
|
"executionOrder": "v1"
|
||||||
|
},
|
||||||
|
"versionId": "cca0f0b9-d01e-435b-9125-9616007f4aea",
|
||||||
|
"id": "xjPY8ZYJK53G6nQ1",
|
||||||
|
"meta": {
|
||||||
|
"instanceId": "ec7a5f4ffdb34436e59d23eaccb5015b5238de2a877e205b28572bf1ffecfe04"
|
||||||
|
},
|
||||||
|
"tags": []
|
||||||
|
}
|
|
@ -0,0 +1,4 @@
|
||||||
|
import { testWorkflows, getWorkflowFilenames } from '@test/nodes/Helpers';
|
||||||
|
const workflows = getWorkflowFilenames(__dirname);
|
||||||
|
|
||||||
|
describe('Execute Switch Node', () => testWorkflows(workflows));
|
|
@ -0,0 +1,187 @@
|
||||||
|
{
|
||||||
|
"name": "My workflow 109",
|
||||||
|
"nodes": [
|
||||||
|
{
|
||||||
|
"parameters": {},
|
||||||
|
"id": "7ae16f96-5c2c-44a3-9f96-167e426336f9",
|
||||||
|
"name": "When clicking \"Execute Workflow\"",
|
||||||
|
"type": "n8n-nodes-base.manualTrigger",
|
||||||
|
"typeVersion": 1,
|
||||||
|
"position": [
|
||||||
|
620,
|
||||||
|
720
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"parameters": {
|
||||||
|
"jsCode": "return [{\n \"output\": \"third\",\n \"text\": \"third output text\"\n}, {\n \"output\": \"fourth\",\n \"text\": \"fourth output text\"\n}, {\n \"output\": \"first\",\n \"text\": \"first output text\"\n}, {\n \"output\": \"second\",\n \"text\": \"second output text\"\n}]"
|
||||||
|
},
|
||||||
|
"id": "31e9aada-7aa2-4c62-8e15-0cecb91788e4",
|
||||||
|
"name": "Code",
|
||||||
|
"type": "n8n-nodes-base.code",
|
||||||
|
"typeVersion": 2,
|
||||||
|
"position": [
|
||||||
|
840,
|
||||||
|
720
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"parameters": {
|
||||||
|
"dataType": "string",
|
||||||
|
"value1": "={{ $json.output }}",
|
||||||
|
"rules": {
|
||||||
|
"rules": [
|
||||||
|
{
|
||||||
|
"value2": "first",
|
||||||
|
"outputKey": "First Output"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"value2": "second",
|
||||||
|
"outputKey": "Second Output"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"value2": "third",
|
||||||
|
"outputKey": "Third Output"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"fallbackOutput": 2
|
||||||
|
},
|
||||||
|
"id": "0dd6e98a-2830-42fb-9a9d-6d4ff8678cbd",
|
||||||
|
"name": "Switch",
|
||||||
|
"type": "n8n-nodes-base.switch",
|
||||||
|
"typeVersion": 2,
|
||||||
|
"position": [
|
||||||
|
1120,
|
||||||
|
720
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"parameters": {},
|
||||||
|
"id": "cf10b4c7-16a6-4c16-a17c-7b83f954f7b9",
|
||||||
|
"name": "No Operation, do nothing",
|
||||||
|
"type": "n8n-nodes-base.noOp",
|
||||||
|
"typeVersion": 1,
|
||||||
|
"position": [
|
||||||
|
1380,
|
||||||
|
560
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"parameters": {},
|
||||||
|
"id": "3e7e7f4a-bff9-4ce1-a5e5-58505853260f",
|
||||||
|
"name": "No Operation, do nothing1",
|
||||||
|
"type": "n8n-nodes-base.noOp",
|
||||||
|
"typeVersion": 1,
|
||||||
|
"position": [
|
||||||
|
1380,
|
||||||
|
720
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"parameters": {},
|
||||||
|
"id": "205f59d6-52f5-4412-9511-b680a91d0be2",
|
||||||
|
"name": "No Operation, do nothing2",
|
||||||
|
"type": "n8n-nodes-base.noOp",
|
||||||
|
"typeVersion": 1,
|
||||||
|
"position": [
|
||||||
|
1380,
|
||||||
|
880
|
||||||
|
]
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"pinData": {
|
||||||
|
"No Operation, do nothing": [
|
||||||
|
{
|
||||||
|
"json": {
|
||||||
|
"output": "first",
|
||||||
|
"text": "first output text"
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"No Operation, do nothing1": [
|
||||||
|
{
|
||||||
|
"json": {
|
||||||
|
"output": "second",
|
||||||
|
"text": "second output text"
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"No Operation, do nothing2": [
|
||||||
|
{
|
||||||
|
"json": {
|
||||||
|
"output": "third",
|
||||||
|
"text": "third output text"
|
||||||
|
}
|
||||||
|
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"json": {
|
||||||
|
"output": "fourth",
|
||||||
|
"text": "fourth output text"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"connections": {
|
||||||
|
"When clicking \"Execute Workflow\"": {
|
||||||
|
"main": [
|
||||||
|
[
|
||||||
|
{
|
||||||
|
"node": "Code",
|
||||||
|
"type": "main",
|
||||||
|
"index": 0
|
||||||
|
}
|
||||||
|
]
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"Code": {
|
||||||
|
"main": [
|
||||||
|
[
|
||||||
|
{
|
||||||
|
"node": "Switch",
|
||||||
|
"type": "main",
|
||||||
|
"index": 0
|
||||||
|
}
|
||||||
|
]
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"Switch": {
|
||||||
|
"main": [
|
||||||
|
[
|
||||||
|
{
|
||||||
|
"node": "No Operation, do nothing",
|
||||||
|
"type": "main",
|
||||||
|
"index": 0
|
||||||
|
}
|
||||||
|
],
|
||||||
|
[
|
||||||
|
{
|
||||||
|
"node": "No Operation, do nothing1",
|
||||||
|
"type": "main",
|
||||||
|
"index": 0
|
||||||
|
}
|
||||||
|
],
|
||||||
|
[
|
||||||
|
{
|
||||||
|
"node": "No Operation, do nothing2",
|
||||||
|
"type": "main",
|
||||||
|
"index": 0
|
||||||
|
}
|
||||||
|
]
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"active": false,
|
||||||
|
"settings": {
|
||||||
|
"executionOrder": "v1"
|
||||||
|
},
|
||||||
|
"versionId": "627af20f-47fc-47a7-8da6-a7e7b21225df",
|
||||||
|
"id": "xjPY8ZYJK53G6nQ1",
|
||||||
|
"meta": {
|
||||||
|
"instanceId": "ec7a5f4ffdb34436e59d23eaccb5015b5238de2a877e205b28572bf1ffecfe04"
|
||||||
|
},
|
||||||
|
"tags": []
|
||||||
|
}
|
|
@ -1027,7 +1027,7 @@ export function getNodeInputs(
|
||||||
node: INode,
|
node: INode,
|
||||||
nodeTypeData: INodeTypeDescription,
|
nodeTypeData: INodeTypeDescription,
|
||||||
): Array<ConnectionTypes | INodeInputConfiguration> {
|
): Array<ConnectionTypes | INodeInputConfiguration> {
|
||||||
if (Array.isArray(nodeTypeData.inputs)) {
|
if (Array.isArray(nodeTypeData?.inputs)) {
|
||||||
return nodeTypeData.inputs;
|
return nodeTypeData.inputs;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
Loading…
Reference in a new issue