mirror of
https://github.com/n8n-io/n8n.git
synced 2024-12-24 04:04:06 -08:00
feat(Filter Node): Overhaul UI by adding the new filter component (#8016)
## Summary Adds a new version of the Filter node (v2) that uses the filter component (like in the new If node). <img width="1612" alt="image" src="https://github.com/n8n-io/n8n/assets/8850410/bca38e47-305f-4a9e-9c5c-ec550b9f7d4a"> Test by adding a new Filter node to the canvas and trying different operators/options. Example workflow can be found in `packages/nodes-base/nodes/Filter/test/workflow_v2.json` ## Related tickets and issues https://linear.app/n8n/issue/NODE-982 ## Review / Merge checklist - [x] PR title and summary are descriptive. **Remember, the title automatically goes into the changelog. Use `(no-changelog)` otherwise.** ([conventions](https://github.com/n8n-io/n8n/blob/master/.github/pull_request_title_conventions.md)) - [x] [Docs updated](https://github.com/n8n-io/n8n-docs) or follow-up ticket created. - [x] Tests included. > A bug is not considered fixed, unless a test is added to prevent it from happening again. > A feature is not complete without tests. Co-authored-by: Michael Kret <michael.k@radency.com>
This commit is contained in:
parent
f18bc5f4b7
commit
3d530522f8
|
@ -1,372 +1,24 @@
|
|||
import type {
|
||||
IExecuteFunctions,
|
||||
INodeExecutionData,
|
||||
INodeParameters,
|
||||
INodeType,
|
||||
INodeTypeDescription,
|
||||
NodeParameterValue,
|
||||
} from 'n8n-workflow';
|
||||
import type { INodeTypeBaseDescription, IVersionedNodeType } from 'n8n-workflow';
|
||||
import { VersionedNodeType } from 'n8n-workflow';
|
||||
|
||||
import { compareOperationFunctions, convertDateTime } from './GenericFunctions';
|
||||
import { FilterV1 } from './V1/FilterV1.node';
|
||||
import { FilterV2 } from './V2/FilterV2.node';
|
||||
|
||||
export class Filter implements INodeType {
|
||||
description: INodeTypeDescription = {
|
||||
displayName: 'Filter',
|
||||
name: 'filter',
|
||||
icon: 'fa:filter',
|
||||
group: ['transform'],
|
||||
version: 1,
|
||||
description: 'Remove items matching a condition',
|
||||
defaults: {
|
||||
name: 'Filter',
|
||||
color: '#229eff',
|
||||
},
|
||||
inputs: ['main'],
|
||||
outputs: ['main'],
|
||||
outputNames: ['Kept', 'Discarded'],
|
||||
properties: [
|
||||
{
|
||||
displayName: 'Conditions',
|
||||
name: 'conditions',
|
||||
placeholder: 'Add Condition',
|
||||
type: 'fixedCollection',
|
||||
typeOptions: {
|
||||
multipleValues: true,
|
||||
sortable: true,
|
||||
},
|
||||
description: 'The type of values to compare',
|
||||
default: {},
|
||||
options: [
|
||||
{
|
||||
name: 'boolean',
|
||||
displayName: 'Boolean',
|
||||
values: [
|
||||
{
|
||||
displayName: 'Value 1',
|
||||
name: 'value1',
|
||||
type: 'boolean',
|
||||
default: false,
|
||||
// eslint-disable-next-line n8n-nodes-base/node-param-description-boolean-without-whether
|
||||
description: 'The value to compare with the second one',
|
||||
},
|
||||
// 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',
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
name: 'dateTime',
|
||||
displayName: 'Date & Time',
|
||||
values: [
|
||||
{
|
||||
displayName: 'Value 1',
|
||||
name: 'value1',
|
||||
type: 'dateTime',
|
||||
default: '',
|
||||
description: 'The value to compare with the second one',
|
||||
},
|
||||
// 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: '',
|
||||
description: 'The value to compare with the first one',
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
name: 'number',
|
||||
displayName: 'Number',
|
||||
values: [
|
||||
{
|
||||
displayName: 'Value 1',
|
||||
name: 'value1',
|
||||
type: 'number',
|
||||
default: 0,
|
||||
description: 'The value to compare with the second one',
|
||||
},
|
||||
{
|
||||
displayName: 'Operation',
|
||||
name: 'operation',
|
||||
type: 'options',
|
||||
noDataExpression: true,
|
||||
// eslint-disable-next-line n8n-nodes-base/node-param-options-type-unsorted-items
|
||||
options: [
|
||||
{
|
||||
name: 'Smaller',
|
||||
value: 'smaller',
|
||||
},
|
||||
{
|
||||
name: 'Smaller or Equal',
|
||||
value: 'smallerEqual',
|
||||
},
|
||||
{
|
||||
name: 'Equal',
|
||||
value: 'equal',
|
||||
},
|
||||
{
|
||||
name: 'Not Equal',
|
||||
value: 'notEqual',
|
||||
},
|
||||
{
|
||||
name: 'Larger',
|
||||
value: 'larger',
|
||||
},
|
||||
{
|
||||
name: 'Larger or Equal',
|
||||
value: 'largerEqual',
|
||||
},
|
||||
{
|
||||
name: 'Is Empty',
|
||||
value: 'isEmpty',
|
||||
},
|
||||
{
|
||||
name: 'Is Not Empty',
|
||||
value: 'isNotEmpty',
|
||||
},
|
||||
],
|
||||
default: 'smaller',
|
||||
description: 'Operation to decide where the the data should be mapped to',
|
||||
},
|
||||
{
|
||||
displayName: 'Value 2',
|
||||
name: 'value2',
|
||||
type: 'number',
|
||||
displayOptions: {
|
||||
hide: {
|
||||
operation: ['isEmpty', 'isNotEmpty'],
|
||||
},
|
||||
},
|
||||
default: 0,
|
||||
description: 'The value to compare with the first one',
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
name: 'string',
|
||||
displayName: 'String',
|
||||
values: [
|
||||
{
|
||||
displayName: 'Value 1',
|
||||
name: 'value1',
|
||||
type: 'string',
|
||||
default: '',
|
||||
description: 'The value to compare with the second one',
|
||||
},
|
||||
{
|
||||
displayName: 'Operation',
|
||||
name: 'operation',
|
||||
type: 'options',
|
||||
noDataExpression: true,
|
||||
// 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',
|
||||
},
|
||||
{
|
||||
name: 'Is Empty',
|
||||
value: 'isEmpty',
|
||||
},
|
||||
{
|
||||
name: 'Is Not Empty',
|
||||
value: 'isNotEmpty',
|
||||
},
|
||||
],
|
||||
default: 'equal',
|
||||
description: 'Operation to decide where the the data should be mapped to',
|
||||
},
|
||||
{
|
||||
displayName: 'Value 2',
|
||||
name: 'value2',
|
||||
type: 'string',
|
||||
displayOptions: {
|
||||
hide: {
|
||||
operation: ['isEmpty', 'isNotEmpty', '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: 'Combine Conditions',
|
||||
name: 'combineConditions',
|
||||
type: 'options',
|
||||
options: [
|
||||
{
|
||||
name: 'AND',
|
||||
description: 'Items are passed to the next node only if they meet all the conditions',
|
||||
value: 'AND',
|
||||
},
|
||||
{
|
||||
name: 'OR',
|
||||
description: 'Items are passed to the next node if they meet at least one condition',
|
||||
value: 'OR',
|
||||
},
|
||||
],
|
||||
default: 'AND',
|
||||
description:
|
||||
'How to combine the conditions: AND requires all conditions to be true, OR requires at least one condition to be true',
|
||||
},
|
||||
],
|
||||
};
|
||||
export class Filter extends VersionedNodeType {
|
||||
constructor() {
|
||||
const baseDescription: INodeTypeBaseDescription = {
|
||||
displayName: 'Filter',
|
||||
name: 'filter',
|
||||
icon: 'fa:filter',
|
||||
group: ['transform'],
|
||||
description: 'Remove items matching a condition',
|
||||
};
|
||||
|
||||
async execute(this: IExecuteFunctions): Promise<INodeExecutionData[][]> {
|
||||
const returnDataTrue: INodeExecutionData[] = [];
|
||||
const returnDataFalse: INodeExecutionData[] = [];
|
||||
const nodeVersions: IVersionedNodeType['nodeVersions'] = {
|
||||
1: new FilterV1(baseDescription),
|
||||
2: new FilterV2(baseDescription),
|
||||
};
|
||||
|
||||
const items = this.getInputData();
|
||||
|
||||
const dataTypes = ['boolean', 'dateTime', 'number', 'string'];
|
||||
|
||||
itemLoop: for (let itemIndex = 0; itemIndex < items.length; itemIndex++) {
|
||||
const item = items[itemIndex];
|
||||
|
||||
const combineConditions = this.getNodeParameter('combineConditions', itemIndex) as string;
|
||||
|
||||
for (const dataType of dataTypes) {
|
||||
const typeConditions = this.getNodeParameter(
|
||||
`conditions.${dataType}`,
|
||||
itemIndex,
|
||||
[],
|
||||
) as INodeParameters[];
|
||||
|
||||
for (const condition of typeConditions) {
|
||||
let value1 = condition.value1 as NodeParameterValue;
|
||||
let value2 = condition.value2 as NodeParameterValue;
|
||||
|
||||
if (dataType === 'dateTime') {
|
||||
const node = this.getNode();
|
||||
value1 = convertDateTime(node, value1);
|
||||
value2 = convertDateTime(node, value2);
|
||||
}
|
||||
|
||||
const compareResult = compareOperationFunctions[condition.operation as string](
|
||||
value1,
|
||||
value2,
|
||||
);
|
||||
|
||||
if (item.pairedItem === undefined) {
|
||||
item.pairedItem = [{ item: itemIndex }];
|
||||
}
|
||||
|
||||
// If the operation is "OR" it means the item did match one condition no ned to check further
|
||||
if (compareResult && combineConditions === 'OR') {
|
||||
returnDataTrue.push(item);
|
||||
continue itemLoop;
|
||||
}
|
||||
|
||||
// If the operation is "AND" it means the item failed one condition no ned to check further
|
||||
if (!compareResult && combineConditions === 'AND') {
|
||||
returnDataFalse.push(item);
|
||||
continue itemLoop;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// If the operation is "AND" it means the item did match all conditions
|
||||
if (combineConditions === 'AND') {
|
||||
returnDataTrue.push(item);
|
||||
} else {
|
||||
// If the operation is "OR" it means the the item did not match any condition.
|
||||
returnDataFalse.push(item);
|
||||
}
|
||||
}
|
||||
|
||||
return [returnDataTrue, returnDataFalse];
|
||||
super(nodeVersions, baseDescription);
|
||||
}
|
||||
}
|
||||
|
|
373
packages/nodes-base/nodes/Filter/V1/FilterV1.node.ts
Normal file
373
packages/nodes-base/nodes/Filter/V1/FilterV1.node.ts
Normal file
|
@ -0,0 +1,373 @@
|
|||
import type {
|
||||
IExecuteFunctions,
|
||||
INodeExecutionData,
|
||||
INodeParameters,
|
||||
INodeType,
|
||||
INodeTypeBaseDescription,
|
||||
INodeTypeDescription,
|
||||
NodeParameterValue,
|
||||
} from 'n8n-workflow';
|
||||
|
||||
import { compareOperationFunctions, convertDateTime } from './GenericFunctions';
|
||||
|
||||
export class FilterV1 implements INodeType {
|
||||
description: INodeTypeDescription;
|
||||
|
||||
constructor(baseDescription: INodeTypeBaseDescription) {
|
||||
this.description = {
|
||||
...baseDescription,
|
||||
version: 1,
|
||||
defaults: {
|
||||
name: 'Filter',
|
||||
color: '#229eff',
|
||||
},
|
||||
inputs: ['main'],
|
||||
outputs: ['main'],
|
||||
outputNames: ['Kept', 'Discarded'],
|
||||
properties: [
|
||||
{
|
||||
displayName: 'Conditions',
|
||||
name: 'conditions',
|
||||
placeholder: 'Add Condition',
|
||||
type: 'fixedCollection',
|
||||
typeOptions: {
|
||||
multipleValues: true,
|
||||
sortable: true,
|
||||
},
|
||||
description: 'The type of values to compare',
|
||||
default: {},
|
||||
options: [
|
||||
{
|
||||
name: 'boolean',
|
||||
displayName: 'Boolean',
|
||||
values: [
|
||||
{
|
||||
displayName: 'Value 1',
|
||||
name: 'value1',
|
||||
type: 'boolean',
|
||||
default: false,
|
||||
// eslint-disable-next-line n8n-nodes-base/node-param-description-boolean-without-whether
|
||||
description: 'The value to compare with the second one',
|
||||
},
|
||||
// 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',
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
name: 'dateTime',
|
||||
displayName: 'Date & Time',
|
||||
values: [
|
||||
{
|
||||
displayName: 'Value 1',
|
||||
name: 'value1',
|
||||
type: 'dateTime',
|
||||
default: '',
|
||||
description: 'The value to compare with the second one',
|
||||
},
|
||||
// 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: '',
|
||||
description: 'The value to compare with the first one',
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
name: 'number',
|
||||
displayName: 'Number',
|
||||
values: [
|
||||
{
|
||||
displayName: 'Value 1',
|
||||
name: 'value1',
|
||||
type: 'number',
|
||||
default: 0,
|
||||
description: 'The value to compare with the second one',
|
||||
},
|
||||
{
|
||||
displayName: 'Operation',
|
||||
name: 'operation',
|
||||
type: 'options',
|
||||
noDataExpression: true,
|
||||
// eslint-disable-next-line n8n-nodes-base/node-param-options-type-unsorted-items
|
||||
options: [
|
||||
{
|
||||
name: 'Smaller',
|
||||
value: 'smaller',
|
||||
},
|
||||
{
|
||||
name: 'Smaller or Equal',
|
||||
value: 'smallerEqual',
|
||||
},
|
||||
{
|
||||
name: 'Equal',
|
||||
value: 'equal',
|
||||
},
|
||||
{
|
||||
name: 'Not Equal',
|
||||
value: 'notEqual',
|
||||
},
|
||||
{
|
||||
name: 'Larger',
|
||||
value: 'larger',
|
||||
},
|
||||
{
|
||||
name: 'Larger or Equal',
|
||||
value: 'largerEqual',
|
||||
},
|
||||
{
|
||||
name: 'Is Empty',
|
||||
value: 'isEmpty',
|
||||
},
|
||||
{
|
||||
name: 'Is Not Empty',
|
||||
value: 'isNotEmpty',
|
||||
},
|
||||
],
|
||||
default: 'smaller',
|
||||
description: 'Operation to decide where the the data should be mapped to',
|
||||
},
|
||||
{
|
||||
displayName: 'Value 2',
|
||||
name: 'value2',
|
||||
type: 'number',
|
||||
displayOptions: {
|
||||
hide: {
|
||||
operation: ['isEmpty', 'isNotEmpty'],
|
||||
},
|
||||
},
|
||||
default: 0,
|
||||
description: 'The value to compare with the first one',
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
name: 'string',
|
||||
displayName: 'String',
|
||||
values: [
|
||||
{
|
||||
displayName: 'Value 1',
|
||||
name: 'value1',
|
||||
type: 'string',
|
||||
default: '',
|
||||
description: 'The value to compare with the second one',
|
||||
},
|
||||
{
|
||||
displayName: 'Operation',
|
||||
name: 'operation',
|
||||
type: 'options',
|
||||
noDataExpression: true,
|
||||
// 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',
|
||||
},
|
||||
{
|
||||
name: 'Is Empty',
|
||||
value: 'isEmpty',
|
||||
},
|
||||
{
|
||||
name: 'Is Not Empty',
|
||||
value: 'isNotEmpty',
|
||||
},
|
||||
],
|
||||
default: 'equal',
|
||||
description: 'Operation to decide where the the data should be mapped to',
|
||||
},
|
||||
{
|
||||
displayName: 'Value 2',
|
||||
name: 'value2',
|
||||
type: 'string',
|
||||
displayOptions: {
|
||||
hide: {
|
||||
operation: ['isEmpty', 'isNotEmpty', '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: 'Combine Conditions',
|
||||
name: 'combineConditions',
|
||||
type: 'options',
|
||||
options: [
|
||||
{
|
||||
name: 'AND',
|
||||
description: 'Items are passed to the next node only if they meet all the conditions',
|
||||
value: 'AND',
|
||||
},
|
||||
{
|
||||
name: 'OR',
|
||||
description: 'Items are passed to the next node if they meet at least one condition',
|
||||
value: 'OR',
|
||||
},
|
||||
],
|
||||
default: 'AND',
|
||||
description:
|
||||
'How to combine the conditions: AND requires all conditions to be true, OR requires at least one condition to be true',
|
||||
},
|
||||
],
|
||||
};
|
||||
}
|
||||
|
||||
async execute(this: IExecuteFunctions): Promise<INodeExecutionData[][]> {
|
||||
const returnDataTrue: INodeExecutionData[] = [];
|
||||
const returnDataFalse: INodeExecutionData[] = [];
|
||||
|
||||
const items = this.getInputData();
|
||||
|
||||
const dataTypes = ['boolean', 'dateTime', 'number', 'string'];
|
||||
|
||||
itemLoop: for (let itemIndex = 0; itemIndex < items.length; itemIndex++) {
|
||||
const item = items[itemIndex];
|
||||
|
||||
const combineConditions = this.getNodeParameter('combineConditions', itemIndex) as string;
|
||||
|
||||
for (const dataType of dataTypes) {
|
||||
const typeConditions = this.getNodeParameter(
|
||||
`conditions.${dataType}`,
|
||||
itemIndex,
|
||||
[],
|
||||
) as INodeParameters[];
|
||||
|
||||
for (const condition of typeConditions) {
|
||||
let value1 = condition.value1 as NodeParameterValue;
|
||||
let value2 = condition.value2 as NodeParameterValue;
|
||||
|
||||
if (dataType === 'dateTime') {
|
||||
const node = this.getNode();
|
||||
value1 = convertDateTime(node, value1);
|
||||
value2 = convertDateTime(node, value2);
|
||||
}
|
||||
|
||||
const compareResult = compareOperationFunctions[condition.operation as string](
|
||||
value1,
|
||||
value2,
|
||||
);
|
||||
|
||||
if (item.pairedItem === undefined) {
|
||||
item.pairedItem = [{ item: itemIndex }];
|
||||
}
|
||||
|
||||
// If the operation is "OR" it means the item did match one condition no ned to check further
|
||||
if (compareResult && combineConditions === 'OR') {
|
||||
returnDataTrue.push(item);
|
||||
continue itemLoop;
|
||||
}
|
||||
|
||||
// If the operation is "AND" it means the item failed one condition no ned to check further
|
||||
if (!compareResult && combineConditions === 'AND') {
|
||||
returnDataFalse.push(item);
|
||||
continue itemLoop;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// If the operation is "AND" it means the item did match all conditions
|
||||
if (combineConditions === 'AND') {
|
||||
returnDataTrue.push(item);
|
||||
} else {
|
||||
// If the operation is "OR" it means the the item did not match any condition.
|
||||
returnDataFalse.push(item);
|
||||
}
|
||||
}
|
||||
|
||||
return [returnDataTrue, returnDataFalse];
|
||||
}
|
||||
}
|
111
packages/nodes-base/nodes/Filter/V2/FilterV2.node.ts
Normal file
111
packages/nodes-base/nodes/Filter/V2/FilterV2.node.ts
Normal file
|
@ -0,0 +1,111 @@
|
|||
import set from 'lodash/set';
|
||||
import type {
|
||||
IExecuteFunctions,
|
||||
INodeExecutionData,
|
||||
INodeType,
|
||||
INodeTypeBaseDescription,
|
||||
INodeTypeDescription,
|
||||
} from 'n8n-workflow';
|
||||
|
||||
export class FilterV2 implements INodeType {
|
||||
description: INodeTypeDescription;
|
||||
|
||||
constructor(baseDescription: INodeTypeBaseDescription) {
|
||||
this.description = {
|
||||
...baseDescription,
|
||||
version: 2,
|
||||
defaults: {
|
||||
name: 'Filter',
|
||||
color: '#229eff',
|
||||
},
|
||||
inputs: ['main'],
|
||||
outputs: ['main'],
|
||||
outputNames: ['Kept', 'Discarded'],
|
||||
properties: [
|
||||
{
|
||||
displayName: 'Conditions',
|
||||
name: 'conditions',
|
||||
placeholder: 'Add Condition',
|
||||
type: 'filter',
|
||||
default: {},
|
||||
typeOptions: {
|
||||
filter: {
|
||||
caseSensitive: '={{!$parameter.options.ignoreCase}}',
|
||||
typeValidation: '={{$parameter.options.looseTypeValidation ? "loose" : "strict"}}',
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
displayName: 'Options',
|
||||
name: 'options',
|
||||
type: 'collection',
|
||||
placeholder: 'Add option',
|
||||
default: {},
|
||||
options: [
|
||||
{
|
||||
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,
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
};
|
||||
}
|
||||
|
||||
async execute(this: IExecuteFunctions): Promise<INodeExecutionData[][]> {
|
||||
const keptItems: INodeExecutionData[] = [];
|
||||
const discardedItems: INodeExecutionData[] = [];
|
||||
|
||||
this.getInputData().forEach((item, itemIndex) => {
|
||||
try {
|
||||
const options = this.getNodeParameter('options', itemIndex) as {
|
||||
ignoreCase?: boolean;
|
||||
looseTypeValidation?: boolean;
|
||||
};
|
||||
let pass = false;
|
||||
try {
|
||||
pass = this.getNodeParameter('conditions', itemIndex, false, {
|
||||
extractValue: true,
|
||||
}) as boolean;
|
||||
} catch (error) {
|
||||
if (!options.looseTypeValidation) {
|
||||
set(
|
||||
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 (item.pairedItem === undefined) {
|
||||
item.pairedItem = { item: itemIndex };
|
||||
}
|
||||
|
||||
if (pass) {
|
||||
keptItems.push(item);
|
||||
} else {
|
||||
discardedItems.push(item);
|
||||
}
|
||||
} catch (error) {
|
||||
if (this.continueOnFail()) {
|
||||
discardedItems.push(item);
|
||||
} else {
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
return [keptItems, discardedItems];
|
||||
}
|
||||
}
|
335
packages/nodes-base/nodes/Filter/test/workflow_v2.json
Normal file
335
packages/nodes-base/nodes/Filter/test/workflow_v2.json
Normal file
|
@ -0,0 +1,335 @@
|
|||
{
|
||||
"name": "Filter v2",
|
||||
"nodes": [
|
||||
{
|
||||
"parameters": {
|
||||
"jsCode": "return [\n {\n id: 1,\n name: 'Adam',\n subscribed: false,\n updatedAt: '2011-10-05T14:48:00.000Z',\n notes: null,\n email: 'adam@mail.com',\n },\n {\n id: 2,\n name: 'Victor',\n subscribed: true,\n updatedAt: '2020-10-05T14:48:00.000Z',\n notes: 'some notes',\n email: 'victor@mail.com',\n },\n {\n id: 3,\n name: 'Sam',\n subscribed: true,\n updatedAt: '2021-10-05T14:48:00.000Z',\n notes: 'other notes',\n email: 'sam@mail.com',\n }, \n];"
|
||||
},
|
||||
"id": "d8f7d6a2-02f5-40f6-83ca-540467f80ad8",
|
||||
"name": "Code",
|
||||
"type": "n8n-nodes-base.code",
|
||||
"typeVersion": 1,
|
||||
"position": [
|
||||
2480,
|
||||
1080
|
||||
]
|
||||
},
|
||||
{
|
||||
"parameters": {},
|
||||
"id": "e112277f-9c6a-404f-a8f2-9b69fd88db16",
|
||||
"name": "When clicking \"Execute Workflow\"",
|
||||
"type": "n8n-nodes-base.manualTrigger",
|
||||
"typeVersion": 1,
|
||||
"position": [
|
||||
2320,
|
||||
1080
|
||||
]
|
||||
},
|
||||
{
|
||||
"parameters": {
|
||||
"conditions": {
|
||||
"options": {
|
||||
"caseSensitive": true,
|
||||
"leftValue": "",
|
||||
"typeValidation": "strict"
|
||||
},
|
||||
"conditions": [
|
||||
{
|
||||
"id": "b65a0fbd-abdb-4622-8312-c12496444ad3",
|
||||
"leftValue": "={{ $json.subscribed }}",
|
||||
"rightValue": "",
|
||||
"operator": {
|
||||
"type": "boolean",
|
||||
"operation": "true",
|
||||
"singleValue": true
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "adf82766-a27f-450e-84a9-9616c5be5191",
|
||||
"leftValue": "={{ $json.notes }}",
|
||||
"rightValue": "",
|
||||
"operator": {
|
||||
"type": "string",
|
||||
"operation": "exists",
|
||||
"singleValue": true
|
||||
}
|
||||
}
|
||||
],
|
||||
"combinator": "and"
|
||||
},
|
||||
"options": {}
|
||||
},
|
||||
"id": "daaa1445-f279-4edb-b2d6-b6415cb5fb55",
|
||||
"name": "Filter Boolean",
|
||||
"type": "n8n-nodes-base.filter",
|
||||
"typeVersion": 2,
|
||||
"position": [
|
||||
2700,
|
||||
800
|
||||
]
|
||||
},
|
||||
{
|
||||
"parameters": {
|
||||
"conditions": {
|
||||
"options": {
|
||||
"caseSensitive": true,
|
||||
"leftValue": "",
|
||||
"typeValidation": "strict"
|
||||
},
|
||||
"conditions": [
|
||||
{
|
||||
"id": "480a5f38-0e31-44c5-9f61-5c34344d265e",
|
||||
"leftValue": "={{ $json.updatedAt }}",
|
||||
"rightValue": "2018-12-31T22:00:00",
|
||||
"operator": {
|
||||
"type": "dateTime",
|
||||
"operation": "after"
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "a9240497-a583-4a6a-97e5-a4197036e04d",
|
||||
"leftValue": "={{ $json.updatedAt }}",
|
||||
"rightValue": "2021-08-03T03:30:08",
|
||||
"operator": {
|
||||
"type": "dateTime",
|
||||
"operation": "before"
|
||||
}
|
||||
}
|
||||
],
|
||||
"combinator": "and"
|
||||
},
|
||||
"options": {}
|
||||
},
|
||||
"id": "816b4bac-2213-4057-b50e-1f7f39542476",
|
||||
"name": "Filter Date",
|
||||
"type": "n8n-nodes-base.filter",
|
||||
"typeVersion": 2,
|
||||
"position": [
|
||||
2700,
|
||||
960
|
||||
]
|
||||
},
|
||||
{
|
||||
"parameters": {
|
||||
"conditions": {
|
||||
"options": {
|
||||
"caseSensitive": true,
|
||||
"leftValue": "",
|
||||
"typeValidation": "loose"
|
||||
},
|
||||
"conditions": [
|
||||
{
|
||||
"id": "21265681-4230-4648-ba79-5513b3d9260f",
|
||||
"leftValue": "={{ $json.id }}",
|
||||
"rightValue": 1,
|
||||
"operator": {
|
||||
"type": "number",
|
||||
"operation": "gt"
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "b4f736a7-d60d-4acd-abc4-04c324581ccf",
|
||||
"leftValue": "={{ $json.id }}",
|
||||
"rightValue": 3,
|
||||
"operator": {
|
||||
"type": "number",
|
||||
"operation": "lte"
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "8b29b1da-8dc3-42b0-9440-7394709e3619",
|
||||
"leftValue": "={{ $json.id }}",
|
||||
"rightValue": "",
|
||||
"operator": {
|
||||
"type": "string",
|
||||
"operation": "exists",
|
||||
"singleValue": true
|
||||
}
|
||||
}
|
||||
],
|
||||
"combinator": "and"
|
||||
},
|
||||
"options": {
|
||||
"looseTypeValidation": true
|
||||
}
|
||||
},
|
||||
"id": "66fe5d53-2652-4860-8d31-cb033af3a7c3",
|
||||
"name": "Filter Number",
|
||||
"type": "n8n-nodes-base.filter",
|
||||
"typeVersion": 2,
|
||||
"position": [
|
||||
2700,
|
||||
1120
|
||||
]
|
||||
},
|
||||
{
|
||||
"parameters": {
|
||||
"conditions": {
|
||||
"options": {
|
||||
"caseSensitive": false,
|
||||
"leftValue": "",
|
||||
"typeValidation": "strict"
|
||||
},
|
||||
"conditions": [
|
||||
{
|
||||
"id": "dafeb20d-80ae-4bb5-8375-ff61002f1bdd",
|
||||
"leftValue": "={{ $json.name }}",
|
||||
"rightValue": "v",
|
||||
"operator": {
|
||||
"type": "string",
|
||||
"operation": "startsWith"
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "8bb39457-005a-427d-9584-6985a05b589d",
|
||||
"leftValue": "={{ $json.name }}",
|
||||
"rightValue": "s",
|
||||
"operator": {
|
||||
"type": "string",
|
||||
"operation": "notContains"
|
||||
}
|
||||
}
|
||||
],
|
||||
"combinator": "or"
|
||||
},
|
||||
"options": {
|
||||
"ignoreCase": true
|
||||
}
|
||||
},
|
||||
"id": "b625a4d4-4bd7-4480-a76a-622d460f4392",
|
||||
"name": "Filter String",
|
||||
"type": "n8n-nodes-base.filter",
|
||||
"typeVersion": 2,
|
||||
"position": [
|
||||
2700,
|
||||
1280
|
||||
]
|
||||
}
|
||||
],
|
||||
"pinData": {
|
||||
"Filter Boolean": [
|
||||
{
|
||||
"json": {
|
||||
"id": 2,
|
||||
"name": "Victor",
|
||||
"subscribed": true,
|
||||
"updatedAt": "2020-10-05T14:48:00.000Z",
|
||||
"notes": "some notes",
|
||||
"email": "victor@mail.com"
|
||||
}
|
||||
},
|
||||
{
|
||||
"json": {
|
||||
"id": 3,
|
||||
"name": "Sam",
|
||||
"subscribed": true,
|
||||
"updatedAt": "2021-10-05T14:48:00.000Z",
|
||||
"notes": "other notes",
|
||||
"email": "sam@mail.com"
|
||||
}
|
||||
}
|
||||
],
|
||||
"Filter Date": [
|
||||
{
|
||||
"json": {
|
||||
"id": 2,
|
||||
"name": "Victor",
|
||||
"subscribed": true,
|
||||
"updatedAt": "2020-10-05T14:48:00.000Z",
|
||||
"notes": "some notes",
|
||||
"email": "victor@mail.com"
|
||||
}
|
||||
}
|
||||
],
|
||||
"Filter Number": [
|
||||
{
|
||||
"json": {
|
||||
"id": 2,
|
||||
"name": "Victor",
|
||||
"subscribed": true,
|
||||
"updatedAt": "2020-10-05T14:48:00.000Z",
|
||||
"notes": "some notes",
|
||||
"email": "victor@mail.com"
|
||||
}
|
||||
},
|
||||
{
|
||||
"json": {
|
||||
"id": 3,
|
||||
"name": "Sam",
|
||||
"subscribed": true,
|
||||
"updatedAt": "2021-10-05T14:48:00.000Z",
|
||||
"notes": "other notes",
|
||||
"email": "sam@mail.com"
|
||||
}
|
||||
}
|
||||
],
|
||||
"Filter String": [
|
||||
{
|
||||
"json": {
|
||||
"id": 1,
|
||||
"name": "Adam",
|
||||
"subscribed": false,
|
||||
"updatedAt": "2011-10-05T14:48:00.000Z",
|
||||
"notes": null,
|
||||
"email": "adam@mail.com"
|
||||
}
|
||||
},
|
||||
{
|
||||
"json": {
|
||||
"id": 2,
|
||||
"name": "Victor",
|
||||
"subscribed": true,
|
||||
"updatedAt": "2020-10-05T14:48:00.000Z",
|
||||
"notes": "some notes",
|
||||
"email": "victor@mail.com"
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
"connections": {
|
||||
"Code": {
|
||||
"main": [
|
||||
[
|
||||
{
|
||||
"node": "Filter Boolean",
|
||||
"type": "main",
|
||||
"index": 0
|
||||
},
|
||||
{
|
||||
"node": "Filter Date",
|
||||
"type": "main",
|
||||
"index": 0
|
||||
},
|
||||
{
|
||||
"node": "Filter Number",
|
||||
"type": "main",
|
||||
"index": 0
|
||||
},
|
||||
{
|
||||
"node": "Filter String",
|
||||
"type": "main",
|
||||
"index": 0
|
||||
}
|
||||
]
|
||||
]
|
||||
},
|
||||
"When clicking \"Execute Workflow\"": {
|
||||
"main": [
|
||||
[
|
||||
{
|
||||
"node": "Code",
|
||||
"type": "main",
|
||||
"index": 0
|
||||
}
|
||||
]
|
||||
]
|
||||
}
|
||||
},
|
||||
"active": false,
|
||||
"settings": {
|
||||
"executionOrder": "v1"
|
||||
},
|
||||
"versionId": "1c680397-66f2-4ec1-a8a6-bb60d6d564d7",
|
||||
"id": "0nx3Xwa3s9uIqflV",
|
||||
"tags": []
|
||||
}
|
Loading…
Reference in a new issue