mirror of
https://github.com/Abdulazizzn/n8n-enterprise-unlocked.git
synced 2025-12-17 18:12:04 +00:00
feat(Switch Node): Overhaul (#7855)
Github issue / Community forum post (link here to close automatically): https://community.n8n.io/t/switch-node-to-more-than-one-path/32791/2 https://community.n8n.io/t/switch-node-routing-same-value-multiple-output/29424 --------- Co-authored-by: Elias Meire <elias@meire.dev> Co-authored-by: Giulio Andreini <andreini@netseven.it>
This commit is contained in:
@@ -288,7 +288,6 @@ const onBlur = (): void => {
|
|||||||
display: flex;
|
display: flex;
|
||||||
align-items: flex-end;
|
align-items: flex-end;
|
||||||
gap: var(--spacing-4xs);
|
gap: var(--spacing-4xs);
|
||||||
padding-left: var(--spacing-l);
|
|
||||||
|
|
||||||
&.hasIssues {
|
&.hasIssues {
|
||||||
--input-border-color: var(--color-danger);
|
--input-border-color: var(--color-danger);
|
||||||
@@ -354,7 +353,6 @@ const onBlur = (): void => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.remove {
|
.remove {
|
||||||
--button-font-color: var(--color-text-light);
|
|
||||||
position: absolute;
|
position: absolute;
|
||||||
left: 0;
|
left: 0;
|
||||||
top: var(--spacing-l);
|
top: var(--spacing-l);
|
||||||
|
|||||||
@@ -61,6 +61,8 @@ const maxConditions = computed(
|
|||||||
() => props.parameter.typeOptions?.filter?.maxConditions ?? DEFAULT_MAX_CONDITIONS,
|
() => props.parameter.typeOptions?.filter?.maxConditions ?? DEFAULT_MAX_CONDITIONS,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const singleCondition = computed(() => props.parameter.typeOptions?.multipleValues === false);
|
||||||
|
|
||||||
const maxConditionsReached = computed(
|
const maxConditionsReached = computed(
|
||||||
() => maxConditions.value <= state.paramValue.conditions.length,
|
() => maxConditions.value <= state.paramValue.conditions.length,
|
||||||
);
|
);
|
||||||
@@ -125,8 +127,12 @@ function getIssues(index: number): string[] {
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div :class="$style.filter" :data-test-id="`filter-${parameter.name}`">
|
<div
|
||||||
|
:class="{ [$style.filter]: true, [$style.single]: singleCondition }"
|
||||||
|
:data-test-id="`filter-${parameter.name}`"
|
||||||
|
>
|
||||||
<n8n-input-label
|
<n8n-input-label
|
||||||
|
v-if="!singleCondition"
|
||||||
:label="parameter.displayName"
|
:label="parameter.displayName"
|
||||||
:underline="true"
|
:underline="true"
|
||||||
:show-options="true"
|
:show-options="true"
|
||||||
@@ -154,12 +160,13 @@ function getIssues(index: number): string[] {
|
|||||||
:can-remove="index !== 0 || state.paramValue.conditions.length > 1"
|
:can-remove="index !== 0 || state.paramValue.conditions.length > 1"
|
||||||
:path="`${path}.${index}`"
|
:path="`${path}.${index}`"
|
||||||
:issues="getIssues(index)"
|
:issues="getIssues(index)"
|
||||||
|
:class="$style.condition"
|
||||||
@update="(value) => onConditionUpdate(index, value)"
|
@update="(value) => onConditionUpdate(index, value)"
|
||||||
@remove="() => onConditionRemove(index)"
|
@remove="() => onConditionRemove(index)"
|
||||||
></Condition>
|
></Condition>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div :class="$style.addConditionWrapper">
|
<div v-if="!singleCondition" :class="$style.addConditionWrapper">
|
||||||
<n8n-button
|
<n8n-button
|
||||||
type="tertiary"
|
type="tertiary"
|
||||||
block
|
block
|
||||||
@@ -195,6 +202,20 @@ function getIssues(index: number): string[] {
|
|||||||
margin-left: var(--spacing-l);
|
margin-left: var(--spacing-l);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.condition {
|
||||||
|
padding-left: var(--spacing-l);
|
||||||
|
}
|
||||||
|
|
||||||
|
.single {
|
||||||
|
.condition {
|
||||||
|
padding-left: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.content {
|
||||||
|
margin-top: calc(var(--spacing-xs) * -1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
.addConditionWrapper {
|
.addConditionWrapper {
|
||||||
margin-top: var(--spacing-l);
|
margin-top: var(--spacing-l);
|
||||||
margin-left: var(--spacing-l);
|
margin-left: var(--spacing-l);
|
||||||
|
|||||||
@@ -167,6 +167,10 @@ export default defineComponent({
|
|||||||
size: 'small',
|
size: 'small',
|
||||||
}),
|
}),
|
||||||
},
|
},
|
||||||
|
entryIndex: {
|
||||||
|
type: Number,
|
||||||
|
default: undefined,
|
||||||
|
},
|
||||||
},
|
},
|
||||||
setup() {
|
setup() {
|
||||||
const eventBus = createEventBus();
|
const eventBus = createEventBus();
|
||||||
|
|||||||
@@ -217,6 +217,10 @@ export default defineComponent({
|
|||||||
type: Array as PropType<string[]>,
|
type: Array as PropType<string[]>,
|
||||||
default: () => [],
|
default: () => [],
|
||||||
},
|
},
|
||||||
|
entryIndex: {
|
||||||
|
type: Number,
|
||||||
|
default: undefined,
|
||||||
|
},
|
||||||
},
|
},
|
||||||
setup() {
|
setup() {
|
||||||
const nodeHelpers = useNodeHelpers();
|
const nodeHelpers = useNodeHelpers();
|
||||||
|
|||||||
@@ -157,7 +157,24 @@ describe('Filter.vue', () => {
|
|||||||
expect(getByTestId('parameter-issues')).toBeInTheDocument();
|
expect(getByTestId('parameter-issues')).toBeInTheDocument();
|
||||||
});
|
});
|
||||||
|
|
||||||
it('renders correctly with typeOptions.leftValue', async () => {
|
it('renders correctly with typeOptions.multipleValues = false (single mode)', async () => {
|
||||||
|
const { getByTestId, queryByTestId, findAllByTestId } = renderComponent({
|
||||||
|
props: {
|
||||||
|
...DEFAULT_SETUP.props,
|
||||||
|
parameter: {
|
||||||
|
...DEFAULT_SETUP.props.parameter,
|
||||||
|
typeOptions: {
|
||||||
|
multipleValues: false,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
expect((await findAllByTestId('filter-condition')).length).toEqual(1);
|
||||||
|
expect(getByTestId('filter-conditions')).toHaveClass('single');
|
||||||
|
expect(queryByTestId('filter-add-condition')).not.toBeInTheDocument();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('renders correctly with typeOptions.filter.leftValue', async () => {
|
||||||
const { findAllByTestId } = renderComponent({
|
const { findAllByTestId } = renderComponent({
|
||||||
props: {
|
props: {
|
||||||
...DEFAULT_SETUP.props,
|
...DEFAULT_SETUP.props,
|
||||||
@@ -173,7 +190,7 @@ describe('Filter.vue', () => {
|
|||||||
expect(conditions[0].querySelector('[data-test-id="filter-condition-left"]')).toBeNull();
|
expect(conditions[0].querySelector('[data-test-id="filter-condition-left"]')).toBeNull();
|
||||||
});
|
});
|
||||||
|
|
||||||
it('renders correctly with typeOptions.allowedCombinators', async () => {
|
it('renders correctly with typeOptions.filter.allowedCombinators', async () => {
|
||||||
const { getByTestId } = renderComponent({
|
const { getByTestId } = renderComponent({
|
||||||
props: {
|
props: {
|
||||||
...DEFAULT_SETUP.props,
|
...DEFAULT_SETUP.props,
|
||||||
@@ -202,7 +219,7 @@ describe('Filter.vue', () => {
|
|||||||
expect(getByTestId('filter-combinator-select')).toHaveTextContent('OR');
|
expect(getByTestId('filter-combinator-select')).toHaveTextContent('OR');
|
||||||
});
|
});
|
||||||
|
|
||||||
it('renders correctly with typeOptions.maxConditions', async () => {
|
it('renders correctly with typeOptions.filter.maxConditions', async () => {
|
||||||
const { getByTestId } = renderComponent({
|
const { getByTestId } = renderComponent({
|
||||||
props: {
|
props: {
|
||||||
...DEFAULT_SETUP.props,
|
...DEFAULT_SETUP.props,
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ import { VersionedNodeType } from 'n8n-workflow';
|
|||||||
|
|
||||||
import { SwitchV1 } from './V1/SwitchV1.node';
|
import { SwitchV1 } from './V1/SwitchV1.node';
|
||||||
import { SwitchV2 } from './V2/SwitchV2.node';
|
import { SwitchV2 } from './V2/SwitchV2.node';
|
||||||
|
import { SwitchV3 } from './V3/SwitchV3.node';
|
||||||
|
|
||||||
export class Switch extends VersionedNodeType {
|
export class Switch extends VersionedNodeType {
|
||||||
constructor() {
|
constructor() {
|
||||||
@@ -12,12 +13,13 @@ export class Switch extends VersionedNodeType {
|
|||||||
icon: 'fa:map-signs',
|
icon: 'fa:map-signs',
|
||||||
group: ['transform'],
|
group: ['transform'],
|
||||||
description: 'Route items depending on defined expression or rules',
|
description: 'Route items depending on defined expression or rules',
|
||||||
defaultVersion: 2,
|
defaultVersion: 3,
|
||||||
};
|
};
|
||||||
|
|
||||||
const nodeVersions: IVersionedNodeType['nodeVersions'] = {
|
const nodeVersions: IVersionedNodeType['nodeVersions'] = {
|
||||||
1: new SwitchV1(baseDescription),
|
1: new SwitchV1(baseDescription),
|
||||||
2: new SwitchV2(baseDescription),
|
2: new SwitchV2(baseDescription),
|
||||||
|
3: new SwitchV3(baseDescription),
|
||||||
};
|
};
|
||||||
|
|
||||||
super(nodeVersions, baseDescription);
|
super(nodeVersions, baseDescription);
|
||||||
|
|||||||
@@ -644,6 +644,8 @@ export class SwitchV2 implements INodeType {
|
|||||||
const rules = this.getNodeParameter('rules.rules', itemIndex, []) as INodeParameters[];
|
const rules = this.getNodeParameter('rules.rules', itemIndex, []) as INodeParameters[];
|
||||||
mode = this.getNodeParameter('mode', itemIndex) as string;
|
mode = this.getNodeParameter('mode', itemIndex) as string;
|
||||||
|
|
||||||
|
item.pairedItem = { item: itemIndex };
|
||||||
|
|
||||||
if (mode === 'expression') {
|
if (mode === 'expression') {
|
||||||
const outputsAmount = this.getNodeParameter('outputsAmount', itemIndex) as number;
|
const outputsAmount = this.getNodeParameter('outputsAmount', itemIndex) as number;
|
||||||
if (itemIndex === 0) {
|
if (itemIndex === 0) {
|
||||||
|
|||||||
390
packages/nodes-base/nodes/Switch/V3/SwitchV3.node.ts
Normal file
390
packages/nodes-base/nodes/Switch/V3/SwitchV3.node.ts
Normal file
@@ -0,0 +1,390 @@
|
|||||||
|
import type {
|
||||||
|
IDataObject,
|
||||||
|
IExecuteFunctions,
|
||||||
|
ILoadOptionsFunctions,
|
||||||
|
INodeExecutionData,
|
||||||
|
INodeParameters,
|
||||||
|
INodePropertyOptions,
|
||||||
|
INodeType,
|
||||||
|
INodeTypeBaseDescription,
|
||||||
|
INodeTypeDescription,
|
||||||
|
} from 'n8n-workflow';
|
||||||
|
import { NodeConnectionType, NodeOperationError } from 'n8n-workflow';
|
||||||
|
import { capitalize } from '@utils/utilities';
|
||||||
|
|
||||||
|
const configuredOutputs = (parameters: INodeParameters) => {
|
||||||
|
const mode = parameters.mode as string;
|
||||||
|
|
||||||
|
if (mode === 'expression') {
|
||||||
|
return Array.from({ length: parameters.numberOutputs as number }, (_, i) => ({
|
||||||
|
type: `${NodeConnectionType.Main}`,
|
||||||
|
displayName: i.toString(),
|
||||||
|
}));
|
||||||
|
} else {
|
||||||
|
const rules = ((parameters.rules as IDataObject)?.values as IDataObject[]) ?? [];
|
||||||
|
const ruleOutputs = rules.map((rule, index) => {
|
||||||
|
return {
|
||||||
|
type: `${NodeConnectionType.Main}`,
|
||||||
|
displayName: rule.outputKey || index.toString(),
|
||||||
|
};
|
||||||
|
});
|
||||||
|
if ((parameters.options as IDataObject)?.fallbackOutput === 'extra') {
|
||||||
|
const renameFallbackOutput = (parameters.options as IDataObject)?.renameFallbackOutput;
|
||||||
|
ruleOutputs.push({
|
||||||
|
type: `${NodeConnectionType.Main}`,
|
||||||
|
displayName: renameFallbackOutput || 'Fallback',
|
||||||
|
});
|
||||||
|
}
|
||||||
|
return ruleOutputs;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
export class SwitchV3 implements INodeType {
|
||||||
|
description: INodeTypeDescription;
|
||||||
|
|
||||||
|
constructor(baseDescription: INodeTypeBaseDescription) {
|
||||||
|
this.description = {
|
||||||
|
...baseDescription,
|
||||||
|
subtitle: `=mode: {{(${capitalize})($parameter["mode"])}}`,
|
||||||
|
version: [3],
|
||||||
|
defaults: {
|
||||||
|
name: 'Switch',
|
||||||
|
color: '#506000',
|
||||||
|
},
|
||||||
|
inputs: ['main'],
|
||||||
|
outputs: `={{(${configuredOutputs})($parameter)}}`,
|
||||||
|
properties: [
|
||||||
|
{
|
||||||
|
displayName: 'Mode',
|
||||||
|
name: 'mode',
|
||||||
|
type: 'options',
|
||||||
|
noDataExpression: true,
|
||||||
|
options: [
|
||||||
|
{
|
||||||
|
name: 'Rules',
|
||||||
|
value: 'rules',
|
||||||
|
description: 'Build a matching rule for each output',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'Expression',
|
||||||
|
value: 'expression',
|
||||||
|
description: 'Write an expression to return the output index',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
default: 'rules',
|
||||||
|
description: 'How data should be routed',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
displayName: 'Number of Outputs',
|
||||||
|
name: 'numberOutputs',
|
||||||
|
type: 'number',
|
||||||
|
displayOptions: {
|
||||||
|
show: {
|
||||||
|
mode: ['expression'],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
default: 4,
|
||||||
|
description: 'How many outputs to create',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
displayName: 'Output Index',
|
||||||
|
name: 'output',
|
||||||
|
type: 'number',
|
||||||
|
validateType: 'number',
|
||||||
|
hint: 'The index to route the item to, starts at 0',
|
||||||
|
displayOptions: {
|
||||||
|
show: {
|
||||||
|
mode: ['expression'],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
// eslint-disable-next-line n8n-nodes-base/node-param-default-wrong-for-number
|
||||||
|
default: '={{}}',
|
||||||
|
description:
|
||||||
|
"The output's index to which send an input item, use expressions to calculate what input item should be routed to which output, expression must return a number",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
displayName: 'Routing Rules',
|
||||||
|
name: 'rules',
|
||||||
|
placeholder: 'Add Routing Rule',
|
||||||
|
type: 'fixedCollection',
|
||||||
|
typeOptions: {
|
||||||
|
multipleValues: true,
|
||||||
|
sortable: true,
|
||||||
|
},
|
||||||
|
default: {
|
||||||
|
values: [
|
||||||
|
{
|
||||||
|
conditions: {
|
||||||
|
options: {
|
||||||
|
caseSensitive: true,
|
||||||
|
leftValue: '',
|
||||||
|
typeValidation: 'strict',
|
||||||
|
},
|
||||||
|
conditions: [
|
||||||
|
{
|
||||||
|
leftValue: '',
|
||||||
|
rightValue: '',
|
||||||
|
operator: {
|
||||||
|
type: 'string',
|
||||||
|
operation: 'equals',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
combinator: 'and',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
displayOptions: {
|
||||||
|
show: {
|
||||||
|
mode: ['rules'],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
options: [
|
||||||
|
{
|
||||||
|
name: 'values',
|
||||||
|
displayName: 'Values',
|
||||||
|
values: [
|
||||||
|
{
|
||||||
|
displayName: 'Conditions',
|
||||||
|
name: 'conditions',
|
||||||
|
placeholder: 'Add Condition',
|
||||||
|
type: 'filter',
|
||||||
|
default: {},
|
||||||
|
typeOptions: {
|
||||||
|
multipleValues: false,
|
||||||
|
filter: {
|
||||||
|
caseSensitive: '={{!$parameter.options.ignoreCase}}',
|
||||||
|
typeValidation:
|
||||||
|
'={{$parameter.options.looseTypeValidation ? "loose" : "strict"}}',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
displayName: 'Rename Output',
|
||||||
|
name: 'renameOutput',
|
||||||
|
type: 'boolean',
|
||||||
|
default: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
displayName: 'Output Name',
|
||||||
|
name: 'outputKey',
|
||||||
|
type: 'string',
|
||||||
|
default: '',
|
||||||
|
description: 'The label of output to which to send data to if rule matches',
|
||||||
|
displayOptions: {
|
||||||
|
show: {
|
||||||
|
renameOutput: [true],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
{
|
||||||
|
displayName: 'Options',
|
||||||
|
name: 'options',
|
||||||
|
type: 'collection',
|
||||||
|
placeholder: 'Add Option',
|
||||||
|
default: {},
|
||||||
|
displayOptions: {
|
||||||
|
show: {
|
||||||
|
mode: ['rules'],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
options: [
|
||||||
|
{
|
||||||
|
// eslint-disable-next-line n8n-nodes-base/node-param-display-name-wrong-for-dynamic-options
|
||||||
|
displayName: 'Fallback Output',
|
||||||
|
name: 'fallbackOutput',
|
||||||
|
type: 'options',
|
||||||
|
typeOptions: {
|
||||||
|
loadOptionsDependsOn: ['rules.values', '/rules', '/rules.values'],
|
||||||
|
loadOptionsMethod: 'getFallbackOutputOptions',
|
||||||
|
},
|
||||||
|
default: 'none',
|
||||||
|
// eslint-disable-next-line n8n-nodes-base/node-param-description-wrong-for-dynamic-options
|
||||||
|
description:
|
||||||
|
'If no rule matches the item will be sent to this output, by default they will be ignored',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
displayName: 'Ignore Case',
|
||||||
|
description: 'Whether to ignore letter case when evaluating conditions',
|
||||||
|
name: 'ignoreCase',
|
||||||
|
type: 'boolean',
|
||||||
|
default: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
displayName: 'Less Strict Type Validation',
|
||||||
|
description: 'Whether to try casting value types based on the selected operator',
|
||||||
|
name: 'looseTypeValidation',
|
||||||
|
type: 'boolean',
|
||||||
|
default: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
displayName: 'Rename Fallback Output',
|
||||||
|
name: 'renameFallbackOutput',
|
||||||
|
type: 'string',
|
||||||
|
placeholder: 'e.g. Fallback',
|
||||||
|
default: '',
|
||||||
|
displayOptions: {
|
||||||
|
show: {
|
||||||
|
fallbackOutput: ['extra'],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
// eslint-disable-next-line n8n-nodes-base/node-param-display-name-miscased
|
||||||
|
displayName: 'Send data to all matching outputs',
|
||||||
|
name: 'allMatchingOutputs',
|
||||||
|
type: 'boolean',
|
||||||
|
default: false,
|
||||||
|
description:
|
||||||
|
'Whether to send data to all outputs meeting conditions (and not just the first one)',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
methods = {
|
||||||
|
loadOptions: {
|
||||||
|
async getFallbackOutputOptions(this: ILoadOptionsFunctions): Promise<INodePropertyOptions[]> {
|
||||||
|
const rules = (this.getCurrentNodeParameter('rules.values') as INodeParameters[]) ?? [];
|
||||||
|
|
||||||
|
const outputOptions: INodePropertyOptions[] = [
|
||||||
|
{
|
||||||
|
// eslint-disable-next-line n8n-nodes-base/node-param-display-name-miscased
|
||||||
|
name: 'None (default)',
|
||||||
|
value: 'none',
|
||||||
|
description: 'Items will be ignored',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'Extra Output',
|
||||||
|
value: 'extra',
|
||||||
|
description: 'Items will be sent to the extra, separate, output',
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
for (const [index, rule] of rules.entries()) {
|
||||||
|
outputOptions.push({
|
||||||
|
name: `Output ${rule.outputKey || index}`,
|
||||||
|
value: index,
|
||||||
|
description: `Items will be sent to the same output as when matched rule ${index + 1}`,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return outputOptions;
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
async execute(this: IExecuteFunctions): Promise<INodeExecutionData[][]> {
|
||||||
|
let returnData: INodeExecutionData[][] = [];
|
||||||
|
|
||||||
|
const items = this.getInputData();
|
||||||
|
const mode = this.getNodeParameter('mode', 0) as string;
|
||||||
|
|
||||||
|
const checkIndexRange = (returnDataLength: number, index: number, itemIndex = 0) => {
|
||||||
|
if (Number(index) === returnDataLength) {
|
||||||
|
throw new NodeOperationError(this.getNode(), `The ouput ${index} is not allowed. `, {
|
||||||
|
itemIndex,
|
||||||
|
description: `Output indexes are zero based, if you want to use the extra output use ${
|
||||||
|
index - 1
|
||||||
|
}`,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
if (index < 0 || index > returnDataLength) {
|
||||||
|
throw new NodeOperationError(this.getNode(), `The ouput ${index} is not allowed`, {
|
||||||
|
itemIndex,
|
||||||
|
description: `It has to be between 0 and ${returnDataLength - 1}`,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
itemLoop: for (let itemIndex = 0; itemIndex < items.length; itemIndex++) {
|
||||||
|
try {
|
||||||
|
const item = items[itemIndex];
|
||||||
|
|
||||||
|
item.pairedItem = { item: itemIndex };
|
||||||
|
|
||||||
|
if (mode === 'expression') {
|
||||||
|
const numberOutputs = this.getNodeParameter('numberOutputs', itemIndex) as number;
|
||||||
|
if (itemIndex === 0) {
|
||||||
|
returnData = new Array(numberOutputs).fill(0).map(() => []);
|
||||||
|
}
|
||||||
|
const outputIndex = this.getNodeParameter('output', itemIndex) as number;
|
||||||
|
checkIndexRange(returnData.length, outputIndex, itemIndex);
|
||||||
|
|
||||||
|
returnData[outputIndex].push(item);
|
||||||
|
} else if (mode === 'rules') {
|
||||||
|
const rules = this.getNodeParameter('rules.values', itemIndex, []) as INodeParameters[];
|
||||||
|
if (!rules.length) continue;
|
||||||
|
const options = this.getNodeParameter('options', itemIndex, {});
|
||||||
|
const fallbackOutput = options.fallbackOutput;
|
||||||
|
|
||||||
|
if (itemIndex === 0) {
|
||||||
|
returnData = new Array(rules.length).fill(0).map(() => []);
|
||||||
|
|
||||||
|
if (fallbackOutput === 'extra') {
|
||||||
|
returnData.push([]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let matchFound = false;
|
||||||
|
for (const [ruleIndex, rule] of rules.entries()) {
|
||||||
|
let conditionPass;
|
||||||
|
|
||||||
|
try {
|
||||||
|
conditionPass = this.getNodeParameter(
|
||||||
|
`rules.values[${ruleIndex}].conditions`,
|
||||||
|
itemIndex,
|
||||||
|
false,
|
||||||
|
{
|
||||||
|
extractValue: true,
|
||||||
|
},
|
||||||
|
) as boolean;
|
||||||
|
} catch (error) {
|
||||||
|
if (!options.looseTypeValidation) {
|
||||||
|
error.description =
|
||||||
|
"Try to change the operator, switch ON the option 'Less Strict Type Validation', or change the type with an expression";
|
||||||
|
}
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (conditionPass) {
|
||||||
|
matchFound = true;
|
||||||
|
checkIndexRange(returnData.length, rule.output as number, itemIndex);
|
||||||
|
returnData[ruleIndex].push(item);
|
||||||
|
|
||||||
|
if (!options.allMatchingOutputs) {
|
||||||
|
continue itemLoop;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (fallbackOutput !== undefined && fallbackOutput !== 'none' && !matchFound) {
|
||||||
|
if (fallbackOutput === 'extra') {
|
||||||
|
returnData[returnData.length - 1].push(item);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
checkIndexRange(returnData.length, fallbackOutput as number, itemIndex);
|
||||||
|
returnData[fallbackOutput as number].push(item);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
if (this.continueOnFail()) {
|
||||||
|
returnData[0].push({ json: { error: error.message } });
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
throw new NodeOperationError(this.getNode(), error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!returnData.length) return [[]];
|
||||||
|
|
||||||
|
return returnData;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,168 @@
|
|||||||
|
{
|
||||||
|
"name": "My workflow 64",
|
||||||
|
"nodes": [
|
||||||
|
{
|
||||||
|
"parameters": {},
|
||||||
|
"id": "58cc2d21-a8b1-424d-a8e4-e79d39955fa8",
|
||||||
|
"name": "When clicking \"Execute Workflow\"",
|
||||||
|
"type": "n8n-nodes-base.manualTrigger",
|
||||||
|
"typeVersion": 1,
|
||||||
|
"position": [
|
||||||
|
0,
|
||||||
|
620
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"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": "85adf7fc-2d33-49aa-b4bb-2000cce07ce0",
|
||||||
|
"name": "Code",
|
||||||
|
"type": "n8n-nodes-base.code",
|
||||||
|
"typeVersion": 2,
|
||||||
|
"position": [
|
||||||
|
220,
|
||||||
|
620
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"parameters": {},
|
||||||
|
"id": "ebab0b65-6feb-416c-828e-ca5e766ea048",
|
||||||
|
"name": "No Operation, do nothing",
|
||||||
|
"type": "n8n-nodes-base.noOp",
|
||||||
|
"typeVersion": 1,
|
||||||
|
"position": [
|
||||||
|
760,
|
||||||
|
460
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"parameters": {},
|
||||||
|
"id": "2f58a703-b7ae-4279-a60b-a0243af3b563",
|
||||||
|
"name": "No Operation, do nothing1",
|
||||||
|
"type": "n8n-nodes-base.noOp",
|
||||||
|
"typeVersion": 1,
|
||||||
|
"position": [
|
||||||
|
760,
|
||||||
|
620
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"parameters": {},
|
||||||
|
"id": "b1d0b310-6edf-4a40-a66d-689078ddbf31",
|
||||||
|
"name": "No Operation, do nothing2",
|
||||||
|
"type": "n8n-nodes-base.noOp",
|
||||||
|
"typeVersion": 1,
|
||||||
|
"position": [
|
||||||
|
760,
|
||||||
|
780
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"parameters": {
|
||||||
|
"mode": "expression",
|
||||||
|
"numberOutputs": 3,
|
||||||
|
"output": "={{ Math.max(0, ['first', 'second', 'third'].indexOf( $json.output)) }}"
|
||||||
|
},
|
||||||
|
"id": "437e2c46-81d8-4c76-a036-db767576f55d",
|
||||||
|
"name": "Switch",
|
||||||
|
"type": "n8n-nodes-base.switch",
|
||||||
|
"typeVersion": 3,
|
||||||
|
"position": [
|
||||||
|
460,
|
||||||
|
620
|
||||||
|
]
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"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": "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": "c72f2f9b-5089-42c2-8939-b70d9467a718",
|
||||||
|
"id": "1vDZkJN9SpYXu0Ic",
|
||||||
|
"meta": {
|
||||||
|
"instanceId": "b888bd11cd1ddbb95450babf3e199556799d999b896f650de768b8370ee50363"
|
||||||
|
},
|
||||||
|
"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,242 @@
|
|||||||
|
{
|
||||||
|
"name": "My workflow 64",
|
||||||
|
"nodes": [
|
||||||
|
{
|
||||||
|
"parameters": {},
|
||||||
|
"id": "2c6504d1-9412-4da5-bd51-e5f9d6a84721",
|
||||||
|
"name": "When clicking \"Execute Workflow\"",
|
||||||
|
"type": "n8n-nodes-base.manualTrigger",
|
||||||
|
"typeVersion": 1,
|
||||||
|
"position": [
|
||||||
|
-560,
|
||||||
|
620
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"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": "73d838de-89f2-476e-9dc8-a4dc59e4bdfb",
|
||||||
|
"name": "Code",
|
||||||
|
"type": "n8n-nodes-base.code",
|
||||||
|
"typeVersion": 2,
|
||||||
|
"position": [
|
||||||
|
-340,
|
||||||
|
620
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"parameters": {},
|
||||||
|
"id": "d19a786e-e9fc-4480-af8a-c1abc9d9ef9a",
|
||||||
|
"name": "No Operation, do nothing",
|
||||||
|
"type": "n8n-nodes-base.noOp",
|
||||||
|
"typeVersion": 1,
|
||||||
|
"position": [
|
||||||
|
200,
|
||||||
|
460
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"parameters": {},
|
||||||
|
"id": "82d5a7b9-6a29-4df8-823a-bd926f2000a9",
|
||||||
|
"name": "No Operation, do nothing1",
|
||||||
|
"type": "n8n-nodes-base.noOp",
|
||||||
|
"typeVersion": 1,
|
||||||
|
"position": [
|
||||||
|
200,
|
||||||
|
620
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"parameters": {},
|
||||||
|
"id": "40276b8e-eaad-49bd-9271-cd82c6688f24",
|
||||||
|
"name": "No Operation, do nothing2",
|
||||||
|
"type": "n8n-nodes-base.noOp",
|
||||||
|
"typeVersion": 1,
|
||||||
|
"position": [
|
||||||
|
200,
|
||||||
|
780
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"parameters": {
|
||||||
|
"rules": {
|
||||||
|
"values": [
|
||||||
|
{
|
||||||
|
"conditions": {
|
||||||
|
"options": {
|
||||||
|
"caseSensitive": true,
|
||||||
|
"leftValue": "",
|
||||||
|
"typeValidation": "strict"
|
||||||
|
},
|
||||||
|
"conditions": [
|
||||||
|
{
|
||||||
|
"leftValue": "={{ $json.output }}",
|
||||||
|
"rightValue": "first",
|
||||||
|
"operator": {
|
||||||
|
"type": "string",
|
||||||
|
"operation": "equals"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"combinator": "and"
|
||||||
|
},
|
||||||
|
"renameOutput": true,
|
||||||
|
"outputKey": "First Output"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"conditions": {
|
||||||
|
"options": {
|
||||||
|
"caseSensitive": true,
|
||||||
|
"leftValue": "",
|
||||||
|
"typeValidation": "strict"
|
||||||
|
},
|
||||||
|
"conditions": [
|
||||||
|
{
|
||||||
|
"id": "199d4f8c-1c92-48cd-99ad-e1da186ed54f",
|
||||||
|
"leftValue": "={{ $json.output }}",
|
||||||
|
"rightValue": "second",
|
||||||
|
"operator": {
|
||||||
|
"type": "string",
|
||||||
|
"operation": "equals",
|
||||||
|
"name": "filter.operator.equals"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"combinator": "and"
|
||||||
|
},
|
||||||
|
"renameOutput": true,
|
||||||
|
"outputKey": "Second Output"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"conditions": {
|
||||||
|
"options": {
|
||||||
|
"caseSensitive": true,
|
||||||
|
"leftValue": "",
|
||||||
|
"typeValidation": "strict"
|
||||||
|
},
|
||||||
|
"conditions": [
|
||||||
|
{
|
||||||
|
"id": "e894662a-7496-4df4-a084-7200c3192485",
|
||||||
|
"leftValue": "={{ $json.output }}",
|
||||||
|
"rightValue": "third",
|
||||||
|
"operator": {
|
||||||
|
"type": "string",
|
||||||
|
"operation": "equals",
|
||||||
|
"name": "filter.operator.equals"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"combinator": "and"
|
||||||
|
},
|
||||||
|
"renameOutput": true,
|
||||||
|
"outputKey": "Third Output"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"options": {
|
||||||
|
"fallbackOutput": 2
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"id": "0c34bae5-89c8-4adb-bb38-092d5a0e349e",
|
||||||
|
"name": "Switch1",
|
||||||
|
"type": "n8n-nodes-base.switch",
|
||||||
|
"typeVersion": 3,
|
||||||
|
"position": [
|
||||||
|
-120,
|
||||||
|
620
|
||||||
|
]
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"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": "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": "c9c4d7fd-b704-4664-8fde-d1c2414e68f0",
|
||||||
|
"id": "1vDZkJN9SpYXu0Ic",
|
||||||
|
"meta": {
|
||||||
|
"instanceId": "b888bd11cd1ddbb95450babf3e199556799d999b896f650de768b8370ee50363"
|
||||||
|
},
|
||||||
|
"tags": []
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user