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.getters
|
||||
.canvasNodeByName('Code')
|
||||
.should('have.attr', 'style', 'left: 860px; top: 220px;');
|
||||
.should('have.css', 'left', '860px')
|
||||
.should('have.css', 'top', '220px')
|
||||
|
||||
WorkflowPage.actions.hitUndo();
|
||||
WorkflowPage.getters.canvasNodes().should('have.have.length', 2);
|
||||
WorkflowPage.getters.nodeConnections().should('have.length', 1);
|
||||
|
@ -59,7 +61,8 @@ describe('Undo/Redo', () => {
|
|||
// Last node should be added back to original position
|
||||
WorkflowPage.getters
|
||||
.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', () => {
|
||||
|
@ -133,15 +136,19 @@ describe('Undo/Redo', () => {
|
|||
cy.drag('[data-test-id="canvas-node"].jtk-drag-selected', [50, 150], { clickToFinish: true });
|
||||
WorkflowPage.getters
|
||||
.canvasNodeByName('Code')
|
||||
.should('have.attr', 'style', 'left: 740px; top: 320px;');
|
||||
.should('have.css', 'left', '740px')
|
||||
.should('have.css', 'top', '320px')
|
||||
|
||||
WorkflowPage.actions.hitUndo();
|
||||
WorkflowPage.getters
|
||||
.canvasNodeByName('Code')
|
||||
.should('have.attr', 'style', 'left: 640px; top: 220px;');
|
||||
.should('have.css', 'left', '640px')
|
||||
.should('have.css', 'top', '220px')
|
||||
WorkflowPage.actions.hitRedo();
|
||||
WorkflowPage.getters
|
||||
.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', () => {
|
||||
|
@ -269,8 +276,8 @@ describe('Undo/Redo', () => {
|
|||
});
|
||||
|
||||
it('should undo/redo multiple steps', () => {
|
||||
const initialPosition = 'left: 420px; top: 220px;';
|
||||
const movedPosition = 'left: 540px; top: 360px;';
|
||||
const initialPosition = {left: '420px', top: '220px'};
|
||||
const movedPosition = {left: '540px', top: '360px'};
|
||||
|
||||
WorkflowPage.actions.addNodeToCanvas(SCHEDULE_TRIGGER_NODE_NAME);
|
||||
WorkflowPage.actions.addNodeToCanvas(CODE_NODE_NAME);
|
||||
|
@ -283,10 +290,17 @@ describe('Undo/Redo', () => {
|
|||
WorkflowPage.getters.canvasNodes().last().click();
|
||||
WorkflowPage.actions.hitDisableNodeShortcut();
|
||||
// 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();
|
||||
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
|
||||
WorkflowPage.getters.canvasNodeByName(EDIT_FIELDS_SET_NODE_NAME).click().click();
|
||||
cy.get('body').type('{backspace}');
|
||||
|
@ -297,7 +311,10 @@ describe('Undo/Redo', () => {
|
|||
WorkflowPage.getters.nodeConnections().should('have.length', 3);
|
||||
// Second undo: Should move first node to it's original position
|
||||
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
|
||||
WorkflowPage.actions.hitUndo();
|
||||
WorkflowPage.getters.disabledNodes().should('have.length', 0);
|
||||
|
@ -307,7 +324,10 @@ describe('Undo/Redo', () => {
|
|||
WorkflowPage.getters.disabledNodes().should('have.length', 1);
|
||||
// Second redo: Should move the first node
|
||||
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
|
||||
WorkflowPage.actions.hitRedo();
|
||||
WorkflowPage.getters.canvasNodes().should('have.length', 3);
|
||||
|
|
|
@ -133,7 +133,8 @@ describe('Canvas Actions', () => {
|
|||
WorkflowPage.getters
|
||||
.canvasNodes()
|
||||
.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', () => {
|
||||
|
|
|
@ -8,11 +8,11 @@ import {
|
|||
MERGE_NODE_NAME,
|
||||
} from './../constants';
|
||||
import { WorkflowPage as WorkflowPageClass } from '../pages/workflow';
|
||||
import { WorkflowExecutionsTab } from '../pages';
|
||||
import { NDV, WorkflowExecutionsTab } from '../pages';
|
||||
|
||||
const WorkflowPage = new WorkflowPageClass();
|
||||
const ExecutionsTab = new WorkflowExecutionsTab();
|
||||
|
||||
const NDVDialog = new NDV();
|
||||
const DEFAULT_ZOOM_FACTOR = 1;
|
||||
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
|
||||
|
@ -29,10 +29,15 @@ describe('Canvas Node Manipulation and Navigation', () => {
|
|||
});
|
||||
|
||||
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 < 4; i++) {
|
||||
for (let i = 0; i < desiredOutputs; 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.nodeCreatorSearchBar().should('be.visible');
|
||||
WorkflowPage.actions.addNodeToCanvas(EDIT_FIELDS_SET_NODE_NAME, false);
|
||||
|
@ -42,7 +47,7 @@ describe('Canvas Node Manipulation and Navigation', () => {
|
|||
cy.reload();
|
||||
cy.waitForLoad();
|
||||
// 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 : ''}`;
|
||||
WorkflowPage.getters
|
||||
.canvasNodeInputEndpointByName(setName)
|
||||
|
@ -167,7 +172,8 @@ describe('Canvas Node Manipulation and Navigation', () => {
|
|||
WorkflowPage.getters
|
||||
.canvasNodes()
|
||||
.last()
|
||||
.should('have.attr', 'style', 'left: 740px; top: 320px;');
|
||||
.should('have.css', 'left', '740px')
|
||||
.should('have.css', 'top', '320px')
|
||||
});
|
||||
|
||||
it('should zoom in', () => {
|
||||
|
|
|
@ -184,7 +184,14 @@ import { nodeHelpers } from '@/mixins/nodeHelpers';
|
|||
import { workflowHelpers } from '@/mixins/workflowHelpers';
|
||||
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 NodeIcon from '@/components/NodeIcon.vue';
|
||||
|
@ -357,9 +364,15 @@ export default defineComponent({
|
|||
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) {
|
||||
const requiredNonMainInputs = this.inputs.filter(
|
||||
const requiredNonMainInputs = inputs.filter(
|
||||
(input) => typeof input !== 'string' && input.required,
|
||||
);
|
||||
|
||||
|
@ -373,6 +386,15 @@ export default defineComponent({
|
|||
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;
|
||||
},
|
||||
nodeClass(): object {
|
||||
|
@ -698,7 +720,12 @@ export default defineComponent({
|
|||
<style lang="scss" scoped>
|
||||
.node-wrapper {
|
||||
--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-input-width: 65px;
|
||||
|
@ -1158,6 +1185,7 @@ export default defineComponent({
|
|||
--endpoint-size-small: 14px;
|
||||
--endpoint-size-medium: 18px;
|
||||
--stalk-size: 40px;
|
||||
--stalk-switch-size: 60px;
|
||||
--stalk-success-size: 87px;
|
||||
--stalk-success-size-without-label: 40px;
|
||||
--stalk-long-size: 127px;
|
||||
|
@ -1405,6 +1433,15 @@ export default defineComponent({
|
|||
margin-top: calc(var(--spacing-l) * -1);
|
||||
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 {
|
||||
|
@ -1446,6 +1483,7 @@ export default defineComponent({
|
|||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
.long-stalk {
|
||||
--stalk-size: var(--stalk-long-size);
|
||||
}
|
||||
|
@ -1455,4 +1493,7 @@ export default defineComponent({
|
|||
.ep-success--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>
|
||||
|
|
|
@ -502,6 +502,7 @@ import type {
|
|||
IBinaryKeyData,
|
||||
IDataObject,
|
||||
INodeExecutionData,
|
||||
INodeOutputConfiguration,
|
||||
INodeTypeDescription,
|
||||
IRunData,
|
||||
IRunExecutionData,
|
||||
|
@ -543,6 +544,7 @@ import { useWorkflowsStore } from '@/stores/workflows.store';
|
|||
import { useNDVStore } from '@/stores/ndv.store';
|
||||
import { useNodeTypesStore } from '@/stores/nodeTypes.store';
|
||||
import { useToast } from '@/composables';
|
||||
import { isObject } from 'lodash-es';
|
||||
|
||||
const RunDataTable = defineAsyncComponent(async () => import('@/components/RunDataTable.vue'));
|
||||
const RunDataJson = defineAsyncComponent(async () => import('@/components/RunDataJson.vue'));
|
||||
|
@ -891,9 +893,8 @@ export default defineComponent({
|
|||
return this.outputIndex;
|
||||
},
|
||||
branches(): ITab[] {
|
||||
function capitalize(name: string) {
|
||||
return name.charAt(0).toLocaleUpperCase() + name.slice(1);
|
||||
}
|
||||
const capitalize = (name: string) => name.charAt(0).toLocaleUpperCase() + name.slice(1);
|
||||
|
||||
const branches: ITab[] = [];
|
||||
|
||||
for (let i = 0; i <= this.maxOutputIndex; i++) {
|
||||
|
@ -903,6 +904,7 @@ export default defineComponent({
|
|||
const itemsCount = this.getDataCount(this.runIndex, i);
|
||||
const items = this.$locale.baseText('ndv.output.items', { adjustToNumber: itemsCount });
|
||||
let outputName = this.getOutputName(i);
|
||||
|
||||
if (`${outputName}` === `${i}`) {
|
||||
outputName = `${this.$locale.baseText('ndv.output')} ${outputName}`;
|
||||
} else {
|
||||
|
@ -936,6 +938,18 @@ export default defineComponent({
|
|||
},
|
||||
},
|
||||
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) {
|
||||
if (itemIndex === null) {
|
||||
this.$emit('itemHover', null);
|
||||
|
@ -1287,9 +1301,7 @@ export default defineComponent({
|
|||
this.closeBinaryDataDisplay();
|
||||
let outputTypes: ConnectionTypes[] = [];
|
||||
if (this.nodeType !== null && this.node !== null) {
|
||||
const workflow = this.workflowsStore.getCurrentWorkflow();
|
||||
const workflowNode = workflow.getNode(this.node.name);
|
||||
const outputs = NodeHelpers.getNodeOutputs(workflow, workflowNode, this.nodeType);
|
||||
const outputs = this.getResolvedNodeOutputs();
|
||||
outputTypes = NodeHelpers.getConnectionTypes(outputs);
|
||||
}
|
||||
this.connectionType = outputTypes.length === 0 ? NodeConnectionType.Main : outputTypes[0];
|
||||
|
@ -1367,6 +1379,12 @@ export default defineComponent({
|
|||
}
|
||||
|
||||
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) {
|
||||
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 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'];
|
||||
|
||||
|
|
|
@ -261,6 +261,7 @@ export const nodeBase = defineComponent({
|
|||
nodeId: this.nodeId,
|
||||
index: typeIndex,
|
||||
totalEndpoints: inputsOfSameRootType.length,
|
||||
nodeType: node.type,
|
||||
};
|
||||
}
|
||||
|
||||
|
@ -423,6 +424,7 @@ export const nodeBase = defineComponent({
|
|||
this.$refs[this.data.name] as Element,
|
||||
newEndpointData,
|
||||
);
|
||||
|
||||
this.__addEndpointTestingData(endpoint, 'output', typeIndex);
|
||||
if (outputConfiguration.displayName || nodeTypeData.outputNames?.[i]) {
|
||||
// Apply output names if they got set
|
||||
|
@ -439,6 +441,7 @@ export const nodeBase = defineComponent({
|
|||
nodeId: this.nodeId,
|
||||
index: typeIndex,
|
||||
totalEndpoints: outputsOfSameRootType.length,
|
||||
nodeType: node.type,
|
||||
};
|
||||
}
|
||||
|
||||
|
@ -454,6 +457,7 @@ export const nodeBase = defineComponent({
|
|||
connectedEndpoint: endpoint,
|
||||
showOutputLabel: outputs.length === 1,
|
||||
size: outputs.length >= 3 ? 'small' : 'medium',
|
||||
nodeType: node.type,
|
||||
hoverMessage: this.$locale.baseText('nodeBase.clickToAddNodeOrDragToConnect'),
|
||||
},
|
||||
},
|
||||
|
@ -486,6 +490,7 @@ export const nodeBase = defineComponent({
|
|||
nodeName: node.name,
|
||||
nodeId: this.nodeId,
|
||||
index: typeIndex,
|
||||
nodeType: node.type,
|
||||
totalEndpoints: outputsOfSameRootType.length,
|
||||
};
|
||||
}
|
||||
|
|
|
@ -14,6 +14,7 @@ interface N8nPlusEndpointParams extends EndpointRepresentationParams {
|
|||
dimensions: number;
|
||||
connectedEndpoint: Endpoint;
|
||||
hoverMessage: string;
|
||||
nodeType: string;
|
||||
size: 'small' | 'medium';
|
||||
showOutputLabel: boolean;
|
||||
}
|
||||
|
@ -23,15 +24,17 @@ export const N8nPlusEndpointType = 'N8nPlus';
|
|||
export const EVENT_PLUS_ENDPOINT_CLICK = 'eventPlusEndpointClick';
|
||||
export class N8nPlusEndpoint extends EndpointRepresentation<ComputedN8nPlusEndpoint> {
|
||||
params: N8nPlusEndpointParams;
|
||||
|
||||
label: string;
|
||||
|
||||
stalkOverlay: Overlay | null;
|
||||
|
||||
messageOverlay: Overlay | null;
|
||||
|
||||
constructor(endpoint: Endpoint, params: N8nPlusEndpointParams) {
|
||||
super(endpoint, params);
|
||||
|
||||
this.params = params;
|
||||
|
||||
this.label = '';
|
||||
this.stalkOverlay = null;
|
||||
this.messageOverlay = null;
|
||||
|
@ -41,6 +44,7 @@ export class N8nPlusEndpoint extends EndpointRepresentation<ComputedN8nPlusEndpo
|
|||
}
|
||||
|
||||
static type = N8nPlusEndpointType;
|
||||
|
||||
type = N8nPlusEndpoint.type;
|
||||
|
||||
setupOverlays() {
|
||||
|
@ -50,6 +54,9 @@ export class N8nPlusEndpoint extends EndpointRepresentation<ComputedN8nPlusEndpo
|
|||
type: 'Custom',
|
||||
options: {
|
||||
id: PlusStalkOverlay,
|
||||
attributes: {
|
||||
'data-endpoint-node-type': this.params.nodeType,
|
||||
},
|
||||
create: () => {
|
||||
const stalk = createElement('div', {}, `${PlusStalkOverlay} ${this.params.size}`);
|
||||
return stalk;
|
||||
|
@ -61,6 +68,9 @@ export class N8nPlusEndpoint extends EndpointRepresentation<ComputedN8nPlusEndpo
|
|||
options: {
|
||||
id: HoverMessageOverlay,
|
||||
location: 0.5,
|
||||
attributes: {
|
||||
'data-endpoint-node-type': this.params.nodeType,
|
||||
},
|
||||
create: () => {
|
||||
const hoverMessage = createElement('p', {}, `${HoverMessageOverlay} ${this.params.size}`);
|
||||
hoverMessage.innerHTML = this.params.hoverMessage;
|
||||
|
@ -70,23 +80,27 @@ export class N8nPlusEndpoint extends EndpointRepresentation<ComputedN8nPlusEndpo
|
|||
});
|
||||
this.endpoint.instance.setSuspendDrawing(false);
|
||||
}
|
||||
|
||||
bindEvents() {
|
||||
this.instance.bind(EVENT_ENDPOINT_MOUSEOVER, this.setHoverMessageVisible);
|
||||
this.instance.bind(EVENT_ENDPOINT_MOUSEOUT, this.unsetHoverMessageVisible);
|
||||
this.instance.bind(EVENT_ENDPOINT_CLICK, this.fireClickEvent);
|
||||
this.instance.bind(EVENT_CONNECTION_ABORT, this.setStalkLabels);
|
||||
}
|
||||
|
||||
unbindEvents() {
|
||||
this.instance.unbind(EVENT_ENDPOINT_MOUSEOVER, this.setHoverMessageVisible);
|
||||
this.instance.unbind(EVENT_ENDPOINT_MOUSEOUT, this.unsetHoverMessageVisible);
|
||||
this.instance.unbind(EVENT_ENDPOINT_CLICK, this.fireClickEvent);
|
||||
this.instance.unbind(EVENT_CONNECTION_ABORT, this.setStalkLabels);
|
||||
}
|
||||
|
||||
setStalkLabels = () => {
|
||||
if (!this.endpoint) return;
|
||||
|
||||
const stalkOverlay = this.endpoint.getOverlay(PlusStalkOverlay);
|
||||
const messageOverlay = this.endpoint.getOverlay(HoverMessageOverlay);
|
||||
|
||||
if (stalkOverlay && messageOverlay) {
|
||||
// Increase the size of the stalk overlay if the label is too long
|
||||
const fnKey = this.label.length > 10 ? 'add' : 'remove';
|
||||
|
@ -100,21 +114,25 @@ export class N8nPlusEndpoint extends EndpointRepresentation<ComputedN8nPlusEndpo
|
|||
}
|
||||
}
|
||||
};
|
||||
|
||||
fireClickEvent = (endpoint: Endpoint) => {
|
||||
if (endpoint === this.endpoint) {
|
||||
this.instance.fire(EVENT_PLUS_ENDPOINT_CLICK, this.endpoint);
|
||||
}
|
||||
};
|
||||
|
||||
setHoverMessageVisible = (endpoint: Endpoint) => {
|
||||
if (endpoint === this.endpoint && this.messageOverlay) {
|
||||
this.instance.addOverlayClass(this.messageOverlay, 'visible');
|
||||
}
|
||||
};
|
||||
|
||||
unsetHoverMessageVisible = (endpoint: Endpoint) => {
|
||||
if (endpoint === this.endpoint && this.messageOverlay) {
|
||||
this.instance.removeOverlayClass(this.messageOverlay, 'visible');
|
||||
}
|
||||
};
|
||||
|
||||
clearOverlays() {
|
||||
Object.keys(this.endpoint.getOverlays()).forEach((key) => {
|
||||
this.endpoint.removeOverlay(key);
|
||||
|
@ -122,6 +140,7 @@ export class N8nPlusEndpoint extends EndpointRepresentation<ComputedN8nPlusEndpo
|
|||
this.stalkOverlay = null;
|
||||
this.messageOverlay = null;
|
||||
}
|
||||
|
||||
getConnections() {
|
||||
const connections = [
|
||||
...this.endpoint.connections,
|
||||
|
@ -130,6 +149,7 @@ export class N8nPlusEndpoint extends EndpointRepresentation<ComputedN8nPlusEndpo
|
|||
|
||||
return connections;
|
||||
}
|
||||
|
||||
setIsVisible(visible: boolean) {
|
||||
this.instance.setSuspendDrawing(true);
|
||||
Object.keys(this.endpoint.getOverlays()).forEach((overlay) => {
|
||||
|
@ -179,6 +199,7 @@ export const N8nPlusEndpointHandler: EndpointHandler<N8nPlusEndpoint, ComputedN8
|
|||
ep.w = w;
|
||||
ep.h = h;
|
||||
|
||||
ep.canvas?.setAttribute('data-endpoint-node-type', ep.params.nodeType);
|
||||
ep.addClass('plus-endpoint');
|
||||
return [x, y, w, h, ep.params.dimensions];
|
||||
},
|
||||
|
|
|
@ -181,46 +181,18 @@ export const getAnchorPosition = (
|
|||
spacerIndexes: number[] = [],
|
||||
): ArrayAnchorSpec[] => {
|
||||
if (connectionType === NodeConnectionType.Main) {
|
||||
const positions = {
|
||||
input: {
|
||||
1: [[0.01, 0.5, -1, 0]],
|
||||
2: [
|
||||
[0.01, 0.3, -1, 0],
|
||||
[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],
|
||||
],
|
||||
},
|
||||
};
|
||||
const anchors: ArrayAnchorSpec[] = [];
|
||||
const x = type === 'input' ? 0.01 : 0.99;
|
||||
const ox = type === 'input' ? -1 : 1;
|
||||
const oy = 0;
|
||||
const stepSize = 1 / (amount + 1); // +1 to not touch the node boundaries
|
||||
|
||||
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;
|
||||
|
@ -317,15 +289,20 @@ export const getOutputEndpointStyle = (
|
|||
outlineStroke: 'none',
|
||||
});
|
||||
|
||||
export const getOutputNameOverlay = (labelText: string, outputName: string): OverlaySpec => ({
|
||||
export const getOutputNameOverlay = (
|
||||
labelText: string,
|
||||
outputName: ConnectionTypes,
|
||||
): OverlaySpec => ({
|
||||
type: 'Custom',
|
||||
options: {
|
||||
id: OVERLAY_OUTPUT_NAME_LABEL,
|
||||
visible: true,
|
||||
create: (component: Endpoint) => {
|
||||
create: (ep: Endpoint) => {
|
||||
const label = document.createElement('div');
|
||||
label.innerHTML = labelText;
|
||||
label.classList.add('node-output-endpoint-label');
|
||||
|
||||
label.setAttribute('data-endpoint-node-type', ep?.__meta?.nodeType);
|
||||
if (outputName !== NodeConnectionType.Main) {
|
||||
label.classList.add('node-output-endpoint-label--data');
|
||||
label.classList.add(`node-connection-type-${getScope(outputName)}`);
|
||||
|
|
|
@ -1,686 +1,25 @@
|
|||
import type {
|
||||
IExecuteFunctions,
|
||||
INodeExecutionData,
|
||||
INodeParameters,
|
||||
INodeType,
|
||||
INodeTypeDescription,
|
||||
NodeParameterValue,
|
||||
} from 'n8n-workflow';
|
||||
import { NodeOperationError } from 'n8n-workflow';
|
||||
import type { INodeTypeBaseDescription, IVersionedNodeType } from 'n8n-workflow';
|
||||
import { VersionedNodeType } from 'n8n-workflow';
|
||||
|
||||
export class Switch implements INodeType {
|
||||
description: INodeTypeDescription = {
|
||||
import { SwitchV1 } from './V1/SwitchV1.node';
|
||||
import { SwitchV2 } from './V2/SwitchV2.node';
|
||||
|
||||
export class Switch extends VersionedNodeType {
|
||||
constructor() {
|
||||
const baseDescription: INodeTypeBaseDescription = {
|
||||
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',
|
||||
},
|
||||
|
||||
// ----------------------------------
|
||||
// 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',
|
||||
},
|
||||
],
|
||||
defaultVersion: 2,
|
||||
};
|
||||
|
||||
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);
|
||||
},
|
||||
const nodeVersions: IVersionedNodeType['nodeVersions'] = {
|
||||
1: new SwitchV1(baseDescription),
|
||||
2: new SwitchV2(baseDescription),
|
||||
};
|
||||
|
||||
// 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;
|
||||
super(nodeVersions, baseDescription);
|
||||
}
|
||||
}
|
||||
|
|
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,
|
||||
nodeTypeData: INodeTypeDescription,
|
||||
): Array<ConnectionTypes | INodeInputConfiguration> {
|
||||
if (Array.isArray(nodeTypeData.inputs)) {
|
||||
if (Array.isArray(nodeTypeData?.inputs)) {
|
||||
return nodeTypeData.inputs;
|
||||
}
|
||||
|
||||
|
|
Loading…
Reference in a new issue