mirror of
https://github.com/n8n-io/n8n.git
synced 2024-12-24 20:24:05 -08:00
feat(editor): Filter component + implement in If node (#7490)
New Filter component + implementation in If node (v2) <img width="3283" alt="image" src="https://github.com/n8n-io/n8n/assets/8850410/35c379ef-4b62-4d06-82e7-673d4edcd652"> --------- Co-authored-by: Giulio Andreini <andreini@netseven.it> Co-authored-by: Michael Kret <michael.k@radency.com>
This commit is contained in:
parent
09a5729305
commit
8a5343401d
|
@ -41,7 +41,7 @@ export const SCHEDULE_TRIGGER_NODE_NAME = 'Schedule Trigger';
|
||||||
export const CODE_NODE_NAME = 'Code';
|
export const CODE_NODE_NAME = 'Code';
|
||||||
export const SET_NODE_NAME = 'Set';
|
export const SET_NODE_NAME = 'Set';
|
||||||
export const EDIT_FIELDS_SET_NODE_NAME = 'Edit Fields';
|
export const EDIT_FIELDS_SET_NODE_NAME = 'Edit Fields';
|
||||||
export const IF_NODE_NAME = 'IF';
|
export const IF_NODE_NAME = 'If';
|
||||||
export const MERGE_NODE_NAME = 'Merge';
|
export const MERGE_NODE_NAME = 'Merge';
|
||||||
export const SWITCH_NODE_NAME = 'Switch';
|
export const SWITCH_NODE_NAME = 'Switch';
|
||||||
export const GMAIL_NODE_NAME = 'Gmail';
|
export const GMAIL_NODE_NAME = 'Gmail';
|
||||||
|
|
58
cypress/e2e/30-if-node.cy.ts
Normal file
58
cypress/e2e/30-if-node.cy.ts
Normal file
|
@ -0,0 +1,58 @@
|
||||||
|
import { IF_NODE_NAME } from '../constants';
|
||||||
|
import { WorkflowPage, NDV } from '../pages';
|
||||||
|
|
||||||
|
const workflowPage = new WorkflowPage();
|
||||||
|
const ndv = new NDV();
|
||||||
|
|
||||||
|
const FILTER_PARAM_NAME = 'conditions';
|
||||||
|
|
||||||
|
describe('If Node (filter component)', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
workflowPage.actions.visit();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should be able to create and delete multiple conditions', () => {
|
||||||
|
workflowPage.actions.addInitialNodeToCanvas(IF_NODE_NAME, { keepNdvOpen: true });
|
||||||
|
|
||||||
|
// Default state
|
||||||
|
ndv.getters.filterComponent(FILTER_PARAM_NAME).should('exist');
|
||||||
|
ndv.getters.filterConditions(FILTER_PARAM_NAME).should('have.length', 1);
|
||||||
|
ndv.getters
|
||||||
|
.filterConditionOperator(FILTER_PARAM_NAME)
|
||||||
|
.find('input')
|
||||||
|
.should('have.value', 'is equal to');
|
||||||
|
|
||||||
|
// Add
|
||||||
|
ndv.actions.addFilterCondition(FILTER_PARAM_NAME);
|
||||||
|
ndv.getters.filterConditionLeft(FILTER_PARAM_NAME, 0).find('input').type('first left');
|
||||||
|
ndv.getters.filterConditionLeft(FILTER_PARAM_NAME, 1).find('input').type('second left');
|
||||||
|
ndv.actions.addFilterCondition(FILTER_PARAM_NAME);
|
||||||
|
ndv.getters.filterConditions(FILTER_PARAM_NAME).should('have.length', 3);
|
||||||
|
|
||||||
|
// Delete
|
||||||
|
ndv.actions.removeFilterCondition(FILTER_PARAM_NAME, 0);
|
||||||
|
ndv.getters.filterConditions(FILTER_PARAM_NAME).should('have.length', 2);
|
||||||
|
ndv.getters
|
||||||
|
.filterConditionLeft(FILTER_PARAM_NAME, 0)
|
||||||
|
.find('input')
|
||||||
|
.should('have.value', 'second left');
|
||||||
|
ndv.actions.removeFilterCondition(FILTER_PARAM_NAME, 1);
|
||||||
|
ndv.getters.filterConditions(FILTER_PARAM_NAME).should('have.length', 1);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should correctly evaluate conditions', () => {
|
||||||
|
cy.fixture('Test_workflow_filter.json').then((data) => {
|
||||||
|
cy.get('body').paste(JSON.stringify(data));
|
||||||
|
});
|
||||||
|
|
||||||
|
workflowPage.actions.zoomToFit();
|
||||||
|
workflowPage.actions.executeWorkflow();
|
||||||
|
|
||||||
|
workflowPage.actions.openNode('Then');
|
||||||
|
ndv.getters.outputPanel().contains('3 items').should('exist');
|
||||||
|
ndv.actions.close();
|
||||||
|
|
||||||
|
workflowPage.actions.openNode('Else');
|
||||||
|
ndv.getters.outputPanel().contains('1 item').should('exist');
|
||||||
|
});
|
||||||
|
});
|
|
@ -2,6 +2,7 @@ import { NodeCreator } from '../pages/features/node-creator';
|
||||||
import { WorkflowPage as WorkflowPageClass } from '../pages/workflow';
|
import { WorkflowPage as WorkflowPageClass } from '../pages/workflow';
|
||||||
import { NDV } from '../pages/ndv';
|
import { NDV } from '../pages/ndv';
|
||||||
import { getVisibleSelect } from '../utils';
|
import { getVisibleSelect } from '../utils';
|
||||||
|
import { IF_NODE_NAME } from '../constants';
|
||||||
|
|
||||||
const nodeCreatorFeature = new NodeCreator();
|
const nodeCreatorFeature = new NodeCreator();
|
||||||
const WorkflowPage = new WorkflowPageClass();
|
const WorkflowPage = new WorkflowPageClass();
|
||||||
|
@ -360,7 +361,7 @@ describe('Node Creator', () => {
|
||||||
nodeCreatorFeature.getters.nodeItemName().first().should('have.text', 'Edit Fields (Set)');
|
nodeCreatorFeature.getters.nodeItemName().first().should('have.text', 'Edit Fields (Set)');
|
||||||
|
|
||||||
nodeCreatorFeature.getters.searchBar().find('input').clear().type('i');
|
nodeCreatorFeature.getters.searchBar().find('input').clear().type('i');
|
||||||
nodeCreatorFeature.getters.nodeItemName().first().should('have.text', 'IF');
|
nodeCreatorFeature.getters.nodeItemName().first().should('have.text', IF_NODE_NAME);
|
||||||
nodeCreatorFeature.getters.nodeItemName().eq(1).should('have.text', 'Switch');
|
nodeCreatorFeature.getters.nodeItemName().eq(1).should('have.text', 'Switch');
|
||||||
|
|
||||||
nodeCreatorFeature.getters.searchBar().find('input').clear().type('sw');
|
nodeCreatorFeature.getters.searchBar().find('input').clear().type('sw');
|
||||||
|
@ -368,11 +369,11 @@ describe('Node Creator', () => {
|
||||||
nodeCreatorFeature.getters.nodeItemName().first().should('have.text', 'Edit Fields (Set)');
|
nodeCreatorFeature.getters.nodeItemName().first().should('have.text', 'Edit Fields (Set)');
|
||||||
|
|
||||||
nodeCreatorFeature.getters.searchBar().find('input').clear().type('i');
|
nodeCreatorFeature.getters.searchBar().find('input').clear().type('i');
|
||||||
nodeCreatorFeature.getters.nodeItemName().first().should('have.text', 'IF');
|
nodeCreatorFeature.getters.nodeItemName().first().should('have.text', IF_NODE_NAME);
|
||||||
nodeCreatorFeature.getters.nodeItemName().eq(1).should('have.text', 'Switch');
|
nodeCreatorFeature.getters.nodeItemName().eq(1).should('have.text', 'Switch');
|
||||||
|
|
||||||
nodeCreatorFeature.getters.searchBar().find('input').clear().type('IF');
|
nodeCreatorFeature.getters.searchBar().find('input').clear().type('IF');
|
||||||
nodeCreatorFeature.getters.nodeItemName().first().should('have.text', 'IF');
|
nodeCreatorFeature.getters.nodeItemName().first().should('have.text', IF_NODE_NAME);
|
||||||
nodeCreatorFeature.getters.nodeItemName().eq(1).should('have.text', 'Switch');
|
nodeCreatorFeature.getters.nodeItemName().eq(1).should('have.text', 'Switch');
|
||||||
|
|
||||||
nodeCreatorFeature.getters.searchBar().find('input').clear().type('sw');
|
nodeCreatorFeature.getters.searchBar().find('input').clear().type('sw');
|
||||||
|
|
153
cypress/fixtures/Test_workflow_filter.json
Normal file
153
cypress/fixtures/Test_workflow_filter.json
Normal file
|
@ -0,0 +1,153 @@
|
||||||
|
{
|
||||||
|
"name": "Filter test",
|
||||||
|
"nodes": [
|
||||||
|
{
|
||||||
|
"parameters": {},
|
||||||
|
"id": "f332a7d1-31b4-4e78-b31e-9e8db945bf3f",
|
||||||
|
"name": "When clicking \"Execute Workflow\"",
|
||||||
|
"type": "n8n-nodes-base.manualTrigger",
|
||||||
|
"typeVersion": 1,
|
||||||
|
"position": [
|
||||||
|
-60,
|
||||||
|
480
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"parameters": {
|
||||||
|
"jsCode": "return [\n {\n \"label\": \"Apple\",\n tags: [],\n meta: {foo: 'bar'}\n },\n {\n \"label\": \"Banana\",\n tags: ['exotic'],\n meta: {}\n },\n {\n \"label\": \"Pear\",\n tags: ['other'],\n meta: {}\n },\n {\n \"label\": \"Orange\",\n meta: {}\n }\n]"
|
||||||
|
},
|
||||||
|
"id": "60697c7f-3948-4790-97ba-8aba03d02ac2",
|
||||||
|
"name": "Code",
|
||||||
|
"type": "n8n-nodes-base.code",
|
||||||
|
"typeVersion": 2,
|
||||||
|
"position": [
|
||||||
|
160,
|
||||||
|
480
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"parameters": {
|
||||||
|
"conditions": {
|
||||||
|
"options": {
|
||||||
|
"caseSensitive": true,
|
||||||
|
"leftValue": ""
|
||||||
|
},
|
||||||
|
"conditions": [
|
||||||
|
{
|
||||||
|
"leftValue": "={{ $json.tags }}",
|
||||||
|
"rightValue": "exotic",
|
||||||
|
"operator": {
|
||||||
|
"type": "array",
|
||||||
|
"operation": "contains",
|
||||||
|
"rightType": "any"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"leftValue": "={{ $json.meta }}",
|
||||||
|
"rightValue": "",
|
||||||
|
"operator": {
|
||||||
|
"type": "object",
|
||||||
|
"operation": "notEmpty",
|
||||||
|
"singleValue": true
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"leftValue": "={{ $json.label }}",
|
||||||
|
"rightValue": "Pea",
|
||||||
|
"operator": {
|
||||||
|
"type": "string",
|
||||||
|
"operation": "startsWith",
|
||||||
|
"rightType": "string"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"combinator": "or"
|
||||||
|
},
|
||||||
|
"options": {}
|
||||||
|
},
|
||||||
|
"id": "7531191b-5ac3-45dc-8afb-27ae83d8f33a",
|
||||||
|
"name": "If",
|
||||||
|
"type": "n8n-nodes-base.if",
|
||||||
|
"typeVersion": 2,
|
||||||
|
"position": [
|
||||||
|
380,
|
||||||
|
480
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"parameters": {},
|
||||||
|
"id": "d8c614ea-0bbf-4b12-ad7d-c9ebe09ce583",
|
||||||
|
"name": "Then",
|
||||||
|
"type": "n8n-nodes-base.noOp",
|
||||||
|
"typeVersion": 1,
|
||||||
|
"position": [
|
||||||
|
600,
|
||||||
|
400
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"parameters": {},
|
||||||
|
"id": "69364770-60d2-4ef4-9f29-9570718a9a10",
|
||||||
|
"name": "Else",
|
||||||
|
"type": "n8n-nodes-base.noOp",
|
||||||
|
"typeVersion": 1,
|
||||||
|
"position": [
|
||||||
|
600,
|
||||||
|
580
|
||||||
|
]
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"pinData": {},
|
||||||
|
"connections": {
|
||||||
|
"When clicking \"Execute Workflow\"": {
|
||||||
|
"main": [
|
||||||
|
[
|
||||||
|
{
|
||||||
|
"node": "Code",
|
||||||
|
"type": "main",
|
||||||
|
"index": 0
|
||||||
|
}
|
||||||
|
]
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"Code": {
|
||||||
|
"main": [
|
||||||
|
[
|
||||||
|
{
|
||||||
|
"node": "If",
|
||||||
|
"type": "main",
|
||||||
|
"index": 0
|
||||||
|
}
|
||||||
|
]
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"If": {
|
||||||
|
"main": [
|
||||||
|
[
|
||||||
|
{
|
||||||
|
"node": "Then",
|
||||||
|
"type": "main",
|
||||||
|
"index": 0
|
||||||
|
}
|
||||||
|
],
|
||||||
|
[
|
||||||
|
{
|
||||||
|
"node": "Else",
|
||||||
|
"type": "main",
|
||||||
|
"index": 0
|
||||||
|
}
|
||||||
|
]
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"active": false,
|
||||||
|
"settings": {
|
||||||
|
"executionOrder": "v1"
|
||||||
|
},
|
||||||
|
"versionId": "a6249f48-d88f-4b80-9ed9-79555e522d48",
|
||||||
|
"id": "BWUTRs5RHxVgQ4uT",
|
||||||
|
"meta": {
|
||||||
|
"instanceId": "78577815012af39cf16dad7a787b0898c42fb7514b8a7f99b2136862c2af502c"
|
||||||
|
},
|
||||||
|
"tags": []
|
||||||
|
}
|
|
@ -49,9 +49,7 @@ export class NDV extends BasePage {
|
||||||
parameterExpressionPreview: (parameterName: string) =>
|
parameterExpressionPreview: (parameterName: string) =>
|
||||||
this.getters
|
this.getters
|
||||||
.nodeParameters()
|
.nodeParameters()
|
||||||
.find(
|
.find(`[data-test-id="parameter-expression-preview-${parameterName}"]`),
|
||||||
`[data-test-id="parameter-input-${parameterName}"] + [data-test-id="parameter-expression-preview"]`,
|
|
||||||
),
|
|
||||||
nodeNameContainer: () => cy.getByTestId('node-title-container'),
|
nodeNameContainer: () => cy.getByTestId('node-title-container'),
|
||||||
nodeRenameInput: () => cy.getByTestId('node-rename-input'),
|
nodeRenameInput: () => cy.getByTestId('node-rename-input'),
|
||||||
executePrevious: () => cy.getByTestId('execute-previous-node'),
|
executePrevious: () => cy.getByTestId('execute-previous-node'),
|
||||||
|
@ -79,6 +77,23 @@ export class NDV extends BasePage {
|
||||||
cy.getByTestId('columns-parameter-input-options-container'),
|
cy.getByTestId('columns-parameter-input-options-container'),
|
||||||
resourceMapperRemoveAllFieldsOption: () => cy.getByTestId('action-removeAllFields'),
|
resourceMapperRemoveAllFieldsOption: () => cy.getByTestId('action-removeAllFields'),
|
||||||
sqlEditorContainer: () => cy.getByTestId('sql-editor-container'),
|
sqlEditorContainer: () => cy.getByTestId('sql-editor-container'),
|
||||||
|
filterComponent: (paramName: string) => cy.getByTestId(`filter-${paramName}`),
|
||||||
|
filterCombinator: (paramName: string, index = 0) =>
|
||||||
|
this.getters.filterComponent(paramName).getByTestId('filter-combinator-select').eq(index),
|
||||||
|
filterConditions: (paramName: string) =>
|
||||||
|
this.getters.filterComponent(paramName).getByTestId('filter-condition'),
|
||||||
|
filterCondition: (paramName: string, index = 0) =>
|
||||||
|
this.getters.filterComponent(paramName).getByTestId('filter-condition').eq(index),
|
||||||
|
filterConditionLeft: (paramName: string, index = 0) =>
|
||||||
|
this.getters.filterComponent(paramName).getByTestId('filter-condition-left').eq(index),
|
||||||
|
filterConditionRight: (paramName: string, index = 0) =>
|
||||||
|
this.getters.filterComponent(paramName).getByTestId('filter-condition-right').eq(index),
|
||||||
|
filterConditionOperator: (paramName: string, index = 0) =>
|
||||||
|
this.getters.filterComponent(paramName).getByTestId('filter-operator-select').eq(index),
|
||||||
|
filterConditionRemove: (paramName: string, index = 0) =>
|
||||||
|
this.getters.filterComponent(paramName).getByTestId('filter-remove-condition').eq(index),
|
||||||
|
filterConditionAdd: (paramName: string) =>
|
||||||
|
this.getters.filterComponent(paramName).getByTestId('filter-add-condition'),
|
||||||
searchInput: () => cy.getByTestId('ndv-search'),
|
searchInput: () => cy.getByTestId('ndv-search'),
|
||||||
pagination: () => cy.getByTestId('ndv-data-pagination'),
|
pagination: () => cy.getByTestId('ndv-data-pagination'),
|
||||||
nodeVersion: () => cy.getByTestId('node-version'),
|
nodeVersion: () => cy.getByTestId('node-version'),
|
||||||
|
@ -199,7 +214,6 @@ export class NDV extends BasePage {
|
||||||
.find('span')
|
.find('span')
|
||||||
.should('include.html', asEncodedHTML(value));
|
.should('include.html', asEncodedHTML(value));
|
||||||
},
|
},
|
||||||
|
|
||||||
refreshResourceMapperColumns: () => {
|
refreshResourceMapperColumns: () => {
|
||||||
this.getters.resourceMapperSelectColumn().realHover();
|
this.getters.resourceMapperSelectColumn().realHover();
|
||||||
this.getters
|
this.getters
|
||||||
|
@ -210,7 +224,12 @@ export class NDV extends BasePage {
|
||||||
|
|
||||||
getVisiblePopper().find('li').last().click();
|
getVisiblePopper().find('li').last().click();
|
||||||
},
|
},
|
||||||
|
addFilterCondition: (paramName: string) => {
|
||||||
|
this.getters.filterConditionAdd(paramName).click();
|
||||||
|
},
|
||||||
|
removeFilterCondition: (paramName: string, index: number) => {
|
||||||
|
this.getters.filterConditionRemove(paramName, index).click();
|
||||||
|
},
|
||||||
setInvalidExpression: ({
|
setInvalidExpression: ({
|
||||||
fieldName,
|
fieldName,
|
||||||
invalidExpression,
|
invalidExpression,
|
||||||
|
|
|
@ -7,6 +7,8 @@ import {
|
||||||
type SupplyData,
|
type SupplyData,
|
||||||
} from 'n8n-workflow';
|
} from 'n8n-workflow';
|
||||||
|
|
||||||
|
import type { TextSplitter } from 'langchain/text_splitter';
|
||||||
|
|
||||||
import { logWrapper } from '../../../utils/logWrapper';
|
import { logWrapper } from '../../../utils/logWrapper';
|
||||||
import { N8nBinaryLoader } from '../../../utils/N8nBinaryLoader';
|
import { N8nBinaryLoader } from '../../../utils/N8nBinaryLoader';
|
||||||
import { getConnectionHintNoticeField, metadataFilterField } from '../../../utils/sharedFields';
|
import { getConnectionHintNoticeField, metadataFilterField } from '../../../utils/sharedFields';
|
||||||
|
@ -17,7 +19,6 @@ import { getConnectionHintNoticeField, metadataFilterField } from '../../../util
|
||||||
import 'mammoth'; // for docx
|
import 'mammoth'; // for docx
|
||||||
import 'epub2'; // for epub
|
import 'epub2'; // for epub
|
||||||
import 'pdf-parse'; // for pdf
|
import 'pdf-parse'; // for pdf
|
||||||
import type { TextSplitter } from 'langchain/text_splitter';
|
|
||||||
|
|
||||||
export class DocumentBinaryInputLoader implements INodeType {
|
export class DocumentBinaryInputLoader implements INodeType {
|
||||||
description: INodeTypeDescription = {
|
description: INodeTypeDescription = {
|
||||||
|
|
|
@ -1,18 +1,19 @@
|
||||||
import type {
|
import get from 'lodash/get';
|
||||||
INode,
|
|
||||||
INodeParameters,
|
|
||||||
INodeProperties,
|
|
||||||
INodePropertyCollection,
|
|
||||||
INodePropertyOptions,
|
|
||||||
INodeType,
|
|
||||||
NodeParameterValueType,
|
|
||||||
} from 'n8n-workflow';
|
|
||||||
import {
|
import {
|
||||||
NodeOperationError,
|
|
||||||
NodeHelpers,
|
|
||||||
LoggerProxy,
|
|
||||||
WorkflowOperationError,
|
|
||||||
ApplicationError,
|
ApplicationError,
|
||||||
|
LoggerProxy,
|
||||||
|
NodeHelpers,
|
||||||
|
NodeOperationError,
|
||||||
|
WorkflowOperationError,
|
||||||
|
executeFilter,
|
||||||
|
isFilterValue,
|
||||||
|
type INode,
|
||||||
|
type INodeParameters,
|
||||||
|
type INodeProperties,
|
||||||
|
type INodePropertyCollection,
|
||||||
|
type INodePropertyOptions,
|
||||||
|
type INodeType,
|
||||||
|
type NodeParameterValueType,
|
||||||
} from 'n8n-workflow';
|
} from 'n8n-workflow';
|
||||||
|
|
||||||
function findPropertyFromParameterName(
|
function findPropertyFromParameterName(
|
||||||
|
@ -123,6 +124,26 @@ function extractValueRLC(
|
||||||
return executeRegexExtractValue(value.value, regex, parameterName, property.displayName);
|
return executeRegexExtractValue(value.value, regex, parameterName, property.displayName);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function extractValueFilter(
|
||||||
|
value: NodeParameterValueType | object,
|
||||||
|
property: INodeProperties,
|
||||||
|
parameterName: string,
|
||||||
|
itemIndex: number,
|
||||||
|
): NodeParameterValueType | object {
|
||||||
|
if (!isFilterValue(value)) {
|
||||||
|
return value;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (property.extractValue?.type) {
|
||||||
|
throw new ApplicationError(
|
||||||
|
`Property "${parameterName}" has an invalid extractValue type. Filter parameters only support extractValue: true`,
|
||||||
|
{ extra: { parameter: parameterName } },
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return executeFilter(value, { itemIndex });
|
||||||
|
}
|
||||||
|
|
||||||
function extractValueOther(
|
function extractValueOther(
|
||||||
value: NodeParameterValueType | object,
|
value: NodeParameterValueType | object,
|
||||||
property: INodeProperties | INodePropertyCollection,
|
property: INodeProperties | INodePropertyCollection,
|
||||||
|
@ -162,6 +183,7 @@ export function extractValue(
|
||||||
parameterName: string,
|
parameterName: string,
|
||||||
node: INode,
|
node: INode,
|
||||||
nodeType: INodeType,
|
nodeType: INodeType,
|
||||||
|
itemIndex = 0,
|
||||||
): NodeParameterValueType | object {
|
): NodeParameterValueType | object {
|
||||||
let property: INodePropertyOptions | INodeProperties | INodePropertyCollection;
|
let property: INodePropertyOptions | INodeProperties | INodePropertyCollection;
|
||||||
try {
|
try {
|
||||||
|
@ -174,10 +196,12 @@ export function extractValue(
|
||||||
|
|
||||||
if (property.type === 'resourceLocator') {
|
if (property.type === 'resourceLocator') {
|
||||||
return extractValueRLC(value, property, parameterName);
|
return extractValueRLC(value, property, parameterName);
|
||||||
|
} else if (property.type === 'filter') {
|
||||||
|
return extractValueFilter(value, property, parameterName, itemIndex);
|
||||||
}
|
}
|
||||||
return extractValueOther(value, property, parameterName);
|
return extractValueOther(value, property, parameterName);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
// eslint-disable-next-line @typescript-eslint/no-unsafe-argument
|
// eslint-disable-next-line @typescript-eslint/no-unsafe-argument, @typescript-eslint/no-unsafe-assignment
|
||||||
throw new NodeOperationError(node, error);
|
throw new NodeOperationError(node, error, { description: get(error, 'description') });
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -2041,12 +2041,9 @@ const validateResourceMapperValue = (
|
||||||
}
|
}
|
||||||
|
|
||||||
if (schemaEntry?.type) {
|
if (schemaEntry?.type) {
|
||||||
const validationResult = validateFieldType(
|
const validationResult = validateFieldType(key, resolvedValue, schemaEntry.type, {
|
||||||
key,
|
valueOptions: schemaEntry.options,
|
||||||
resolvedValue,
|
});
|
||||||
schemaEntry.type,
|
|
||||||
schemaEntry.options,
|
|
||||||
);
|
|
||||||
if (!validationResult.valid) {
|
if (!validationResult.valid) {
|
||||||
return { ...validationResult, fieldName: key };
|
return { ...validationResult, fieldName: key };
|
||||||
} else {
|
} else {
|
||||||
|
@ -2107,12 +2104,9 @@ const validateCollection = (
|
||||||
for (const key of Object.keys(value)) {
|
for (const key of Object.keys(value)) {
|
||||||
if (!validationMap[key]) continue;
|
if (!validationMap[key]) continue;
|
||||||
|
|
||||||
const fieldValidationResult = validateFieldType(
|
const fieldValidationResult = validateFieldType(key, value[key], validationMap[key].type, {
|
||||||
key,
|
valueOptions: validationMap[key].options,
|
||||||
value[key],
|
});
|
||||||
validationMap[key].type,
|
|
||||||
validationMap[key].options,
|
|
||||||
);
|
|
||||||
|
|
||||||
if (!fieldValidationResult.valid) {
|
if (!fieldValidationResult.valid) {
|
||||||
throw new ExpressionError(
|
throw new ExpressionError(
|
||||||
|
@ -2270,7 +2264,7 @@ export function getNodeParameter(
|
||||||
|
|
||||||
// This is outside the try/catch because it throws errors with proper messages
|
// This is outside the try/catch because it throws errors with proper messages
|
||||||
if (options?.extractValue) {
|
if (options?.extractValue) {
|
||||||
returnData = extractValue(returnData, parameterName, node, nodeType);
|
returnData = extractValue(returnData, parameterName, node, nodeType, itemIndex);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Validate parameter value if it has a schema defined(RMC) or validateType defined
|
// Validate parameter value if it has a schema defined(RMC) or validateType defined
|
||||||
|
|
|
@ -190,21 +190,20 @@ export default defineComponent({
|
||||||
opacity: 1;
|
opacity: 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
.heading {
|
|
||||||
display: flex;
|
|
||||||
}
|
|
||||||
|
|
||||||
.overflow {
|
.overflow {
|
||||||
overflow-x: hidden;
|
overflow-x: hidden;
|
||||||
overflow-y: clip;
|
overflow-y: clip;
|
||||||
}
|
}
|
||||||
|
|
||||||
.small {
|
.heading {
|
||||||
margin-bottom: var(--spacing-5xs);
|
display: flex;
|
||||||
}
|
|
||||||
|
|
||||||
.medium {
|
&.small {
|
||||||
margin-bottom: var(--spacing-2xs);
|
margin-bottom: var(--spacing-5xs);
|
||||||
|
}
|
||||||
|
&.medium {
|
||||||
|
margin-bottom: var(--spacing-2xs);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.underline {
|
.underline {
|
||||||
|
|
|
@ -38,40 +38,49 @@ export default defineComponent({
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const unsortedBreakpoints = [...(this.breakpoints || [])] as Array<{
|
const root = this.$refs.root as HTMLDivElement;
|
||||||
width: number;
|
|
||||||
bp: string;
|
|
||||||
}>;
|
|
||||||
|
|
||||||
const bps = unsortedBreakpoints.sort((a, b) => a.width - b.width);
|
if (!root) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.bp = this.getBreakpointFromWidth(root.offsetWidth);
|
||||||
|
|
||||||
const observer = new ResizeObserver((entries) => {
|
const observer = new ResizeObserver((entries) => {
|
||||||
entries.forEach((entry) => {
|
entries.forEach((entry) => {
|
||||||
// We wrap it in requestAnimationFrame to avoid this error - ResizeObserver loop limit exceeded
|
// We wrap it in requestAnimationFrame to avoid this error - ResizeObserver loop limit exceeded
|
||||||
requestAnimationFrame(() => {
|
requestAnimationFrame(() => {
|
||||||
const newWidth = entry.contentRect.width;
|
this.bp = this.getBreakpointFromWidth(entry.contentRect.width);
|
||||||
let newBP = 'default';
|
|
||||||
for (let i = 0; i < bps.length; i++) {
|
|
||||||
if (newWidth < bps[i].width) {
|
|
||||||
newBP = bps[i].bp;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
this.bp = newBP;
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
this.observer = observer;
|
this.observer = observer;
|
||||||
|
observer.observe(root);
|
||||||
if (this.$refs.root) {
|
|
||||||
observer.observe(this.$refs.root as HTMLDivElement);
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
beforeUnmount() {
|
beforeUnmount() {
|
||||||
if (this.enabled) {
|
if (this.enabled) {
|
||||||
this.observer?.disconnect();
|
this.observer?.disconnect();
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
methods: {
|
||||||
|
getBreakpointFromWidth(width: number): string {
|
||||||
|
let newBP = 'default';
|
||||||
|
const unsortedBreakpoints = [...(this.breakpoints || [])] as Array<{
|
||||||
|
width: number;
|
||||||
|
bp: string;
|
||||||
|
}>;
|
||||||
|
|
||||||
|
const bps = unsortedBreakpoints.sort((a, b) => a.width - b.width);
|
||||||
|
for (let i = 0; i < bps.length; i++) {
|
||||||
|
if (width < bps[i].width) {
|
||||||
|
newBP = bps[i].bp;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return newBP;
|
||||||
|
},
|
||||||
|
},
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
|
@ -50,4 +50,5 @@ export { default as N8nUserStack } from './N8nUserStack';
|
||||||
export { default as N8nUserInfo } from './N8nUserInfo';
|
export { default as N8nUserInfo } from './N8nUserInfo';
|
||||||
export { default as N8nUserSelect } from './N8nUserSelect';
|
export { default as N8nUserSelect } from './N8nUserSelect';
|
||||||
export { default as N8nUsersList } from './N8nUsersList';
|
export { default as N8nUsersList } from './N8nUsersList';
|
||||||
|
export { default as N8nResizeObserver } from './ResizeObserver';
|
||||||
export { N8nKeyboardShortcut } from './N8nKeyboardShortcut';
|
export { N8nKeyboardShortcut } from './N8nKeyboardShortcut';
|
||||||
|
|
|
@ -399,8 +399,16 @@ $input-placeholder-color: var(--input-placeholder-color, var(--color-text-light)
|
||||||
$input-focus-border: var(--input-focus-border-color, var(--color-secondary));
|
$input-focus-border: var(--input-focus-border-color, var(--color-secondary));
|
||||||
$input-border-color: var(--input-border-color, var(--border-color-base));
|
$input-border-color: var(--input-border-color, var(--border-color-base));
|
||||||
$input-border-style: var(--input-border-style, var(--border-style-base));
|
$input-border-style: var(--input-border-style, var(--border-style-base));
|
||||||
$input-border-width: var(--border-width-base);
|
$input-border-width: var(--input-border-width, var(--border-width-base));
|
||||||
$input-border: $input-border-color $input-border-style $input-border-width;
|
$input-border: $input-border-color $input-border-style $input-border-width;
|
||||||
|
$input-border-right-color: var(
|
||||||
|
--input-border-right-color,
|
||||||
|
var(--input-border-color, var(--border-color-base))
|
||||||
|
);
|
||||||
|
$input-border-bottom-color: var(
|
||||||
|
--input-border-bottom-color,
|
||||||
|
var(--input-border-color, var(--border-color-base))
|
||||||
|
);
|
||||||
|
|
||||||
$input-font-size: var(--input-font-size, var(--font-size-s));
|
$input-font-size: var(--input-font-size, var(--font-size-s));
|
||||||
/// color||Color|0
|
/// color||Color|0
|
||||||
|
@ -411,6 +419,23 @@ $input-width: 140px;
|
||||||
$input-height: 40px;
|
$input-height: 40px;
|
||||||
/// borderRadius||Border|2
|
/// borderRadius||Border|2
|
||||||
$input-border-radius: var(--input-border-radius, var(--border-radius-base));
|
$input-border-radius: var(--input-border-radius, var(--border-radius-base));
|
||||||
|
$input-border-top-left-radius: var(
|
||||||
|
--input-border-top-left-radius,
|
||||||
|
var(--input-border-radius, var(--border-radius-base))
|
||||||
|
);
|
||||||
|
$input-border-top-right-radius: var(
|
||||||
|
--input-border-top-right-radius,
|
||||||
|
var(--input-border-radius, var(--border-radius-base)),
|
||||||
|
);
|
||||||
|
$input-border-bottom-left-radius: var(
|
||||||
|
--input-border-bottom-left-radius,
|
||||||
|
var(--input-border-radius, var(--border-radius-base)),
|
||||||
|
);
|
||||||
|
$input-border-bottom-right-radius: var(
|
||||||
|
--input-border-bottom-right-radius,
|
||||||
|
var(--input-border-radius, var(--border-radius-base)),
|
||||||
|
);
|
||||||
|
$input-border-radius: var(--input-border-radius, var(--border-radius-base));
|
||||||
$input-border-color-hover: $border-color-hover;
|
$input-border-color-hover: $border-color-hover;
|
||||||
/// color||Color|0
|
/// color||Color|0
|
||||||
$input-background-color: var(--input-background-color, var(--color-foreground-xlight));
|
$input-background-color: var(--input-background-color, var(--color-foreground-xlight));
|
||||||
|
|
|
@ -20,6 +20,11 @@
|
||||||
background-color: var.$input-background-color;
|
background-color: var.$input-background-color;
|
||||||
background-image: none;
|
background-image: none;
|
||||||
border-radius: var.$input-border-radius;
|
border-radius: var.$input-border-radius;
|
||||||
|
border-top-left-radius: var.$input-border-top-left-radius;
|
||||||
|
border-top-right-radius: var.$input-border-top-right-radius;
|
||||||
|
border-bottom-left-radius: var.$input-border-bottom-left-radius;
|
||||||
|
border-bottom-right-radius: var.$input-border-bottom-right-radius;
|
||||||
|
|
||||||
transition: var.$border-transition-base;
|
transition: var.$border-transition-base;
|
||||||
|
|
||||||
&,
|
&,
|
||||||
|
@ -108,7 +113,13 @@
|
||||||
background-color: var.$input-background-color;
|
background-color: var.$input-background-color;
|
||||||
background-image: none;
|
background-image: none;
|
||||||
border-radius: var.$input-border-radius;
|
border-radius: var.$input-border-radius;
|
||||||
|
border-top-left-radius: var.$input-border-top-left-radius;
|
||||||
|
border-top-right-radius: var.$input-border-top-right-radius;
|
||||||
|
border-bottom-left-radius: var.$input-border-bottom-left-radius;
|
||||||
|
border-bottom-right-radius: var.$input-border-bottom-right-radius;
|
||||||
border: var.$input-border;
|
border: var.$input-border;
|
||||||
|
border-right-color: var.$input-border-right-color;
|
||||||
|
border-bottom-color: var.$input-border-bottom-color;
|
||||||
box-sizing: border-box;
|
box-sizing: border-box;
|
||||||
color: var.$input-font-color;
|
color: var.$input-font-color;
|
||||||
display: inline-block;
|
display: inline-block;
|
||||||
|
@ -145,6 +156,7 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
@include mixins.e(suffix-inner) {
|
@include mixins.e(suffix-inner) {
|
||||||
|
display: inline-flex;
|
||||||
pointer-events: all;
|
pointer-events: all;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -286,8 +298,14 @@
|
||||||
vertical-align: middle;
|
vertical-align: middle;
|
||||||
display: table-cell;
|
display: table-cell;
|
||||||
position: relative;
|
position: relative;
|
||||||
border: var(--border-base);
|
border: var.$input-border;
|
||||||
border-radius: var.$input-border-radius;
|
border-radius: var.$input-border-radius;
|
||||||
|
border-top-left-radius: var.$input-border-top-left-radius;
|
||||||
|
border-top-right-radius: var.$input-border-top-right-radius;
|
||||||
|
border-bottom-left-radius: var.$input-border-bottom-left-radius;
|
||||||
|
border-bottom-right-radius: var.$input-border-bottom-right-radius;
|
||||||
|
border-right-color: var.$input-border-right-color;
|
||||||
|
border-bottom-color: var.$input-border-bottom-color;
|
||||||
padding: 0 10px;
|
padding: 0 10px;
|
||||||
width: 1px;
|
width: 1px;
|
||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
|
|
|
@ -80,6 +80,14 @@
|
||||||
&.is-focus .el-input__inner {
|
&.is-focus .el-input__inner {
|
||||||
border-color: var.$select-input-focus-border-color;
|
border-color: var.$select-input-focus-border-color;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
&__prefix {
|
||||||
|
left: var(--spacing-2xs);
|
||||||
|
}
|
||||||
|
|
||||||
|
&--prefix .el-input__inner {
|
||||||
|
padding-left: 26px;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
> .el-input {
|
> .el-input {
|
||||||
|
|
|
@ -51,6 +51,7 @@ import {
|
||||||
N8nUserInfo,
|
N8nUserInfo,
|
||||||
N8nUserSelect,
|
N8nUserSelect,
|
||||||
N8nUsersList,
|
N8nUsersList,
|
||||||
|
N8nResizeObserver,
|
||||||
N8nKeyboardShortcut,
|
N8nKeyboardShortcut,
|
||||||
N8nUserStack,
|
N8nUserStack,
|
||||||
} from './components';
|
} from './components';
|
||||||
|
@ -111,6 +112,7 @@ export const N8nPlugin: Plugin<N8nPluginOptions> = {
|
||||||
app.component('n8n-user-info', N8nUserInfo);
|
app.component('n8n-user-info', N8nUserInfo);
|
||||||
app.component('n8n-users-list', N8nUsersList);
|
app.component('n8n-users-list', N8nUsersList);
|
||||||
app.component('n8n-user-select', N8nUserSelect);
|
app.component('n8n-user-select', N8nUserSelect);
|
||||||
|
app.component('n8n-resize-observer', N8nResizeObserver);
|
||||||
app.component('n8n-keyboard-shortcut', N8nKeyboardShortcut);
|
app.component('n8n-keyboard-shortcut', N8nKeyboardShortcut);
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
|
@ -1215,7 +1215,7 @@ export interface NDVState {
|
||||||
isDragging: boolean;
|
isDragging: boolean;
|
||||||
type: string;
|
type: string;
|
||||||
data: string;
|
data: string;
|
||||||
canDrop: boolean;
|
activeTargetId: string | null;
|
||||||
stickyPosition: null | XYPosition;
|
stickyPosition: null | XYPosition;
|
||||||
};
|
};
|
||||||
isMappingOnboarded: boolean;
|
isMappingOnboarded: boolean;
|
||||||
|
|
|
@ -9,6 +9,7 @@ import { defineComponent } from 'vue';
|
||||||
import type { PropType } from 'vue';
|
import type { PropType } from 'vue';
|
||||||
import { mapStores } from 'pinia';
|
import { mapStores } from 'pinia';
|
||||||
import { useNDVStore } from '@/stores/ndv.store';
|
import { useNDVStore } from '@/stores/ndv.store';
|
||||||
|
import { v4 as uuid } from 'uuid';
|
||||||
|
|
||||||
export default defineComponent({
|
export default defineComponent({
|
||||||
props: {
|
props: {
|
||||||
|
@ -29,6 +30,7 @@ export default defineComponent({
|
||||||
data() {
|
data() {
|
||||||
return {
|
return {
|
||||||
hovering: false,
|
hovering: false,
|
||||||
|
id: uuid(),
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
mounted() {
|
mounted() {
|
||||||
|
@ -83,7 +85,12 @@ export default defineComponent({
|
||||||
},
|
},
|
||||||
watch: {
|
watch: {
|
||||||
activeDrop(active) {
|
activeDrop(active) {
|
||||||
this.ndvStore.setDraggableCanDrop(active);
|
if (active) {
|
||||||
|
this.ndvStore.setDraggableTargetId(this.id);
|
||||||
|
} else if (this.ndvStore.draggable.activeTargetId === this.id) {
|
||||||
|
// Only clear active target if it is this one
|
||||||
|
this.ndvStore.setDraggableTargetId(null);
|
||||||
|
}
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
|
@ -5,20 +5,14 @@
|
||||||
@keydown.tab="onBlur"
|
@keydown.tab="onBlur"
|
||||||
>
|
>
|
||||||
<div :class="[$style['all-sections'], { [$style['focused']]: isFocused }]">
|
<div :class="[$style['all-sections'], { [$style['focused']]: isFocused }]">
|
||||||
<div
|
<div :class="[$style['prepend-section'], 'el-input-group__prepend']">
|
||||||
:class="[
|
|
||||||
$style['prepend-section'],
|
|
||||||
'el-input-group__prepend',
|
|
||||||
{ [$style['squared']]: isForRecordLocator },
|
|
||||||
]"
|
|
||||||
>
|
|
||||||
<ExpressionFunctionIcon />
|
<ExpressionFunctionIcon />
|
||||||
</div>
|
</div>
|
||||||
<InlineExpressionEditorInput
|
<InlineExpressionEditorInput
|
||||||
:modelValue="modelValue"
|
:modelValue="modelValue"
|
||||||
:isReadOnly="isReadOnly"
|
:isReadOnly="isReadOnly"
|
||||||
:targetItem="hoveringItem"
|
:targetItem="hoveringItem"
|
||||||
:isSingleLine="isForRecordLocator"
|
:isSingleLine="isSingleLine"
|
||||||
:additionalData="additionalExpressionData"
|
:additionalData="additionalExpressionData"
|
||||||
:path="path"
|
:path="path"
|
||||||
@focus="onFocus"
|
@focus="onFocus"
|
||||||
|
@ -86,7 +80,7 @@ export default defineComponent({
|
||||||
type: Boolean,
|
type: Boolean,
|
||||||
default: false,
|
default: false,
|
||||||
},
|
},
|
||||||
isForRecordLocator: {
|
isSingleLine: {
|
||||||
type: Boolean,
|
type: Boolean,
|
||||||
default: false,
|
default: false,
|
||||||
},
|
},
|
||||||
|
@ -182,10 +176,6 @@ export default defineComponent({
|
||||||
width: 22px;
|
width: 22px;
|
||||||
text-align: center;
|
text-align: center;
|
||||||
}
|
}
|
||||||
|
|
||||||
.squared {
|
|
||||||
border-radius: 0;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.expression-editor-modal-opener {
|
.expression-editor-modal-opener {
|
||||||
|
@ -197,7 +187,15 @@ export default defineComponent({
|
||||||
line-height: 9px;
|
line-height: 9px;
|
||||||
border: var(--border-base);
|
border: var(--border-base);
|
||||||
border-top-left-radius: var(--border-radius-base);
|
border-top-left-radius: var(--border-radius-base);
|
||||||
border-bottom-right-radius: var(--border-radius-base);
|
border-bottom-right-radius: var(--input-border-bottom-right-radius, var(--border-radius-base));
|
||||||
|
border-right-color: var(
|
||||||
|
--input-border-right-color,
|
||||||
|
var(--input-border-color, var(--border-color-base))
|
||||||
|
);
|
||||||
|
border-bottom-color: var(
|
||||||
|
--input-border-bottom-color,
|
||||||
|
var(--input-border-color, var(--border-color-base))
|
||||||
|
);
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
|
|
||||||
svg {
|
svg {
|
||||||
|
|
|
@ -0,0 +1,48 @@
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { useI18n } from '@/composables/useI18n';
|
||||||
|
import type { FilterTypeCombinator } from 'n8n-workflow';
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
options: FilterTypeCombinator[];
|
||||||
|
selected: FilterTypeCombinator;
|
||||||
|
readOnly: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
defineProps<Props>();
|
||||||
|
|
||||||
|
const emit = defineEmits<{
|
||||||
|
(event: 'combinatorChange', value: FilterTypeCombinator): void;
|
||||||
|
}>();
|
||||||
|
|
||||||
|
const i18n = useI18n();
|
||||||
|
|
||||||
|
const onCombinatorChange = (combinator: FilterTypeCombinator): void => {
|
||||||
|
emit('combinatorChange', combinator);
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div data-test-id="filter-combinator-select" :class="$style.combinatorSelect">
|
||||||
|
<div v-if="readOnly || options.length === 1">
|
||||||
|
{{ i18n.baseText(`filter.combinator.${selected}`) }}
|
||||||
|
</div>
|
||||||
|
<n8n-select v-else size="small" :modelValue="selected" @update:modelValue="onCombinatorChange">
|
||||||
|
<n8n-option
|
||||||
|
v-for="option in options"
|
||||||
|
:key="option"
|
||||||
|
:value="option"
|
||||||
|
:label="i18n.baseText(`filter.combinator.${option}`)"
|
||||||
|
>
|
||||||
|
</n8n-option>
|
||||||
|
</n8n-select>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style lang="scss" module>
|
||||||
|
.combinatorSelect {
|
||||||
|
max-width: 80px;
|
||||||
|
line-height: var(--font-line-height-xloose);
|
||||||
|
font-size: var(--font-size-2xs);
|
||||||
|
color: var(--color-text-light);
|
||||||
|
}
|
||||||
|
</style>
|
451
packages/editor-ui/src/components/FilterConditions/Condition.vue
Normal file
451
packages/editor-ui/src/components/FilterConditions/Condition.vue
Normal file
|
@ -0,0 +1,451 @@
|
||||||
|
<script setup lang="ts">
|
||||||
|
import type { IUpdateInformation } from '@/Interface';
|
||||||
|
import ParameterInputFull from '@/components/ParameterInputFull.vue';
|
||||||
|
import ParameterIssues from '@/components/ParameterIssues.vue';
|
||||||
|
import { useI18n } from '@/composables/useI18n';
|
||||||
|
import { DateTime } from 'luxon';
|
||||||
|
import {
|
||||||
|
executeFilterCondition,
|
||||||
|
type FilterOptionsValue,
|
||||||
|
type FilterConditionValue,
|
||||||
|
type FilterOperatorType,
|
||||||
|
type INodeProperties,
|
||||||
|
type NodeParameterValue,
|
||||||
|
type NodePropertyTypes,
|
||||||
|
FilterError,
|
||||||
|
validateFieldType,
|
||||||
|
} from 'n8n-workflow';
|
||||||
|
import { computed, ref } from 'vue';
|
||||||
|
import OperatorSelect from './OperatorSelect.vue';
|
||||||
|
import { OPERATORS_BY_ID, type FilterOperatorId } from './constants';
|
||||||
|
import type { FilterOperator } from './types';
|
||||||
|
import { resolveParameter } from '@/mixins/workflowHelpers';
|
||||||
|
type ConditionResult =
|
||||||
|
| { status: 'resolve_error' }
|
||||||
|
| { status: 'validation_error'; error: string }
|
||||||
|
| { status: 'success'; result: boolean };
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
path: string;
|
||||||
|
condition: FilterConditionValue;
|
||||||
|
options: FilterOptionsValue;
|
||||||
|
issues?: string[];
|
||||||
|
fixedLeftValue?: boolean;
|
||||||
|
canRemove?: boolean;
|
||||||
|
index?: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
const props = withDefaults(defineProps<Props>(), {
|
||||||
|
issues: () => [],
|
||||||
|
canRemove: true,
|
||||||
|
fixedLeftValue: false,
|
||||||
|
});
|
||||||
|
|
||||||
|
const emit = defineEmits<{
|
||||||
|
(event: 'update', value: FilterConditionValue): void;
|
||||||
|
(event: 'remove'): void;
|
||||||
|
}>();
|
||||||
|
|
||||||
|
const i18n = useI18n();
|
||||||
|
|
||||||
|
const condition = ref<FilterConditionValue>(props.condition);
|
||||||
|
|
||||||
|
const operatorId = computed<FilterOperatorId>(() => {
|
||||||
|
const { type, operation } = props.condition.operator;
|
||||||
|
return `${type}:${operation}` as FilterOperatorId;
|
||||||
|
});
|
||||||
|
const operator = computed(() => OPERATORS_BY_ID[operatorId.value] as FilterOperator);
|
||||||
|
|
||||||
|
const operatorTypeToNodePropType = (operatorType: FilterOperatorType): NodePropertyTypes => {
|
||||||
|
switch (operatorType) {
|
||||||
|
case 'array':
|
||||||
|
case 'object':
|
||||||
|
case 'boolean':
|
||||||
|
case 'any':
|
||||||
|
return 'string';
|
||||||
|
default:
|
||||||
|
return operatorType;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const conditionResult = computed<ConditionResult>(() => {
|
||||||
|
try {
|
||||||
|
const resolved = resolveParameter(
|
||||||
|
condition.value as unknown as NodeParameterValue,
|
||||||
|
) as FilterConditionValue;
|
||||||
|
|
||||||
|
if (resolved.leftValue === undefined || resolved.rightValue === undefined) {
|
||||||
|
return { status: 'resolve_error' };
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
const result = executeFilterCondition(resolved, props.options, {
|
||||||
|
index: props.index ?? 0,
|
||||||
|
errorFormat: 'inline',
|
||||||
|
});
|
||||||
|
return { status: 'success', result };
|
||||||
|
} catch (error) {
|
||||||
|
let errorMessage = i18n.baseText('parameterInput.error');
|
||||||
|
|
||||||
|
if (error instanceof FilterError) {
|
||||||
|
errorMessage = `${error.message}.\n${error.description}`;
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
status: 'validation_error',
|
||||||
|
error: errorMessage,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
return { status: 'resolve_error' };
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const allIssues = computed(() => {
|
||||||
|
if (conditionResult.value.status === 'validation_error') {
|
||||||
|
return [conditionResult.value.error];
|
||||||
|
}
|
||||||
|
|
||||||
|
return props.issues;
|
||||||
|
});
|
||||||
|
|
||||||
|
const now = computed(() => DateTime.now().toISO());
|
||||||
|
|
||||||
|
const leftParameter = computed<INodeProperties>(() => ({
|
||||||
|
name: '',
|
||||||
|
displayName: '',
|
||||||
|
default: '',
|
||||||
|
placeholder:
|
||||||
|
operator.value.type === 'dateTime'
|
||||||
|
? now.value
|
||||||
|
: i18n.baseText('filter.condition.placeholderLeft'),
|
||||||
|
type: operatorTypeToNodePropType(operator.value.type),
|
||||||
|
}));
|
||||||
|
|
||||||
|
const rightParameter = computed<INodeProperties>(() => ({
|
||||||
|
name: '',
|
||||||
|
displayName: '',
|
||||||
|
default: '',
|
||||||
|
placeholder:
|
||||||
|
operator.value.type === 'dateTime'
|
||||||
|
? now.value
|
||||||
|
: i18n.baseText('filter.condition.placeholderRight'),
|
||||||
|
type: operatorTypeToNodePropType(operator.value.rightType ?? operator.value.type),
|
||||||
|
}));
|
||||||
|
|
||||||
|
const onLeftValueChange = (update: IUpdateInformation): void => {
|
||||||
|
condition.value.leftValue = update.value;
|
||||||
|
};
|
||||||
|
|
||||||
|
const onRightValueChange = (update: IUpdateInformation): void => {
|
||||||
|
condition.value.rightValue = update.value;
|
||||||
|
};
|
||||||
|
|
||||||
|
const convertToType = (value: unknown, type: FilterOperatorType): unknown => {
|
||||||
|
if (type === 'any') return value;
|
||||||
|
|
||||||
|
return (
|
||||||
|
validateFieldType('filter', condition.value.leftValue, type, { parseStrings: true }).newValue ??
|
||||||
|
value
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const onOperatorChange = (value: string): void => {
|
||||||
|
const newOperator = OPERATORS_BY_ID[value as FilterOperatorId] as FilterOperator;
|
||||||
|
const rightType = operator.value.rightType ?? operator.value.type;
|
||||||
|
const newRightType = newOperator.rightType ?? newOperator.type;
|
||||||
|
const leftTypeChanged = operator.value.type !== newOperator.type;
|
||||||
|
const rightTypeChanged = rightType !== newRightType;
|
||||||
|
|
||||||
|
// Try to convert left & right values to operator type
|
||||||
|
if (leftTypeChanged) {
|
||||||
|
condition.value.leftValue = convertToType(condition.value.leftValue, newOperator.type);
|
||||||
|
}
|
||||||
|
if (rightTypeChanged && !newOperator.singleValue) {
|
||||||
|
condition.value.rightValue = convertToType(condition.value.rightValue, newRightType);
|
||||||
|
}
|
||||||
|
|
||||||
|
condition.value.operator = {
|
||||||
|
type: newOperator.type,
|
||||||
|
operation: newOperator.operation,
|
||||||
|
rightType: newOperator.rightType,
|
||||||
|
singleValue: newOperator.singleValue,
|
||||||
|
};
|
||||||
|
emit('update', condition.value);
|
||||||
|
};
|
||||||
|
|
||||||
|
const onRemove = (): void => {
|
||||||
|
emit('remove');
|
||||||
|
};
|
||||||
|
|
||||||
|
const onBlur = (): void => {
|
||||||
|
emit('update', condition.value);
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div
|
||||||
|
:class="{
|
||||||
|
[$style.wrapper]: true,
|
||||||
|
[$style.hasIssues]: allIssues.length > 0,
|
||||||
|
}"
|
||||||
|
data-test-id="filter-condition"
|
||||||
|
>
|
||||||
|
<n8n-icon-button
|
||||||
|
v-if="canRemove"
|
||||||
|
type="tertiary"
|
||||||
|
text
|
||||||
|
size="mini"
|
||||||
|
icon="trash"
|
||||||
|
data-test-id="filter-remove-condition"
|
||||||
|
:title="i18n.baseText('filter.removeCondition')"
|
||||||
|
:class="$style.remove"
|
||||||
|
@click="onRemove"
|
||||||
|
></n8n-icon-button>
|
||||||
|
<n8n-resize-observer
|
||||||
|
:class="$style.observer"
|
||||||
|
:breakpoints="[
|
||||||
|
{ bp: 'stacked', width: 340 },
|
||||||
|
{ bp: 'medium', width: 520 },
|
||||||
|
]"
|
||||||
|
>
|
||||||
|
<template #default="{ bp }">
|
||||||
|
<div
|
||||||
|
:class="{
|
||||||
|
[$style.condition]: true,
|
||||||
|
[$style.hideRightInput]: operator.singleValue,
|
||||||
|
[$style.stacked]: bp === 'stacked',
|
||||||
|
[$style.medium]: bp === 'medium',
|
||||||
|
}"
|
||||||
|
>
|
||||||
|
<parameter-input-full
|
||||||
|
v-if="!fixedLeftValue"
|
||||||
|
displayOptions
|
||||||
|
hideLabel
|
||||||
|
hideHint
|
||||||
|
isSingleLine
|
||||||
|
:key="leftParameter.type"
|
||||||
|
:parameter="leftParameter"
|
||||||
|
:value="condition.leftValue"
|
||||||
|
:path="`${path}.left`"
|
||||||
|
:class="[$style.input, $style.inputLeft]"
|
||||||
|
data-test-id="filter-condition-left"
|
||||||
|
@update="onLeftValueChange"
|
||||||
|
@blur="onBlur"
|
||||||
|
/>
|
||||||
|
<operator-select
|
||||||
|
:class="$style.select"
|
||||||
|
:selected="`${operator.type}:${operator.operation}`"
|
||||||
|
@operatorChange="onOperatorChange"
|
||||||
|
></operator-select>
|
||||||
|
<parameter-input-full
|
||||||
|
v-if="!operator.singleValue"
|
||||||
|
displayOptions
|
||||||
|
hideLabel
|
||||||
|
hideHint
|
||||||
|
isSingleLine
|
||||||
|
:key="rightParameter.type"
|
||||||
|
:optionsPosition="bp === 'default' ? 'top' : 'bottom'"
|
||||||
|
:parameter="rightParameter"
|
||||||
|
:value="condition.rightValue"
|
||||||
|
:path="`${path}.right`"
|
||||||
|
:class="[$style.input, $style.inputRight]"
|
||||||
|
data-test-id="filter-condition-right"
|
||||||
|
@update="onRightValueChange"
|
||||||
|
@blur="onBlur"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</n8n-resize-observer>
|
||||||
|
|
||||||
|
<div :class="$style.status">
|
||||||
|
<parameter-issues v-if="allIssues.length > 0" :issues="allIssues" />
|
||||||
|
|
||||||
|
<n8n-tooltip
|
||||||
|
:show-after="500"
|
||||||
|
v-else-if="conditionResult.status === 'success' && conditionResult.result === true"
|
||||||
|
>
|
||||||
|
<template #content>
|
||||||
|
{{ i18n.baseText('filter.condition.resolvedTrue') }}
|
||||||
|
</template>
|
||||||
|
<n8n-icon :class="$style.statusIcon" icon="check-circle" size="medium" color="text-light" />
|
||||||
|
</n8n-tooltip>
|
||||||
|
|
||||||
|
<n8n-tooltip
|
||||||
|
:show-after="500"
|
||||||
|
v-else-if="conditionResult.status === 'success' && conditionResult.result === false"
|
||||||
|
>
|
||||||
|
<template #content>
|
||||||
|
{{ i18n.baseText('filter.condition.resolvedFalse') }}
|
||||||
|
</template>
|
||||||
|
<n8n-icon :class="$style.statusIcon" icon="times-circle" size="medium" color="text-light" />
|
||||||
|
</n8n-tooltip>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style lang="scss" module>
|
||||||
|
.wrapper {
|
||||||
|
position: relative;
|
||||||
|
display: flex;
|
||||||
|
align-items: flex-end;
|
||||||
|
gap: var(--spacing-4xs);
|
||||||
|
padding-left: var(--spacing-l);
|
||||||
|
|
||||||
|
&.hasIssues {
|
||||||
|
--input-border-color: var(--color-danger);
|
||||||
|
}
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
.remove {
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.condition {
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: nowrap;
|
||||||
|
align-items: flex-end;
|
||||||
|
}
|
||||||
|
|
||||||
|
.observer {
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.status {
|
||||||
|
align-self: flex-start;
|
||||||
|
padding-top: 28px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.statusIcon {
|
||||||
|
padding-left: var(--spacing-4xs);
|
||||||
|
}
|
||||||
|
|
||||||
|
.select {
|
||||||
|
flex-shrink: 0;
|
||||||
|
flex-grow: 0;
|
||||||
|
flex-basis: 160px;
|
||||||
|
--input-border-radius: 0;
|
||||||
|
--input-border-right-color: transparent;
|
||||||
|
}
|
||||||
|
|
||||||
|
.input {
|
||||||
|
flex-shrink: 0;
|
||||||
|
flex-basis: 160px;
|
||||||
|
flex-grow: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.inputLeft {
|
||||||
|
--input-border-top-right-radius: 0;
|
||||||
|
--input-border-bottom-right-radius: 0;
|
||||||
|
--input-border-right-color: transparent;
|
||||||
|
}
|
||||||
|
|
||||||
|
.inputRight {
|
||||||
|
--input-border-top-left-radius: 0;
|
||||||
|
--input-border-bottom-left-radius: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hideRightInput {
|
||||||
|
.select {
|
||||||
|
--input-border-top-right-radius: var(--border-radius-base);
|
||||||
|
--input-border-bottom-right-radius: var(--border-radius-base);
|
||||||
|
--input-border-right-color: var(--input-border-color-base);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.remove {
|
||||||
|
--button-font-color: var(--color-text-light);
|
||||||
|
position: absolute;
|
||||||
|
left: 0;
|
||||||
|
top: var(--spacing-l);
|
||||||
|
opacity: 0;
|
||||||
|
transition: opacity 100ms ease-in;
|
||||||
|
}
|
||||||
|
|
||||||
|
.medium {
|
||||||
|
flex-wrap: wrap;
|
||||||
|
|
||||||
|
.select {
|
||||||
|
--input-border-top-right-radius: var(--border-radius-base);
|
||||||
|
--input-border-bottom-right-radius: 0;
|
||||||
|
--input-border-bottom-color: transparent;
|
||||||
|
--input-border-right-color: var(--input-border-color-base);
|
||||||
|
}
|
||||||
|
|
||||||
|
.inputLeft {
|
||||||
|
--input-border-top-right-radius: 0;
|
||||||
|
--input-border-bottom-left-radius: 0;
|
||||||
|
--input-border-right-color: transparent;
|
||||||
|
--input-border-bottom-color: transparent;
|
||||||
|
}
|
||||||
|
|
||||||
|
.inputRight {
|
||||||
|
flex-basis: 340px;
|
||||||
|
flex-shrink: 1;
|
||||||
|
--input-border-top-right-radius: 0;
|
||||||
|
--input-border-bottom-left-radius: var(--border-radius-base);
|
||||||
|
}
|
||||||
|
|
||||||
|
&.hideRightInput {
|
||||||
|
.select {
|
||||||
|
--input-border-bottom-color: var(--input-border-color-base);
|
||||||
|
--input-border-top-left-radius: 0;
|
||||||
|
--input-border-bottom-left-radius: 0;
|
||||||
|
--input-border-top-right-radius: var(--border-radius-base);
|
||||||
|
--input-border-bottom-right-radius: var(--border-radius-base);
|
||||||
|
}
|
||||||
|
|
||||||
|
.inputLeft {
|
||||||
|
--input-border-top-right-radius: 0;
|
||||||
|
--input-border-bottom-left-radius: var(--border-radius-base);
|
||||||
|
--input-border-bottom-right-radius: 0;
|
||||||
|
--input-border-bottom-color: var(--input-border-color-base);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.stacked {
|
||||||
|
display: block;
|
||||||
|
|
||||||
|
.select {
|
||||||
|
width: 100%;
|
||||||
|
--input-border-right-color: var(--input-border-color-base);
|
||||||
|
--input-border-bottom-color: transparent;
|
||||||
|
--input-border-radius: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.inputLeft {
|
||||||
|
--input-border-right-color: var(--input-border-color-base);
|
||||||
|
--input-border-bottom-color: transparent;
|
||||||
|
--input-border-top-left-radius: var(--border-radius-base);
|
||||||
|
--input-border-top-right-radius: var(--border-radius-base);
|
||||||
|
--input-border-bottom-left-radius: 0;
|
||||||
|
--input-border-bottom-right-radius: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.inputRight {
|
||||||
|
--input-border-top-left-radius: 0;
|
||||||
|
--input-border-top-right-radius: 0;
|
||||||
|
--input-border-bottom-left-radius: var(--border-radius-base);
|
||||||
|
--input-border-bottom-right-radius: var(--border-radius-base);
|
||||||
|
}
|
||||||
|
|
||||||
|
&.hideRightInput {
|
||||||
|
.select {
|
||||||
|
--input-border-bottom-color: var(--input-border-color-base);
|
||||||
|
--input-border-top-left-radius: 0;
|
||||||
|
--input-border-top-right-radius: 0;
|
||||||
|
--input-border-bottom-left-radius: var(--border-radius-base);
|
||||||
|
--input-border-bottom-right-radius: var(--border-radius-base);
|
||||||
|
}
|
||||||
|
|
||||||
|
.inputLeft {
|
||||||
|
--input-border-top-left-radius: var(--border-radius-base);
|
||||||
|
--input-border-top-right-radius: var(--border-radius-base);
|
||||||
|
--input-border-bottom-left-radius: 0;
|
||||||
|
--input-border-bottom-right-radius: 0;
|
||||||
|
--input-border-bottom-color: transparent;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
|
@ -0,0 +1,228 @@
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { isEqual } from 'lodash-es';
|
||||||
|
|
||||||
|
import {
|
||||||
|
type FilterConditionValue,
|
||||||
|
type FilterValue,
|
||||||
|
type INodeProperties,
|
||||||
|
type FilterTypeCombinator,
|
||||||
|
type INode,
|
||||||
|
type NodeParameterValue,
|
||||||
|
type FilterOptionsValue,
|
||||||
|
} from 'n8n-workflow';
|
||||||
|
import { computed, reactive, watch } from 'vue';
|
||||||
|
import { useNDVStore } from '@/stores/ndv.store';
|
||||||
|
import {
|
||||||
|
DEFAULT_FILTER_OPTIONS,
|
||||||
|
DEFAULT_MAX_CONDITIONS,
|
||||||
|
DEFAULT_OPERATOR_VALUE,
|
||||||
|
} from './constants';
|
||||||
|
import { useI18n } from '@/composables/useI18n';
|
||||||
|
import { useDebounceHelper } from '@/composables/useDebounce';
|
||||||
|
import Condition from './Condition.vue';
|
||||||
|
import CombinatorSelect from './CombinatorSelect.vue';
|
||||||
|
import { resolveParameter } from '@/mixins/workflowHelpers';
|
||||||
|
import { v4 as uuid } from 'uuid';
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
parameter: INodeProperties;
|
||||||
|
value: FilterValue;
|
||||||
|
path: string;
|
||||||
|
node: INode | null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const props = defineProps<Props>();
|
||||||
|
|
||||||
|
const emit = defineEmits<{
|
||||||
|
(event: 'valueChanged', value: { name: string; node: string; value: FilterValue }): void;
|
||||||
|
}>();
|
||||||
|
|
||||||
|
const i18n = useI18n();
|
||||||
|
const ndvStore = useNDVStore();
|
||||||
|
const { callDebounced } = useDebounceHelper();
|
||||||
|
|
||||||
|
function createCondition(): FilterConditionValue {
|
||||||
|
return { id: uuid(), leftValue: '', rightValue: '', operator: DEFAULT_OPERATOR_VALUE };
|
||||||
|
}
|
||||||
|
|
||||||
|
const allowedCombinators = computed<FilterTypeCombinator[]>(
|
||||||
|
() => props.parameter.typeOptions?.filter?.allowedCombinators ?? ['and', 'or'],
|
||||||
|
);
|
||||||
|
|
||||||
|
const state = reactive<{ paramValue: FilterValue }>({
|
||||||
|
paramValue: {
|
||||||
|
options: props.value?.options ?? DEFAULT_FILTER_OPTIONS,
|
||||||
|
conditions: props.value?.conditions ?? [createCondition()],
|
||||||
|
combinator: props.value?.combinator ?? allowedCombinators.value[0],
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const maxConditions = computed(
|
||||||
|
() => props.parameter.typeOptions?.filter?.maxConditions ?? DEFAULT_MAX_CONDITIONS,
|
||||||
|
);
|
||||||
|
|
||||||
|
const maxConditionsReached = computed(
|
||||||
|
() => maxConditions.value <= state.paramValue.conditions.length,
|
||||||
|
);
|
||||||
|
|
||||||
|
const issues = computed(() => {
|
||||||
|
if (!ndvStore.activeNode) return {};
|
||||||
|
return ndvStore.activeNode?.issues?.parameters ?? {};
|
||||||
|
});
|
||||||
|
|
||||||
|
watch(
|
||||||
|
() => props.node?.parameters,
|
||||||
|
() => {
|
||||||
|
const typeOptions = props.parameter.typeOptions?.filter;
|
||||||
|
|
||||||
|
if (!typeOptions) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
let newOptions: FilterOptionsValue = DEFAULT_FILTER_OPTIONS;
|
||||||
|
try {
|
||||||
|
newOptions = {
|
||||||
|
...DEFAULT_FILTER_OPTIONS,
|
||||||
|
...resolveParameter(typeOptions as NodeParameterValue),
|
||||||
|
};
|
||||||
|
} catch (error) {}
|
||||||
|
|
||||||
|
if (!isEqual(state.paramValue.options, newOptions)) {
|
||||||
|
state.paramValue.options = newOptions;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{ immediate: true },
|
||||||
|
);
|
||||||
|
|
||||||
|
watch(state.paramValue, (value) => {
|
||||||
|
void callDebounced(
|
||||||
|
() => {
|
||||||
|
emit('valueChanged', { name: props.path, value, node: props.node?.name as string });
|
||||||
|
},
|
||||||
|
{ debounceTime: 1000 },
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
function addCondition(): void {
|
||||||
|
state.paramValue.conditions.push(createCondition());
|
||||||
|
}
|
||||||
|
|
||||||
|
function onConditionUpdate(index: number, value: FilterConditionValue): void {
|
||||||
|
state.paramValue.conditions[index] = value;
|
||||||
|
}
|
||||||
|
|
||||||
|
function onCombinatorChange(combinator: FilterTypeCombinator): void {
|
||||||
|
state.paramValue.combinator = combinator;
|
||||||
|
}
|
||||||
|
|
||||||
|
function onConditionRemove(index: number): void {
|
||||||
|
state.paramValue.conditions.splice(index, 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
function getIssues(index: number): string[] {
|
||||||
|
return issues.value[`${props.parameter.name}.${index}`] ?? [];
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div :class="$style.filter" :data-test-id="`filter-${parameter.name}`">
|
||||||
|
<n8n-input-label
|
||||||
|
:label="parameter.displayName"
|
||||||
|
:underline="true"
|
||||||
|
:showOptions="true"
|
||||||
|
:showExpressionSelector="false"
|
||||||
|
color="text-dark"
|
||||||
|
>
|
||||||
|
</n8n-input-label>
|
||||||
|
<div :class="$style.content">
|
||||||
|
<div :class="$style.conditions">
|
||||||
|
<div v-for="(condition, index) of state.paramValue.conditions" :key="condition.id">
|
||||||
|
<combinator-select
|
||||||
|
v-if="index !== 0"
|
||||||
|
:readOnly="index !== 1"
|
||||||
|
:options="allowedCombinators"
|
||||||
|
:selected="state.paramValue.combinator"
|
||||||
|
:class="$style.combinator"
|
||||||
|
@combinatorChange="onCombinatorChange"
|
||||||
|
/>
|
||||||
|
|
||||||
|
<condition
|
||||||
|
:condition="condition"
|
||||||
|
:index="index"
|
||||||
|
:options="state.paramValue.options"
|
||||||
|
:fixedLeftValue="!!parameter.typeOptions?.filter?.leftValue"
|
||||||
|
:canRemove="index !== 0 || state.paramValue.conditions.length > 1"
|
||||||
|
:path="`${path}.${index}`"
|
||||||
|
:issues="getIssues(index)"
|
||||||
|
@update="(value) => onConditionUpdate(index, value)"
|
||||||
|
@remove="() => onConditionRemove(index)"
|
||||||
|
></condition>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div :class="$style.addConditionWrapper">
|
||||||
|
<n8n-button
|
||||||
|
type="tertiary"
|
||||||
|
block
|
||||||
|
@click="addCondition"
|
||||||
|
:class="$style.addCondition"
|
||||||
|
:label="i18n.baseText('filter.addCondition')"
|
||||||
|
:title="maxConditionsReached ? i18n.baseText('filter.maxConditions') : ''"
|
||||||
|
:disabled="maxConditionsReached"
|
||||||
|
data-test-id="filter-add-condition"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style lang="scss" module>
|
||||||
|
.filter {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
margin: var(--spacing-xs) 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.conditions {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: var(--spacing-4xs);
|
||||||
|
}
|
||||||
|
.combinator {
|
||||||
|
position: relative;
|
||||||
|
z-index: 1;
|
||||||
|
margin-top: var(--spacing-2xs);
|
||||||
|
margin-bottom: calc(var(--spacing-2xs) * -1);
|
||||||
|
margin-left: var(--spacing-l);
|
||||||
|
}
|
||||||
|
|
||||||
|
.addConditionWrapper {
|
||||||
|
margin-top: var(--spacing-l);
|
||||||
|
margin-left: var(--spacing-l);
|
||||||
|
}
|
||||||
|
|
||||||
|
.addCondition {
|
||||||
|
// Styling to match collection button (should move to standard button in future)
|
||||||
|
font-weight: var(--font-weight-normal);
|
||||||
|
--button-font-color: var(--color-text-dark);
|
||||||
|
--button-border-color: var(--color-foreground-base);
|
||||||
|
--button-background-color: var(--color-background-base);
|
||||||
|
|
||||||
|
--button-hover-font-color: var(--color-text-dark);
|
||||||
|
--button-hover-border-color: var(--color-foreground-base);
|
||||||
|
--button-hover-background-color: var(--color-background-base);
|
||||||
|
|
||||||
|
--button-active-font-color: var(--color-text-dark);
|
||||||
|
--button-active-border-color: var(--color-foreground-base);
|
||||||
|
--button-active-background-color: var(--color-background-base);
|
||||||
|
|
||||||
|
--button-focus-font-color: var(--color-text-dark);
|
||||||
|
--button-focus-border-color: var(--color-foreground-base);
|
||||||
|
--button-focus-background-color: var(--color-background-base);
|
||||||
|
|
||||||
|
&:hover,
|
||||||
|
&:focus,
|
||||||
|
&:active {
|
||||||
|
outline: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
|
@ -0,0 +1,137 @@
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { useI18n } from '@/composables/useI18n';
|
||||||
|
import { OPERATORS_BY_ID, OPERATOR_GROUPS } from './constants';
|
||||||
|
import { computed, ref } from 'vue';
|
||||||
|
import type { FilterOperator } from './types';
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
selected: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
const props = defineProps<Props>();
|
||||||
|
|
||||||
|
const selected = ref(props.selected);
|
||||||
|
const menuOpen = ref(false);
|
||||||
|
const shouldRenderItems = ref(false);
|
||||||
|
const submenu = ref('none');
|
||||||
|
|
||||||
|
const emit = defineEmits<{
|
||||||
|
(event: 'operatorChange', value: string): void;
|
||||||
|
}>();
|
||||||
|
|
||||||
|
const i18n = useI18n();
|
||||||
|
|
||||||
|
const groups = OPERATOR_GROUPS;
|
||||||
|
|
||||||
|
const selectedGroupIcon = computed(
|
||||||
|
() => groups.find((group) => group.id === selected.value.split(':')[0])?.icon,
|
||||||
|
);
|
||||||
|
|
||||||
|
const selectedOperator = computed(() => OPERATORS_BY_ID[selected.value] as FilterOperator);
|
||||||
|
|
||||||
|
const onOperatorChange = (operator: string): void => {
|
||||||
|
selected.value = operator;
|
||||||
|
emit('operatorChange', operator);
|
||||||
|
};
|
||||||
|
|
||||||
|
const getOperatorId = (operator: FilterOperator): string =>
|
||||||
|
`${operator.type}:${operator.operation}`;
|
||||||
|
|
||||||
|
function onSelectVisibleChange(open: boolean) {
|
||||||
|
menuOpen.value = open;
|
||||||
|
if (!open) {
|
||||||
|
submenu.value = 'none';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function onGroupSelect(group: string) {
|
||||||
|
if (menuOpen.value) {
|
||||||
|
submenu.value = group;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<n8n-select
|
||||||
|
data-test-id="filter-operator-select"
|
||||||
|
size="small"
|
||||||
|
:key="selectedGroupIcon"
|
||||||
|
:modelValue="selected"
|
||||||
|
@update:modelValue="onOperatorChange"
|
||||||
|
@visible-change="onSelectVisibleChange"
|
||||||
|
@mouseenter="shouldRenderItems = true"
|
||||||
|
>
|
||||||
|
<template v-if="selectedGroupIcon" #prefix>
|
||||||
|
<n8n-icon
|
||||||
|
:class="$style.selectedGroupIcon"
|
||||||
|
:icon="selectedGroupIcon"
|
||||||
|
color="text-light"
|
||||||
|
size="small"
|
||||||
|
/>
|
||||||
|
</template>
|
||||||
|
<div :class="$style.groups" v-if="shouldRenderItems">
|
||||||
|
<div :key="group.name" v-for="group of groups">
|
||||||
|
<n8n-popover
|
||||||
|
:visible="submenu === group.id"
|
||||||
|
placement="right-start"
|
||||||
|
:show-arrow="false"
|
||||||
|
:offset="2"
|
||||||
|
:popper-style="{ padding: 'var(--spacing-3xs) 0' }"
|
||||||
|
width="auto"
|
||||||
|
>
|
||||||
|
<template #reference>
|
||||||
|
<div
|
||||||
|
@mouseenter="() => onGroupSelect(group.id)"
|
||||||
|
@click="() => onGroupSelect(group.id)"
|
||||||
|
:class="$style.groupTitle"
|
||||||
|
>
|
||||||
|
<n8n-icon v-if="group.icon" :icon="group.icon" color="text-light" size="small" />
|
||||||
|
<span>{{ i18n.baseText(group.name) }}</span>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
<div>
|
||||||
|
<n8n-option
|
||||||
|
v-for="operator in group.children"
|
||||||
|
:key="getOperatorId(operator)"
|
||||||
|
:value="getOperatorId(operator)"
|
||||||
|
:label="i18n.baseText(operator.name)"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</n8n-popover>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<n8n-option
|
||||||
|
v-else
|
||||||
|
:key="selected"
|
||||||
|
:value="selected"
|
||||||
|
:label="i18n.baseText(selectedOperator.name)"
|
||||||
|
/>
|
||||||
|
</n8n-select>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style lang="scss" module>
|
||||||
|
.selectedGroupIcon {
|
||||||
|
color: var(--color-text-light);
|
||||||
|
}
|
||||||
|
|
||||||
|
.groups {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
|
||||||
|
.groupTitle {
|
||||||
|
display: flex;
|
||||||
|
gap: var(--spacing-2xs);
|
||||||
|
align-items: center;
|
||||||
|
font-size: var(--font-size-s);
|
||||||
|
font-weight: var(--font-weight-bold);
|
||||||
|
line-height: var(--font-line-height-regular);
|
||||||
|
color: var(--color-text-dark);
|
||||||
|
padding: var(--spacing-2xs) var(--spacing-s);
|
||||||
|
cursor: pointer;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
background: var(--color-background-base);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
272
packages/editor-ui/src/components/FilterConditions/constants.ts
Normal file
272
packages/editor-ui/src/components/FilterConditions/constants.ts
Normal file
|
@ -0,0 +1,272 @@
|
||||||
|
/* eslint-disable @typescript-eslint/naming-convention */
|
||||||
|
import type { FilterConditionValue, FilterOptionsValue } from 'n8n-workflow';
|
||||||
|
import type { FilterOperator, FilterOperatorGroup } from './types';
|
||||||
|
|
||||||
|
export const DEFAULT_MAX_CONDITIONS = 10;
|
||||||
|
|
||||||
|
export const DEFAULT_FILTER_OPTIONS: FilterOptionsValue = {
|
||||||
|
caseSensitive: true,
|
||||||
|
leftValue: '',
|
||||||
|
typeValidation: 'strict',
|
||||||
|
};
|
||||||
|
|
||||||
|
export const OPERATORS_BY_ID = {
|
||||||
|
'string:exists': {
|
||||||
|
type: 'string',
|
||||||
|
operation: 'exists',
|
||||||
|
name: 'filter.operator.exists',
|
||||||
|
singleValue: true,
|
||||||
|
},
|
||||||
|
'string:notExists': {
|
||||||
|
type: 'string',
|
||||||
|
operation: 'notExists',
|
||||||
|
name: 'filter.operator.notExists',
|
||||||
|
singleValue: true,
|
||||||
|
},
|
||||||
|
'string:equals': { type: 'string', operation: 'equals', name: 'filter.operator.equals' },
|
||||||
|
'string:notEquals': { type: 'string', operation: 'notEquals', name: 'filter.operator.notEquals' },
|
||||||
|
'string:contains': { type: 'string', operation: 'contains', name: 'filter.operator.contains' },
|
||||||
|
'string:notContains': {
|
||||||
|
type: 'string',
|
||||||
|
operation: 'notContains',
|
||||||
|
name: 'filter.operator.notContains',
|
||||||
|
},
|
||||||
|
'string:startsWith': {
|
||||||
|
type: 'string',
|
||||||
|
operation: 'startsWith',
|
||||||
|
name: 'filter.operator.startsWith',
|
||||||
|
},
|
||||||
|
'string:notStartsWith': {
|
||||||
|
type: 'string',
|
||||||
|
operation: 'notStartsWith',
|
||||||
|
name: 'filter.operator.notStartsWith',
|
||||||
|
},
|
||||||
|
'string:endsWith': { type: 'string', operation: 'endsWith', name: 'filter.operator.endsWith' },
|
||||||
|
'string:notEndsWith': {
|
||||||
|
type: 'string',
|
||||||
|
operation: 'notEndsWith',
|
||||||
|
name: 'filter.operator.notEndsWith',
|
||||||
|
},
|
||||||
|
'string:regex': { type: 'string', operation: 'regex', name: 'filter.operator.regex' },
|
||||||
|
'string:notRegex': { type: 'string', operation: 'notRegex', name: 'filter.operator.notRegex' },
|
||||||
|
'number:exists': {
|
||||||
|
type: 'number',
|
||||||
|
operation: 'exists',
|
||||||
|
name: 'filter.operator.exists',
|
||||||
|
singleValue: true,
|
||||||
|
},
|
||||||
|
'number:notExists': {
|
||||||
|
type: 'number',
|
||||||
|
operation: 'notExists',
|
||||||
|
name: 'filter.operator.notExists',
|
||||||
|
singleValue: true,
|
||||||
|
},
|
||||||
|
'number:equals': { type: 'number', operation: 'equals', name: 'filter.operator.equals' },
|
||||||
|
'number:notEquals': { type: 'number', operation: 'notEquals', name: 'filter.operator.notEquals' },
|
||||||
|
'number:gt': { type: 'number', operation: 'gt', name: 'filter.operator.gt' },
|
||||||
|
'number:lt': { type: 'number', operation: 'lt', name: 'filter.operator.lt' },
|
||||||
|
'number:gte': { type: 'number', operation: 'gte', name: 'filter.operator.gte' },
|
||||||
|
'number:lte': { type: 'number', operation: 'lte', name: 'filter.operator.lte' },
|
||||||
|
'dateTime:exists': {
|
||||||
|
type: 'dateTime',
|
||||||
|
operation: 'exists',
|
||||||
|
name: 'filter.operator.exists',
|
||||||
|
singleValue: true,
|
||||||
|
},
|
||||||
|
'dateTime:notExists': {
|
||||||
|
type: 'dateTime',
|
||||||
|
operation: 'notExists',
|
||||||
|
name: 'filter.operator.notExists',
|
||||||
|
singleValue: true,
|
||||||
|
},
|
||||||
|
'dateTime:equals': { type: 'dateTime', operation: 'equals', name: 'filter.operator.equals' },
|
||||||
|
'dateTime:notEquals': {
|
||||||
|
type: 'dateTime',
|
||||||
|
operation: 'notEquals',
|
||||||
|
name: 'filter.operator.notEquals',
|
||||||
|
},
|
||||||
|
'dateTime:after': { type: 'dateTime', operation: 'after', name: 'filter.operator.after' },
|
||||||
|
'dateTime:before': { type: 'dateTime', operation: 'before', name: 'filter.operator.before' },
|
||||||
|
'dateTime:afterOrEquals': {
|
||||||
|
type: 'dateTime',
|
||||||
|
operation: 'afterOrEquals',
|
||||||
|
name: 'filter.operator.afterOrEquals',
|
||||||
|
},
|
||||||
|
'dateTime:beforeOrEquals': {
|
||||||
|
type: 'dateTime',
|
||||||
|
operation: 'beforeOrEquals',
|
||||||
|
name: 'filter.operator.beforeOrEquals',
|
||||||
|
},
|
||||||
|
'boolean:exists': {
|
||||||
|
type: 'boolean',
|
||||||
|
operation: 'exists',
|
||||||
|
name: 'filter.operator.exists',
|
||||||
|
singleValue: true,
|
||||||
|
},
|
||||||
|
'boolean:notExists': {
|
||||||
|
type: 'boolean',
|
||||||
|
operation: 'notExists',
|
||||||
|
name: 'filter.operator.notExists',
|
||||||
|
singleValue: true,
|
||||||
|
},
|
||||||
|
'boolean:true': {
|
||||||
|
type: 'boolean',
|
||||||
|
operation: 'true',
|
||||||
|
name: 'filter.operator.true',
|
||||||
|
singleValue: true,
|
||||||
|
},
|
||||||
|
'boolean:false': {
|
||||||
|
type: 'boolean',
|
||||||
|
operation: 'false',
|
||||||
|
name: 'filter.operator.false',
|
||||||
|
singleValue: true,
|
||||||
|
},
|
||||||
|
'boolean:equals': { type: 'boolean', operation: 'equals', name: 'filter.operator.equals' },
|
||||||
|
'boolean:notEquals': {
|
||||||
|
type: 'boolean',
|
||||||
|
operation: 'notEquals',
|
||||||
|
name: 'filter.operator.notEquals',
|
||||||
|
},
|
||||||
|
'array:exists': {
|
||||||
|
type: 'array',
|
||||||
|
operation: 'exists',
|
||||||
|
name: 'filter.operator.exists',
|
||||||
|
singleValue: true,
|
||||||
|
},
|
||||||
|
'array:notExists': {
|
||||||
|
type: 'array',
|
||||||
|
operation: 'notExists',
|
||||||
|
name: 'filter.operator.notExists',
|
||||||
|
singleValue: true,
|
||||||
|
},
|
||||||
|
'array:empty': {
|
||||||
|
type: 'array',
|
||||||
|
operation: 'empty',
|
||||||
|
name: 'filter.operator.empty',
|
||||||
|
singleValue: true,
|
||||||
|
},
|
||||||
|
'array:notEmpty': {
|
||||||
|
type: 'array',
|
||||||
|
operation: 'notEmpty',
|
||||||
|
name: 'filter.operator.notEmpty',
|
||||||
|
singleValue: true,
|
||||||
|
},
|
||||||
|
'array:contains': {
|
||||||
|
type: 'array',
|
||||||
|
operation: 'contains',
|
||||||
|
name: 'filter.operator.contains',
|
||||||
|
rightType: 'any',
|
||||||
|
},
|
||||||
|
'array:notContains': {
|
||||||
|
type: 'array',
|
||||||
|
operation: 'notContains',
|
||||||
|
name: 'filter.operator.notContains',
|
||||||
|
rightType: 'any',
|
||||||
|
},
|
||||||
|
'array:lengthEquals': {
|
||||||
|
type: 'array',
|
||||||
|
operation: 'lengthEquals',
|
||||||
|
name: 'filter.operator.lengthEquals',
|
||||||
|
rightType: 'number',
|
||||||
|
},
|
||||||
|
'array:lengthNotEquals': {
|
||||||
|
type: 'array',
|
||||||
|
operation: 'lengthNotEquals',
|
||||||
|
name: 'filter.operator.lengthNotEquals',
|
||||||
|
rightType: 'number',
|
||||||
|
},
|
||||||
|
'array:lengthGt': {
|
||||||
|
type: 'array',
|
||||||
|
operation: 'lengthGt',
|
||||||
|
name: 'filter.operator.lengthGt',
|
||||||
|
rightType: 'number',
|
||||||
|
},
|
||||||
|
'array:lengthLt': {
|
||||||
|
type: 'array',
|
||||||
|
operation: 'lengthLt',
|
||||||
|
name: 'filter.operator.lengthLt',
|
||||||
|
rightType: 'number',
|
||||||
|
},
|
||||||
|
'array:lengthGte': {
|
||||||
|
type: 'array',
|
||||||
|
operation: 'lengthGte',
|
||||||
|
name: 'filter.operator.lengthGte',
|
||||||
|
rightType: 'number',
|
||||||
|
},
|
||||||
|
'array:lengthLte': {
|
||||||
|
type: 'array',
|
||||||
|
operation: 'lengthLte',
|
||||||
|
name: 'filter.operator.lengthLte',
|
||||||
|
rightType: 'number',
|
||||||
|
},
|
||||||
|
'object:exists': {
|
||||||
|
type: 'object',
|
||||||
|
operation: 'exists',
|
||||||
|
name: 'filter.operator.exists',
|
||||||
|
singleValue: true,
|
||||||
|
},
|
||||||
|
'object:notExists': {
|
||||||
|
type: 'object',
|
||||||
|
operation: 'notExists',
|
||||||
|
name: 'filter.operator.notExists',
|
||||||
|
singleValue: true,
|
||||||
|
},
|
||||||
|
'object:empty': {
|
||||||
|
type: 'object',
|
||||||
|
operation: 'empty',
|
||||||
|
name: 'filter.operator.empty',
|
||||||
|
singleValue: true,
|
||||||
|
},
|
||||||
|
'object:notEmpty': {
|
||||||
|
type: 'object',
|
||||||
|
operation: 'notEmpty',
|
||||||
|
name: 'filter.operator.notEmpty',
|
||||||
|
singleValue: true,
|
||||||
|
},
|
||||||
|
} as const satisfies Record<string, FilterOperator>;
|
||||||
|
|
||||||
|
export const OPERATORS = Object.values(OPERATORS_BY_ID);
|
||||||
|
|
||||||
|
export type FilterOperatorId = keyof typeof OPERATORS_BY_ID;
|
||||||
|
|
||||||
|
export const DEFAULT_OPERATOR_VALUE: FilterConditionValue['operator'] =
|
||||||
|
OPERATORS_BY_ID['string:equals'];
|
||||||
|
|
||||||
|
export const OPERATOR_GROUPS: FilterOperatorGroup[] = [
|
||||||
|
{
|
||||||
|
id: 'string',
|
||||||
|
name: 'filter.operatorGroup.string',
|
||||||
|
icon: 'font',
|
||||||
|
children: OPERATORS.filter((operator) => operator.type === 'string'),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'number',
|
||||||
|
name: 'filter.operatorGroup.number',
|
||||||
|
icon: 'hashtag',
|
||||||
|
children: OPERATORS.filter((operator) => operator.type === 'number'),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'dateTime',
|
||||||
|
name: 'filter.operatorGroup.date',
|
||||||
|
icon: 'calendar',
|
||||||
|
children: OPERATORS.filter((operator) => operator.type === 'dateTime'),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'boolean',
|
||||||
|
name: 'filter.operatorGroup.boolean',
|
||||||
|
icon: 'check-square',
|
||||||
|
children: OPERATORS.filter((operator) => operator.type === 'boolean'),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'array',
|
||||||
|
name: 'filter.operatorGroup.array',
|
||||||
|
icon: 'list',
|
||||||
|
children: OPERATORS.filter((operator) => operator.type === 'array'),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: 'object',
|
||||||
|
name: 'filter.operatorGroup.object',
|
||||||
|
icon: 'cube',
|
||||||
|
children: OPERATORS.filter((operator) => operator.type === 'object'),
|
||||||
|
},
|
||||||
|
];
|
13
packages/editor-ui/src/components/FilterConditions/types.ts
Normal file
13
packages/editor-ui/src/components/FilterConditions/types.ts
Normal file
|
@ -0,0 +1,13 @@
|
||||||
|
import type { BaseTextKey } from '@/plugins/i18n';
|
||||||
|
import type { FilterOperatorValue } from 'n8n-workflow';
|
||||||
|
|
||||||
|
export interface FilterOperator extends FilterOperatorValue {
|
||||||
|
name: BaseTextKey;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface FilterOperatorGroup {
|
||||||
|
id: string;
|
||||||
|
name: BaseTextKey;
|
||||||
|
icon?: string;
|
||||||
|
children: FilterOperator[];
|
||||||
|
}
|
|
@ -28,28 +28,32 @@
|
||||||
:class="index ? 'border-top-dashed parameter-item-wrapper ' : 'parameter-item-wrapper'"
|
:class="index ? 'border-top-dashed parameter-item-wrapper ' : 'parameter-item-wrapper'"
|
||||||
>
|
>
|
||||||
<div class="delete-option" v-if="!isReadOnly">
|
<div class="delete-option" v-if="!isReadOnly">
|
||||||
<font-awesome-icon
|
<n8n-icon-button
|
||||||
|
type="tertiary"
|
||||||
|
text
|
||||||
|
size="mini"
|
||||||
icon="trash"
|
icon="trash"
|
||||||
class="reset-icon clickable"
|
|
||||||
:title="$locale.baseText('fixedCollectionParameter.deleteItem')"
|
:title="$locale.baseText('fixedCollectionParameter.deleteItem')"
|
||||||
@click="deleteOption(property.name, index)"
|
@click="deleteOption(property.name, index)"
|
||||||
/>
|
></n8n-icon-button>
|
||||||
<div v-if="sortable" class="sort-icon">
|
<n8n-icon-button
|
||||||
<font-awesome-icon
|
v-if="sortable && index !== 0"
|
||||||
v-if="index !== 0"
|
type="tertiary"
|
||||||
icon="angle-up"
|
text
|
||||||
class="clickable"
|
size="mini"
|
||||||
:title="$locale.baseText('fixedCollectionParameter.moveUp')"
|
icon="angle-up"
|
||||||
@click="moveOptionUp(property.name, index)"
|
:title="$locale.baseText('fixedCollectionParameter.moveUp')"
|
||||||
/>
|
@click="moveOptionUp(property.name, index)"
|
||||||
<font-awesome-icon
|
></n8n-icon-button>
|
||||||
v-if="index !== mutableValues[property.name].length - 1"
|
<n8n-icon-button
|
||||||
icon="angle-down"
|
v-if="sortable && index !== mutableValues[property.name].length - 1"
|
||||||
class="clickable"
|
type="tertiary"
|
||||||
:title="$locale.baseText('fixedCollectionParameter.moveDown')"
|
text
|
||||||
@click="moveOptionDown(property.name, index)"
|
size="mini"
|
||||||
/>
|
icon="angle-down"
|
||||||
</div>
|
:title="$locale.baseText('fixedCollectionParameter.moveDown')"
|
||||||
|
@click="moveOptionDown(property.name, index)"
|
||||||
|
></n8n-icon-button>
|
||||||
</div>
|
</div>
|
||||||
<Suspense>
|
<Suspense>
|
||||||
<parameter-input-list
|
<parameter-input-list
|
||||||
|
@ -67,12 +71,14 @@
|
||||||
<div v-else class="parameter-item">
|
<div v-else class="parameter-item">
|
||||||
<div class="parameter-item-wrapper">
|
<div class="parameter-item-wrapper">
|
||||||
<div class="delete-option" v-if="!isReadOnly">
|
<div class="delete-option" v-if="!isReadOnly">
|
||||||
<font-awesome-icon
|
<n8n-icon-button
|
||||||
|
type="tertiary"
|
||||||
|
text
|
||||||
|
size="mini"
|
||||||
icon="trash"
|
icon="trash"
|
||||||
class="reset-icon clickable"
|
|
||||||
:title="$locale.baseText('fixedCollectionParameter.deleteItem')"
|
:title="$locale.baseText('fixedCollectionParameter.deleteItem')"
|
||||||
@click="deleteOption(property.name)"
|
@click="deleteOption(property.name)"
|
||||||
/>
|
></n8n-icon-button>
|
||||||
</div>
|
</div>
|
||||||
<parameter-input-list
|
<parameter-input-list
|
||||||
:parameters="property.values"
|
:parameters="property.values"
|
||||||
|
@ -87,7 +93,7 @@
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div v-if="parameterOptions.length > 0 && !isReadOnly">
|
<div class="controls" v-if="parameterOptions.length > 0 && !isReadOnly">
|
||||||
<n8n-button
|
<n8n-button
|
||||||
v-if="parameter.options.length === 1"
|
v-if="parameter.options.length === 1"
|
||||||
type="tertiary"
|
type="tertiary"
|
||||||
|
@ -346,28 +352,35 @@ export default defineComponent({
|
||||||
.fixed-collection-parameter {
|
.fixed-collection-parameter {
|
||||||
padding-left: var(--spacing-s);
|
padding-left: var(--spacing-s);
|
||||||
|
|
||||||
:deep(.button) {
|
.delete-option {
|
||||||
font-weight: var(--font-weight-normal);
|
display: flex;
|
||||||
--button-font-color: var(--color-text-dark);
|
flex-direction: column;
|
||||||
--button-border-color: var(--color-foreground-base);
|
}
|
||||||
--button-background-color: var(--color-background-base);
|
|
||||||
|
|
||||||
--button-hover-font-color: var(--color-text-dark);
|
.controls {
|
||||||
--button-hover-border-color: var(--color-foreground-base);
|
:deep(.button) {
|
||||||
--button-hover-background-color: var(--color-background-base);
|
font-weight: var(--font-weight-normal);
|
||||||
|
--button-font-color: var(--color-text-dark);
|
||||||
|
--button-border-color: var(--color-foreground-base);
|
||||||
|
--button-background-color: var(--color-background-base);
|
||||||
|
|
||||||
--button-active-font-color: var(--color-text-dark);
|
--button-hover-font-color: var(--color-text-dark);
|
||||||
--button-active-border-color: var(--color-foreground-base);
|
--button-hover-border-color: var(--color-foreground-base);
|
||||||
--button-active-background-color: var(--color-background-base);
|
--button-hover-background-color: var(--color-background-base);
|
||||||
|
|
||||||
--button-focus-font-color: var(--color-text-dark);
|
--button-active-font-color: var(--color-text-dark);
|
||||||
--button-focus-border-color: var(--color-foreground-base);
|
--button-active-border-color: var(--color-foreground-base);
|
||||||
--button-focus-background-color: var(--color-background-base);
|
--button-active-background-color: var(--color-background-base);
|
||||||
|
|
||||||
&:active,
|
--button-focus-font-color: var(--color-text-dark);
|
||||||
&.active,
|
--button-focus-border-color: var(--color-foreground-base);
|
||||||
&:focus {
|
--button-focus-background-color: var(--color-background-base);
|
||||||
outline: none;
|
|
||||||
|
&:active,
|
||||||
|
&.active,
|
||||||
|
&:focus {
|
||||||
|
outline: none;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -376,19 +389,8 @@ export default defineComponent({
|
||||||
margin: var(--spacing-xs) 0;
|
margin: var(--spacing-xs) 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.delete-option {
|
|
||||||
display: none;
|
|
||||||
position: absolute;
|
|
||||||
z-index: 999;
|
|
||||||
color: #f56c6c;
|
|
||||||
left: 0;
|
|
||||||
top: 0.5em;
|
|
||||||
width: 15px;
|
|
||||||
height: 100%;
|
|
||||||
}
|
|
||||||
|
|
||||||
.parameter-item:hover > .parameter-item-wrapper > .delete-option {
|
.parameter-item:hover > .parameter-item-wrapper > .delete-option {
|
||||||
display: block;
|
opacity: 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
.parameter-item {
|
.parameter-item {
|
||||||
|
@ -411,11 +413,4 @@ export default defineComponent({
|
||||||
.no-items-exist {
|
.no-items-exist {
|
||||||
margin: var(--spacing-xs) 0;
|
margin: var(--spacing-xs) 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.sort-icon {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
margin-left: 1px;
|
|
||||||
margin-top: 0.5em;
|
|
||||||
}
|
|
||||||
</style>
|
</style>
|
||||||
|
|
|
@ -27,9 +27,17 @@ export const inputTheme = ({ isSingleLine } = { isSingleLine: false }) => {
|
||||||
borderWidth: 'var(--border-width-base)',
|
borderWidth: 'var(--border-width-base)',
|
||||||
borderStyle: 'var(--input-border-style, var(--border-style-base))',
|
borderStyle: 'var(--input-border-style, var(--border-style-base))',
|
||||||
borderColor: 'var(--input-border-color, var(--border-color-base))',
|
borderColor: 'var(--input-border-color, var(--border-color-base))',
|
||||||
|
borderRightColor:
|
||||||
|
'var(--input-border-right-color,var(--input-border-color, var(--border-color-base)))',
|
||||||
|
borderBottomColor:
|
||||||
|
'var(--input-border-bottom-color,var(--input-border-color, var(--border-color-base)))',
|
||||||
borderRadius: 'var(--input-border-radius, var(--border-radius-base))',
|
borderRadius: 'var(--input-border-radius, var(--border-radius-base))',
|
||||||
borderTopLeftRadius: '0',
|
borderTopLeftRadius: 0,
|
||||||
borderBottomLeftRadius: '0',
|
borderTopRightRadius:
|
||||||
|
'var(--input-border-top-right-radius, var(--input-border-radius, var(--border-radius-base)))',
|
||||||
|
borderBottomLeftRadius: 0,
|
||||||
|
borderBottomRightRadius:
|
||||||
|
'var(--input-border-bottom-right-radius, var(--input-border-radius, var(--border-radius-base)))',
|
||||||
backgroundColor: 'white',
|
backgroundColor: 'white',
|
||||||
},
|
},
|
||||||
'.cm-scroller': {
|
'.cm-scroller': {
|
||||||
|
|
|
@ -45,6 +45,7 @@
|
||||||
:modelValue="expressionDisplayValue"
|
:modelValue="expressionDisplayValue"
|
||||||
:title="displayTitle"
|
:title="displayTitle"
|
||||||
:isReadOnly="isReadOnly"
|
:isReadOnly="isReadOnly"
|
||||||
|
:isSingleLine="isSingleLine"
|
||||||
:path="path"
|
:path="path"
|
||||||
:additional-expression-data="additionalExpressionData"
|
:additional-expression-data="additionalExpressionData"
|
||||||
:class="{ 'ph-no-capture': shouldRedactValue }"
|
:class="{ 'ph-no-capture': shouldRedactValue }"
|
||||||
|
@ -209,6 +210,7 @@
|
||||||
v-model="tempValue"
|
v-model="tempValue"
|
||||||
ref="inputField"
|
ref="inputField"
|
||||||
type="datetime"
|
type="datetime"
|
||||||
|
valueFormat="YYYY-MM-DDTHH:mm:ss"
|
||||||
:size="inputSize"
|
:size="inputSize"
|
||||||
:modelValue="displayValue"
|
:modelValue="displayValue"
|
||||||
:title="displayTitle"
|
:title="displayTitle"
|
||||||
|
@ -447,6 +449,9 @@ export default defineComponent({
|
||||||
isReadOnly: {
|
isReadOnly: {
|
||||||
type: Boolean,
|
type: Boolean,
|
||||||
},
|
},
|
||||||
|
isSingleLine: {
|
||||||
|
type: Boolean,
|
||||||
|
},
|
||||||
parameter: {
|
parameter: {
|
||||||
type: Object as PropType<INodeProperties>,
|
type: Object as PropType<INodeProperties>,
|
||||||
},
|
},
|
||||||
|
|
|
@ -1,16 +1,17 @@
|
||||||
<template>
|
<template>
|
||||||
<n8n-input-label
|
<n8n-input-label
|
||||||
|
:class="$style.wrapper"
|
||||||
:label="hideLabel ? '' : i18n.nodeText().inputLabelDisplayName(parameter, path)"
|
:label="hideLabel ? '' : i18n.nodeText().inputLabelDisplayName(parameter, path)"
|
||||||
:tooltipText="hideLabel ? '' : i18n.nodeText().inputLabelDescription(parameter, path)"
|
:tooltipText="hideLabel ? '' : i18n.nodeText().inputLabelDescription(parameter, path)"
|
||||||
:showTooltip="focused"
|
:showTooltip="focused"
|
||||||
:showOptions="menuExpanded || focused || forceShowExpression"
|
:showOptions="menuExpanded || focused || forceShowExpression"
|
||||||
|
:optionsPosition="optionsPosition"
|
||||||
:bold="false"
|
:bold="false"
|
||||||
:size="label.size"
|
:size="label.size"
|
||||||
color="text-dark"
|
color="text-dark"
|
||||||
>
|
>
|
||||||
<template #options>
|
<template v-if="displayOptions && optionsPosition === 'top'" #options>
|
||||||
<parameter-options
|
<parameter-options
|
||||||
v-if="displayOptions"
|
|
||||||
:parameter="parameter"
|
:parameter="parameter"
|
||||||
:value="value"
|
:value="value"
|
||||||
:isReadOnly="isReadOnly"
|
:isReadOnly="isReadOnly"
|
||||||
|
@ -48,10 +49,12 @@
|
||||||
:modelValue="value"
|
:modelValue="value"
|
||||||
:path="path"
|
:path="path"
|
||||||
:isReadOnly="isReadOnly"
|
:isReadOnly="isReadOnly"
|
||||||
|
:isSingleLine="isSingleLine"
|
||||||
:droppable="droppable"
|
:droppable="droppable"
|
||||||
:activeDrop="activeDrop"
|
:activeDrop="activeDrop"
|
||||||
:forceShowExpression="forceShowExpression"
|
:forceShowExpression="forceShowExpression"
|
||||||
:hint="hint"
|
:hint="hint"
|
||||||
|
:hideHint="hideHint"
|
||||||
:hide-issues="hideIssues"
|
:hide-issues="hideIssues"
|
||||||
:label="label"
|
:label="label"
|
||||||
:event-bus="eventBus"
|
:event-bus="eventBus"
|
||||||
|
@ -65,6 +68,23 @@
|
||||||
</n8n-tooltip>
|
</n8n-tooltip>
|
||||||
</template>
|
</template>
|
||||||
</draggable-target>
|
</draggable-target>
|
||||||
|
<div
|
||||||
|
:class="{
|
||||||
|
[$style.options]: true,
|
||||||
|
[$style.visible]: menuExpanded || focused || forceShowExpression,
|
||||||
|
}"
|
||||||
|
>
|
||||||
|
<parameter-options
|
||||||
|
v-if="optionsPosition === 'bottom'"
|
||||||
|
:parameter="parameter"
|
||||||
|
:value="value"
|
||||||
|
:isReadOnly="isReadOnly"
|
||||||
|
:showOptions="displayOptions"
|
||||||
|
:showExpressionSelector="showExpressionSelector"
|
||||||
|
@update:modelValue="optionSelected"
|
||||||
|
@menu-expanded="onMenuExpanded"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
</n8n-input-label>
|
</n8n-input-label>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
|
@ -83,10 +103,10 @@ import { hasExpressionMapping, hasOnlyListMode, isValueExpression } from '@/util
|
||||||
import { isResourceLocatorValue } from '@/utils/typeGuards';
|
import { isResourceLocatorValue } from '@/utils/typeGuards';
|
||||||
import ParameterInputWrapper from '@/components/ParameterInputWrapper.vue';
|
import ParameterInputWrapper from '@/components/ParameterInputWrapper.vue';
|
||||||
import type {
|
import type {
|
||||||
INodeParameters,
|
|
||||||
INodeProperties,
|
INodeProperties,
|
||||||
INodePropertyMode,
|
INodePropertyMode,
|
||||||
IParameterLabel,
|
IParameterLabel,
|
||||||
|
NodeParameterValueType,
|
||||||
} from 'n8n-workflow';
|
} from 'n8n-workflow';
|
||||||
import type { BaseTextKey } from '@/plugins/i18n';
|
import type { BaseTextKey } from '@/plugins/i18n';
|
||||||
import { useNDVStore } from '@/stores/ndv.store';
|
import { useNDVStore } from '@/stores/ndv.store';
|
||||||
|
@ -127,10 +147,22 @@ export default defineComponent({
|
||||||
type: Boolean,
|
type: Boolean,
|
||||||
default: false,
|
default: false,
|
||||||
},
|
},
|
||||||
|
optionsPosition: {
|
||||||
|
type: String as PropType<'bottom' | 'top'>,
|
||||||
|
default: 'top',
|
||||||
|
},
|
||||||
|
hideHint: {
|
||||||
|
type: Boolean,
|
||||||
|
default: false,
|
||||||
|
},
|
||||||
isReadOnly: {
|
isReadOnly: {
|
||||||
type: Boolean,
|
type: Boolean,
|
||||||
default: false,
|
default: false,
|
||||||
},
|
},
|
||||||
|
isSingleLine: {
|
||||||
|
type: Boolean,
|
||||||
|
default: false,
|
||||||
|
},
|
||||||
hideLabel: {
|
hideLabel: {
|
||||||
type: Boolean,
|
type: Boolean,
|
||||||
default: false,
|
default: false,
|
||||||
|
@ -146,7 +178,7 @@ export default defineComponent({
|
||||||
type: String,
|
type: String,
|
||||||
},
|
},
|
||||||
value: {
|
value: {
|
||||||
type: [Number, String, Boolean, Array, Object] as PropType<INodeParameters>,
|
type: [Number, String, Boolean, Array, Object] as PropType<NodeParameterValueType>,
|
||||||
},
|
},
|
||||||
label: {
|
label: {
|
||||||
type: Object as PropType<IParameterLabel>,
|
type: Object as PropType<IParameterLabel>,
|
||||||
|
@ -336,3 +368,26 @@ export default defineComponent({
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
<style module>
|
||||||
|
.wrapper {
|
||||||
|
position: relative;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
.options {
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.options {
|
||||||
|
position: absolute;
|
||||||
|
bottom: -22px;
|
||||||
|
right: 0;
|
||||||
|
opacity: 0;
|
||||||
|
transition: opacity 100ms ease-in;
|
||||||
|
|
||||||
|
&.visible {
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
|
@ -48,18 +48,16 @@
|
||||||
v-else-if="['collection', 'fixedCollection'].includes(parameter.type)"
|
v-else-if="['collection', 'fixedCollection'].includes(parameter.type)"
|
||||||
class="multi-parameter"
|
class="multi-parameter"
|
||||||
>
|
>
|
||||||
<div
|
<n8n-icon-button
|
||||||
class="delete-option clickable"
|
|
||||||
:title="$locale.baseText('parameterInputList.delete')"
|
|
||||||
v-if="hideDelete !== true && !isReadOnly"
|
v-if="hideDelete !== true && !isReadOnly"
|
||||||
>
|
type="tertiary"
|
||||||
<font-awesome-icon
|
text
|
||||||
icon="trash"
|
size="mini"
|
||||||
class="reset-icon clickable"
|
icon="trash"
|
||||||
:title="$locale.baseText('parameterInputList.parameterOptions')"
|
class="delete-option"
|
||||||
@click="deleteOption(parameter.name)"
|
:title="$locale.baseText('parameterInputList.delete')"
|
||||||
/>
|
@click="deleteOption(parameter.name)"
|
||||||
</div>
|
></n8n-icon-button>
|
||||||
<n8n-input-label
|
<n8n-input-label
|
||||||
:label="$locale.nodeText().inputLabelDisplayName(parameter, path)"
|
:label="$locale.nodeText().inputLabelDisplayName(parameter, path)"
|
||||||
:tooltipText="$locale.nodeText().inputLabelDescription(parameter, path)"
|
:tooltipText="$locale.nodeText().inputLabelDescription(parameter, path)"
|
||||||
|
@ -98,22 +96,28 @@
|
||||||
labelSize="small"
|
labelSize="small"
|
||||||
@valueChanged="valueChanged"
|
@valueChanged="valueChanged"
|
||||||
/>
|
/>
|
||||||
|
<FilterConditions
|
||||||
|
v-else-if="parameter.type === 'filter'"
|
||||||
|
:parameter="parameter"
|
||||||
|
:value="nodeHelpers.getParameterValue(nodeValues, parameter.name, path)"
|
||||||
|
:path="getPath(parameter.name)"
|
||||||
|
:node="node"
|
||||||
|
@valueChanged="valueChanged"
|
||||||
|
/>
|
||||||
<div
|
<div
|
||||||
v-else-if="displayNodeParameter(parameter) && credentialsParameterIndex !== index"
|
v-else-if="displayNodeParameter(parameter) && credentialsParameterIndex !== index"
|
||||||
class="parameter-item"
|
class="parameter-item"
|
||||||
>
|
>
|
||||||
<div
|
<n8n-icon-button
|
||||||
class="delete-option clickable"
|
|
||||||
:title="$locale.baseText('parameterInputList.delete')"
|
|
||||||
v-if="hideDelete !== true && !isReadOnly"
|
v-if="hideDelete !== true && !isReadOnly"
|
||||||
>
|
type="tertiary"
|
||||||
<font-awesome-icon
|
text
|
||||||
icon="trash"
|
size="mini"
|
||||||
class="reset-icon clickable"
|
icon="trash"
|
||||||
:title="$locale.baseText('parameterInputList.deleteParameter')"
|
class="delete-option"
|
||||||
@click="deleteOption(parameter.name)"
|
:title="$locale.baseText('parameterInputList.delete')"
|
||||||
/>
|
@click="deleteOption(parameter.name)"
|
||||||
</div>
|
></n8n-icon-button>
|
||||||
|
|
||||||
<parameter-input-full
|
<parameter-input-full
|
||||||
:parameter="parameter"
|
:parameter="parameter"
|
||||||
|
@ -153,6 +157,7 @@ import ImportParameter from '@/components/ImportParameter.vue';
|
||||||
import MultipleParameter from '@/components/MultipleParameter.vue';
|
import MultipleParameter from '@/components/MultipleParameter.vue';
|
||||||
import ParameterInputFull from '@/components/ParameterInputFull.vue';
|
import ParameterInputFull from '@/components/ParameterInputFull.vue';
|
||||||
import ResourceMapper from '@/components/ResourceMapper/ResourceMapper.vue';
|
import ResourceMapper from '@/components/ResourceMapper/ResourceMapper.vue';
|
||||||
|
import Conditions from '@/components/FilterConditions/FilterConditions.vue';
|
||||||
import { KEEP_AUTH_IN_NDV_FOR_NODES } from '@/constants';
|
import { KEEP_AUTH_IN_NDV_FOR_NODES } from '@/constants';
|
||||||
import { workflowHelpers } from '@/mixins/workflowHelpers';
|
import { workflowHelpers } from '@/mixins/workflowHelpers';
|
||||||
import { useNDVStore } from '@/stores/ndv.store';
|
import { useNDVStore } from '@/stores/ndv.store';
|
||||||
|
@ -181,6 +186,7 @@ export default defineComponent({
|
||||||
CollectionParameter,
|
CollectionParameter,
|
||||||
ImportParameter,
|
ImportParameter,
|
||||||
ResourceMapper,
|
ResourceMapper,
|
||||||
|
FilterConditions: Conditions,
|
||||||
},
|
},
|
||||||
setup() {
|
setup() {
|
||||||
const nodeHelpers = useNodeHelpers();
|
const nodeHelpers = useNodeHelpers();
|
||||||
|
@ -519,15 +525,11 @@ export default defineComponent({
|
||||||
<style lang="scss">
|
<style lang="scss">
|
||||||
.parameter-input-list-wrapper {
|
.parameter-input-list-wrapper {
|
||||||
.delete-option {
|
.delete-option {
|
||||||
display: none;
|
|
||||||
position: absolute;
|
position: absolute;
|
||||||
z-index: 999;
|
opacity: 0;
|
||||||
color: #f56c6c;
|
top: 0;
|
||||||
font-size: var(--font-size-2xs);
|
left: calc(-1 * var(--spacing-2xs));
|
||||||
|
transition: opacity 100ms ease-in;
|
||||||
&:hover {
|
|
||||||
color: #ff0000;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.indent > div {
|
.indent > div {
|
||||||
|
@ -538,11 +540,6 @@ export default defineComponent({
|
||||||
position: relative;
|
position: relative;
|
||||||
margin: var(--spacing-xs) 0;
|
margin: var(--spacing-xs) 0;
|
||||||
|
|
||||||
.delete-option {
|
|
||||||
top: 0;
|
|
||||||
left: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.parameter-info {
|
.parameter-info {
|
||||||
display: none;
|
display: none;
|
||||||
}
|
}
|
||||||
|
@ -551,15 +548,10 @@ export default defineComponent({
|
||||||
.parameter-item {
|
.parameter-item {
|
||||||
position: relative;
|
position: relative;
|
||||||
margin: var(--spacing-xs) 0;
|
margin: var(--spacing-xs) 0;
|
||||||
|
|
||||||
> .delete-option {
|
|
||||||
top: var(--spacing-5xs);
|
|
||||||
left: 0;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
.parameter-item:hover > .delete-option,
|
.parameter-item:hover > .delete-option,
|
||||||
.multi-parameter:hover > .delete-option {
|
.multi-parameter:hover > .delete-option {
|
||||||
display: block;
|
opacity: 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
.parameter-notice {
|
.parameter-notice {
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
<template>
|
<template>
|
||||||
<div data-test-id="parameter-input">
|
<div :class="$style.parameterInput" data-test-id="parameter-input">
|
||||||
<parameter-input
|
<parameter-input
|
||||||
ref="param"
|
ref="param"
|
||||||
:inputSize="inputSize"
|
:inputSize="inputSize"
|
||||||
|
@ -18,6 +18,7 @@
|
||||||
:expressionEvaluated="expressionValueComputed"
|
:expressionEvaluated="expressionValueComputed"
|
||||||
:additionalExpressionData="resolvedAdditionalExpressionData"
|
:additionalExpressionData="resolvedAdditionalExpressionData"
|
||||||
:label="label"
|
:label="label"
|
||||||
|
:isSingleLine="isSingleLine"
|
||||||
:data-test-id="`parameter-input-${parsedParameterName}`"
|
:data-test-id="`parameter-input-${parsedParameterName}`"
|
||||||
:event-bus="eventBus"
|
:event-bus="eventBus"
|
||||||
@focus="onFocus"
|
@focus="onFocus"
|
||||||
|
@ -26,20 +27,20 @@
|
||||||
@textInput="onTextInput"
|
@textInput="onTextInput"
|
||||||
@update="onValueChanged"
|
@update="onValueChanged"
|
||||||
/>
|
/>
|
||||||
<input-hint
|
<div v-if="!hideHint && (expressionOutput || parameterHint)" :class="$style.hint">
|
||||||
v-if="expressionOutput"
|
<div>
|
||||||
:class="{ [$style.hint]: true, 'ph-no-capture': isForCredential }"
|
<input-hint
|
||||||
data-test-id="parameter-expression-preview"
|
v-if="expressionOutput"
|
||||||
:highlight="!!(expressionOutput && targetItem) && isInputParentOfActiveNode"
|
:class="{ [$style.hint]: true, 'ph-no-capture': isForCredential }"
|
||||||
:hint="expressionOutput"
|
:data-test-id="`parameter-expression-preview-${parsedParameterName}`"
|
||||||
:singleLine="true"
|
:highlight="!!(expressionOutput && targetItem) && isInputParentOfActiveNode"
|
||||||
/>
|
:hint="expressionOutput"
|
||||||
<input-hint
|
:singleLine="true"
|
||||||
v-else-if="parameterHint"
|
/>
|
||||||
:class="$style.hint"
|
<input-hint v-else-if="parameterHint" :renderHTML="true" :hint="parameterHint" />
|
||||||
:renderHTML="true"
|
</div>
|
||||||
:hint="parameterHint"
|
<slot v-if="$slots.options" name="options" />
|
||||||
/>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
|
@ -84,6 +85,9 @@ export default defineComponent({
|
||||||
isReadOnly: {
|
isReadOnly: {
|
||||||
type: Boolean,
|
type: Boolean,
|
||||||
},
|
},
|
||||||
|
isSingleLine: {
|
||||||
|
type: Boolean,
|
||||||
|
},
|
||||||
parameter: {
|
parameter: {
|
||||||
type: Object as PropType<INodeProperties>,
|
type: Object as PropType<INodeProperties>,
|
||||||
},
|
},
|
||||||
|
@ -106,6 +110,10 @@ export default defineComponent({
|
||||||
type: String,
|
type: String,
|
||||||
required: false,
|
required: false,
|
||||||
},
|
},
|
||||||
|
hideHint: {
|
||||||
|
type: Boolean,
|
||||||
|
required: false,
|
||||||
|
},
|
||||||
inputSize: {
|
inputSize: {
|
||||||
type: String,
|
type: String,
|
||||||
},
|
},
|
||||||
|
@ -252,8 +260,10 @@ export default defineComponent({
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style lang="scss" module>
|
<style lang="scss" module>
|
||||||
.hint {
|
.parameterInput {
|
||||||
margin-top: var(--spacing-4xs);
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: var(--spacing-4xs);
|
||||||
}
|
}
|
||||||
|
|
||||||
.hovering {
|
.hovering {
|
||||||
|
|
|
@ -92,7 +92,7 @@
|
||||||
v-if="isValueExpression || forceShowExpression"
|
v-if="isValueExpression || forceShowExpression"
|
||||||
:modelValue="expressionDisplayValue"
|
:modelValue="expressionDisplayValue"
|
||||||
:path="path"
|
:path="path"
|
||||||
isForRecordLocator
|
isSingleLine
|
||||||
@update:modelValue="onInputChange"
|
@update:modelValue="onInputChange"
|
||||||
@modalOpenerClick="$emit('modalOpenerClick')"
|
@modalOpenerClick="$emit('modalOpenerClick')"
|
||||||
ref="input"
|
ref="input"
|
||||||
|
|
|
@ -341,10 +341,14 @@ defineExpose({
|
||||||
props.showMatchingColumnsSelector,
|
props.showMatchingColumnsSelector,
|
||||||
)
|
)
|
||||||
"
|
"
|
||||||
:class="['delete-option', 'clickable', 'mt-5xs']"
|
:class="['delete-option', 'mt-5xs']"
|
||||||
>
|
>
|
||||||
<font-awesome-icon
|
<n8n-icon-button
|
||||||
|
type="tertiary"
|
||||||
|
text
|
||||||
|
size="mini"
|
||||||
icon="trash"
|
icon="trash"
|
||||||
|
:data-test-id="`remove-field-button-${getParsedFieldName(field.name)}`"
|
||||||
:title="
|
:title="
|
||||||
locale.baseText('resourceMapper.removeField', {
|
locale.baseText('resourceMapper.removeField', {
|
||||||
interpolate: {
|
interpolate: {
|
||||||
|
@ -352,9 +356,8 @@ defineExpose({
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
"
|
"
|
||||||
:data-test-id="`remove-field-button-${getParsedFieldName(field.name)}`"
|
|
||||||
@click="removeField(field.name)"
|
@click="removeField(field.name)"
|
||||||
/>
|
></n8n-icon-button>
|
||||||
</div>
|
</div>
|
||||||
<div :class="$style.parameterInput">
|
<div :class="$style.parameterInput">
|
||||||
<parameter-input-full
|
<parameter-input-full
|
||||||
|
|
|
@ -0,0 +1,250 @@
|
||||||
|
import { createComponentRenderer } from '@/__tests__/render';
|
||||||
|
import { SETTINGS_STORE_DEFAULT_STATE } from '@/__tests__/utils';
|
||||||
|
import FilterConditions from '@/components/FilterConditions/FilterConditions.vue';
|
||||||
|
import { STORES } from '@/constants';
|
||||||
|
import { useNDVStore } from '@/stores/ndv.store';
|
||||||
|
import { createTestingPinia } from '@pinia/testing';
|
||||||
|
import userEvent from '@testing-library/user-event';
|
||||||
|
|
||||||
|
const DEFAULT_SETUP = {
|
||||||
|
pinia: createTestingPinia({
|
||||||
|
initialState: {
|
||||||
|
[STORES.SETTINGS]: SETTINGS_STORE_DEFAULT_STATE,
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
props: {
|
||||||
|
path: 'parameters.conditions',
|
||||||
|
node: {
|
||||||
|
parameters: {},
|
||||||
|
id: 'f63efb2d-3cc5-4500-89f9-b39aab19baf5',
|
||||||
|
name: 'If',
|
||||||
|
type: 'n8n-nodes-base.if',
|
||||||
|
typeVersion: 2,
|
||||||
|
position: [1120, 380],
|
||||||
|
credentials: {},
|
||||||
|
disabled: false,
|
||||||
|
},
|
||||||
|
parameter: { name: 'conditions', displayName: 'Conditions' },
|
||||||
|
value: {},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
const renderComponent = createComponentRenderer(FilterConditions, DEFAULT_SETUP);
|
||||||
|
|
||||||
|
describe('Filter.vue', () => {
|
||||||
|
afterEach(() => {
|
||||||
|
vi.clearAllMocks();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('renders default state properly', async () => {
|
||||||
|
const { getByTestId, queryByTestId, findAllByTestId } = renderComponent();
|
||||||
|
expect(getByTestId('filter-conditions')).toBeInTheDocument();
|
||||||
|
expect(await findAllByTestId('filter-condition')).toHaveLength(1);
|
||||||
|
expect(getByTestId('filter-condition-left')).toBeInTheDocument();
|
||||||
|
expect(getByTestId('filter-operator-select')).toBeInTheDocument();
|
||||||
|
|
||||||
|
// Only visible when multiple conditions
|
||||||
|
expect(queryByTestId('filter-combinator-select')).not.toBeInTheDocument();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('renders conditions with different operators', async () => {
|
||||||
|
const { getByTestId, findAllByTestId } = renderComponent({
|
||||||
|
...DEFAULT_SETUP,
|
||||||
|
props: {
|
||||||
|
...DEFAULT_SETUP.props,
|
||||||
|
value: {
|
||||||
|
options: {
|
||||||
|
caseSensitive: true,
|
||||||
|
leftValue: '',
|
||||||
|
},
|
||||||
|
conditions: [
|
||||||
|
{
|
||||||
|
leftValue: '={{ $json.tags }}',
|
||||||
|
rightValue: 'exotic',
|
||||||
|
operator: {
|
||||||
|
type: 'array',
|
||||||
|
operation: 'contains',
|
||||||
|
rightType: 'any',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
leftValue: '={{ $json.meta }}',
|
||||||
|
rightValue: '',
|
||||||
|
operator: {
|
||||||
|
type: 'object',
|
||||||
|
operation: 'notEmpty',
|
||||||
|
singleValue: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
leftValue: '={{ $json.test }}',
|
||||||
|
rightValue: 'other',
|
||||||
|
operator: {
|
||||||
|
type: 'string',
|
||||||
|
operation: 'equals',
|
||||||
|
singleValue: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
combinator: 'or',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
expect(getByTestId('filter-conditions')).toBeInTheDocument();
|
||||||
|
const conditions = await findAllByTestId('filter-condition');
|
||||||
|
const combinators = await findAllByTestId('filter-combinator-select');
|
||||||
|
|
||||||
|
expect(combinators).toHaveLength(2);
|
||||||
|
expect(combinators[0].querySelector('input')?.value).toEqual('OR');
|
||||||
|
expect(combinators[1]).toHaveTextContent('OR');
|
||||||
|
|
||||||
|
expect(conditions).toHaveLength(3);
|
||||||
|
expect(conditions[0].querySelector('[data-test-id="filter-condition-left"]')).toHaveTextContent(
|
||||||
|
'{{ $json.tags }}',
|
||||||
|
);
|
||||||
|
expect(
|
||||||
|
conditions[0].querySelector('[data-test-id="filter-operator-select"]')?.querySelector('input')
|
||||||
|
?.value,
|
||||||
|
).toEqual('contains');
|
||||||
|
expect(
|
||||||
|
conditions[0].querySelector('[data-test-id="filter-condition-right"]')?.querySelector('input')
|
||||||
|
?.value,
|
||||||
|
).toEqual('exotic');
|
||||||
|
|
||||||
|
expect(conditions[1].querySelector('[data-test-id="filter-condition-left"]')).toHaveTextContent(
|
||||||
|
'{{ $json.meta }}',
|
||||||
|
);
|
||||||
|
expect(
|
||||||
|
conditions[1].querySelector('[data-test-id="filter-operator-select"]')?.querySelector('input')
|
||||||
|
?.value,
|
||||||
|
).toEqual('is not empty');
|
||||||
|
expect(conditions[1].querySelector('[data-test-id="filter-condition-right"]')).toBeNull();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('renders parameter issues', async () => {
|
||||||
|
const ndvStore = useNDVStore();
|
||||||
|
vi.spyOn(ndvStore, 'activeNode', 'get').mockReturnValue({
|
||||||
|
...DEFAULT_SETUP.props.node,
|
||||||
|
issues: { parameters: { 'conditions.1': ['not a number sir'] } },
|
||||||
|
} as never);
|
||||||
|
|
||||||
|
const { getByTestId } = renderComponent({
|
||||||
|
props: {
|
||||||
|
...DEFAULT_SETUP.props,
|
||||||
|
value: {
|
||||||
|
conditions: [
|
||||||
|
{
|
||||||
|
leftValue: '={{ $json.num }}',
|
||||||
|
rightValue: '5',
|
||||||
|
operator: {
|
||||||
|
type: 'number',
|
||||||
|
operation: 'equals',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
leftValue: '={{ $json.num }}',
|
||||||
|
rightValue: 'not a number',
|
||||||
|
operator: {
|
||||||
|
type: 'number',
|
||||||
|
operation: 'equals',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(getByTestId('parameter-issues')).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('renders correctly with typeOptions.leftValue', async () => {
|
||||||
|
const { findAllByTestId } = renderComponent({
|
||||||
|
props: {
|
||||||
|
...DEFAULT_SETUP.props,
|
||||||
|
parameter: {
|
||||||
|
typeOptions: {
|
||||||
|
filter: { leftValue: 'leftValue is always this' },
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const conditions = await findAllByTestId('filter-condition');
|
||||||
|
expect(conditions[0].querySelector('[data-test-id="filter-condition-left"]')).toBeNull();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('renders correctly with typeOptions.allowedCombinators', async () => {
|
||||||
|
const { getByTestId } = renderComponent({
|
||||||
|
props: {
|
||||||
|
...DEFAULT_SETUP.props,
|
||||||
|
value: {
|
||||||
|
conditions: [
|
||||||
|
{
|
||||||
|
leftValue: 'foo',
|
||||||
|
operator: { type: 'string', operation: 'equals' },
|
||||||
|
rightValue: 'bar',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
leftValue: 'foo',
|
||||||
|
operator: { type: 'string', operation: 'equals' },
|
||||||
|
rightValue: 'bar',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
parameter: {
|
||||||
|
typeOptions: {
|
||||||
|
filter: { allowedCombinators: ['or'] },
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(getByTestId('filter-combinator-select')).toHaveTextContent('OR');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('renders correctly with typeOptions.maxConditions', async () => {
|
||||||
|
const { getByTestId } = renderComponent({
|
||||||
|
props: {
|
||||||
|
...DEFAULT_SETUP.props,
|
||||||
|
value: {
|
||||||
|
conditions: [
|
||||||
|
{
|
||||||
|
leftValue: 'foo',
|
||||||
|
operator: { type: 'string', operation: 'equals' },
|
||||||
|
rightValue: 'bar',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
leftValue: 'foo',
|
||||||
|
operator: { type: 'string', operation: 'equals' },
|
||||||
|
rightValue: 'bar',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
parameter: {
|
||||||
|
typeOptions: {
|
||||||
|
filter: { maxConditions: 2 },
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(getByTestId('filter-add-condition')).toBeDisabled();
|
||||||
|
expect(getByTestId('filter-add-condition').title).toEqual('Maximum conditions reached');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('can add and remove conditions', async () => {
|
||||||
|
const { getByTestId, findAllByTestId } = renderComponent(DEFAULT_SETUP);
|
||||||
|
await userEvent.click(getByTestId('filter-add-condition'));
|
||||||
|
|
||||||
|
let conditions = await findAllByTestId('filter-condition');
|
||||||
|
expect(conditions.length).toEqual(2);
|
||||||
|
|
||||||
|
const removeButton = conditions[0].querySelector('[data-test-id="filter-remove-condition"]');
|
||||||
|
|
||||||
|
await userEvent.click(removeButton as Element);
|
||||||
|
|
||||||
|
conditions = await findAllByTestId('filter-condition');
|
||||||
|
expect(conditions.length).toEqual(1);
|
||||||
|
expect(conditions[0].querySelector('[data-test-id="filter-remove-condition"]')).toBeNull();
|
||||||
|
});
|
||||||
|
});
|
|
@ -2289,6 +2289,52 @@
|
||||||
"executionUsage.button.upgrade": "Upgrade plan",
|
"executionUsage.button.upgrade": "Upgrade plan",
|
||||||
"executionUsage.expired.text": "Your trial is over. Upgrade now to keep your data.",
|
"executionUsage.expired.text": "Your trial is over. Upgrade now to keep your data.",
|
||||||
"executionUsage.ranOutOfExecutions.text": "You’re out of executions. Upgrade your plan to keep automating.",
|
"executionUsage.ranOutOfExecutions.text": "You’re out of executions. Upgrade your plan to keep automating.",
|
||||||
|
"filter.operatorGroup.basic": "Basic",
|
||||||
|
"filter.operatorGroup.string": "String",
|
||||||
|
"filter.operatorGroup.number": "Number",
|
||||||
|
"filter.operatorGroup.date": "Date & Time",
|
||||||
|
"filter.operatorGroup.boolean": "Boolean",
|
||||||
|
"filter.operatorGroup.array": "Array",
|
||||||
|
"filter.operatorGroup.object": "Object",
|
||||||
|
"filter.operator.equals": "is equal to",
|
||||||
|
"filter.operator.notEquals": "is not equal to",
|
||||||
|
"filter.operator.contains": "contains",
|
||||||
|
"filter.operator.notContains": "does not contain",
|
||||||
|
"filter.operator.startsWith": "starts with",
|
||||||
|
"filter.operator.notStartsWith": "does not start with",
|
||||||
|
"filter.operator.endsWith": "ends with",
|
||||||
|
"filter.operator.notEndsWith": "does not end with",
|
||||||
|
"filter.operator.exists": "exists",
|
||||||
|
"filter.operator.notExists": "does not exist",
|
||||||
|
"filter.operator.regex": "matches regex",
|
||||||
|
"filter.operator.notRegex": "does not match regex",
|
||||||
|
"filter.operator.gt": "is greater than",
|
||||||
|
"filter.operator.lt": "is less than",
|
||||||
|
"filter.operator.gte": "is greater than or equal",
|
||||||
|
"filter.operator.lte": "is less than or equal",
|
||||||
|
"filter.operator.after": "is after",
|
||||||
|
"filter.operator.before": "is before",
|
||||||
|
"filter.operator.afterOrEquals": "is after or equal",
|
||||||
|
"filter.operator.beforeOrEquals": "is before or equal",
|
||||||
|
"filter.operator.true": "is true",
|
||||||
|
"filter.operator.false": "is false",
|
||||||
|
"filter.operator.lengthEquals": "length equal to",
|
||||||
|
"filter.operator.lengthNotEquals": "length not equal to",
|
||||||
|
"filter.operator.lengthGt": "length greater than",
|
||||||
|
"filter.operator.lengthLt": "length less than",
|
||||||
|
"filter.operator.lengthGte": "length greater than or equal",
|
||||||
|
"filter.operator.lengthLte": "length less than or equal",
|
||||||
|
"filter.operator.empty": "is empty",
|
||||||
|
"filter.operator.notEmpty": "is not empty",
|
||||||
|
"filter.combinator.or": "OR",
|
||||||
|
"filter.combinator.and": "AND",
|
||||||
|
"filter.addCondition": "Add condition",
|
||||||
|
"filter.removeCondition": "Remove condition",
|
||||||
|
"filter.maxConditions": "Maximum conditions reached",
|
||||||
|
"filter.condition.resolvedTrue": "This condition is true for the first input item",
|
||||||
|
"filter.condition.resolvedFalse": "This condition is false for the first input item",
|
||||||
|
"filter.condition.placeholderLeft": "value1",
|
||||||
|
"filter.condition.placeholderRight": "value2",
|
||||||
"templateSetup.title": "Setup '{name}' template",
|
"templateSetup.title": "Setup '{name}' template",
|
||||||
"templateSetup.instructions": "You need {0} account to setup this template",
|
"templateSetup.instructions": "You need {0} account to setup this template",
|
||||||
"templateSetup.skip": "Skip",
|
"templateSetup.skip": "Skip",
|
||||||
|
|
|
@ -46,7 +46,7 @@ export const useNDVStore = defineStore(STORES.NDV, {
|
||||||
isDragging: false,
|
isDragging: false,
|
||||||
type: '',
|
type: '',
|
||||||
data: '',
|
data: '',
|
||||||
canDrop: false,
|
activeTargetId: null,
|
||||||
stickyPosition: null,
|
stickyPosition: null,
|
||||||
},
|
},
|
||||||
isMappingOnboarded: useStorage(LOCAL_STORAGE_MAPPING_IS_ONBOARDED).value === 'true',
|
isMappingOnboarded: useStorage(LOCAL_STORAGE_MAPPING_IS_ONBOARDED).value === 'true',
|
||||||
|
@ -94,7 +94,7 @@ export const useNDVStore = defineStore(STORES.NDV, {
|
||||||
return this.draggable.data;
|
return this.draggable.data;
|
||||||
},
|
},
|
||||||
canDraggableDrop(): boolean {
|
canDraggableDrop(): boolean {
|
||||||
return this.draggable.canDrop;
|
return this.draggable.activeTargetId !== null;
|
||||||
},
|
},
|
||||||
outputPanelEditMode(): NDVState['output']['editMode'] {
|
outputPanelEditMode(): NDVState['output']['editMode'] {
|
||||||
return this.output.editMode;
|
return this.output.editMode;
|
||||||
|
@ -191,7 +191,7 @@ export const useNDVStore = defineStore(STORES.NDV, {
|
||||||
isDragging: true,
|
isDragging: true,
|
||||||
type,
|
type,
|
||||||
data,
|
data,
|
||||||
canDrop: false,
|
activeTargetId: null,
|
||||||
stickyPosition: null,
|
stickyPosition: null,
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
|
@ -200,15 +200,15 @@ export const useNDVStore = defineStore(STORES.NDV, {
|
||||||
isDragging: false,
|
isDragging: false,
|
||||||
type: '',
|
type: '',
|
||||||
data: '',
|
data: '',
|
||||||
canDrop: false,
|
activeTargetId: null,
|
||||||
stickyPosition: null,
|
stickyPosition: null,
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
setDraggableStickyPos(position: XYPosition | null): void {
|
setDraggableStickyPos(position: XYPosition | null): void {
|
||||||
this.draggable.stickyPosition = position;
|
this.draggable.stickyPosition = position;
|
||||||
},
|
},
|
||||||
setDraggableCanDrop(canDrop: boolean): void {
|
setDraggableTargetId(id: string | null): void {
|
||||||
this.draggable.canDrop = canDrop;
|
this.draggable.activeTargetId = id;
|
||||||
},
|
},
|
||||||
setMappingTelemetry(telemetry: { [key: string]: string | number | boolean }): void {
|
setMappingTelemetry(telemetry: { [key: string]: string | number | boolean }): void {
|
||||||
this.mappingTelemetry = { ...this.mappingTelemetry, ...telemetry };
|
this.mappingTelemetry = { ...this.mappingTelemetry, ...telemetry };
|
||||||
|
|
|
@ -1,489 +1,25 @@
|
||||||
import moment from 'moment';
|
import type { INodeTypeBaseDescription, IVersionedNodeType } from 'n8n-workflow';
|
||||||
import type {
|
import { VersionedNodeType } from 'n8n-workflow';
|
||||||
IExecuteFunctions,
|
|
||||||
INodeExecutionData,
|
|
||||||
INodeParameters,
|
|
||||||
INodeType,
|
|
||||||
INodeTypeDescription,
|
|
||||||
NodeParameterValue,
|
|
||||||
} from 'n8n-workflow';
|
|
||||||
import { NodeOperationError } from 'n8n-workflow';
|
|
||||||
|
|
||||||
export class If implements INodeType {
|
import { IfV1 } from './V1/IfV1.node';
|
||||||
description: INodeTypeDescription = {
|
import { IfV2 } from './V2/IfV2.node';
|
||||||
displayName: 'IF',
|
|
||||||
name: 'if',
|
|
||||||
icon: 'fa:map-signs',
|
|
||||||
group: ['transform'],
|
|
||||||
version: 1,
|
|
||||||
description: 'Route items to different branches (true/false)',
|
|
||||||
defaults: {
|
|
||||||
name: 'IF',
|
|
||||||
color: '#408000',
|
|
||||||
},
|
|
||||||
inputs: ['main'],
|
|
||||||
// eslint-disable-next-line n8n-nodes-base/node-class-description-outputs-wrong
|
|
||||||
outputs: ['main', 'main'],
|
|
||||||
outputNames: ['true', 'false'],
|
|
||||||
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',
|
|
||||||
name: 'combineOperation',
|
|
||||||
type: 'options',
|
|
||||||
options: [
|
|
||||||
{
|
|
||||||
name: 'ALL',
|
|
||||||
description: 'Only if all conditions are met it goes into "true" branch',
|
|
||||||
value: 'all',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: 'ANY',
|
|
||||||
description: 'If any of the conditions is met it goes into "true" branch',
|
|
||||||
value: 'any',
|
|
||||||
},
|
|
||||||
],
|
|
||||||
default: 'all',
|
|
||||||
description:
|
|
||||||
'If multiple rules got set this settings decides if it is true as soon as ANY condition matches or only if ALL get meet',
|
|
||||||
},
|
|
||||||
],
|
|
||||||
};
|
|
||||||
|
|
||||||
async execute(this: IExecuteFunctions): Promise<INodeExecutionData[][]> {
|
export class If extends VersionedNodeType {
|
||||||
const returnDataTrue: INodeExecutionData[] = [];
|
constructor() {
|
||||||
const returnDataFalse: INodeExecutionData[] = [];
|
const baseDescription: INodeTypeBaseDescription = {
|
||||||
|
displayName: 'If',
|
||||||
const items = this.getInputData();
|
name: 'if',
|
||||||
|
icon: 'fa:map-signs',
|
||||||
let item: INodeExecutionData;
|
group: ['transform'],
|
||||||
let combineOperation: string;
|
description: 'Route items to different branches (true/false)',
|
||||||
|
defaultVersion: 2,
|
||||||
const isDateObject = (value: NodeParameterValue) =>
|
|
||||||
Object.prototype.toString.call(value) === '[object Date]';
|
|
||||||
const isDateInvalid = (value: NodeParameterValue) => value?.toString() === 'Invalid Date';
|
|
||||||
|
|
||||||
// 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),
|
|
||||||
isEmpty: (value1: NodeParameterValue) =>
|
|
||||||
[undefined, null, '', NaN].includes(value1 as string) ||
|
|
||||||
(typeof value1 === 'object' && value1 !== null && !isDateObject(value1)
|
|
||||||
? Object.entries(value1 as string).length === 0
|
|
||||||
: false) ||
|
|
||||||
(isDateObject(value1) && isDateInvalid(value1)),
|
|
||||||
isNotEmpty: (value1: NodeParameterValue) =>
|
|
||||||
!(
|
|
||||||
[undefined, null, '', NaN].includes(value1 as string) ||
|
|
||||||
(typeof value1 === 'object' && value1 !== null && !isDateObject(value1)
|
|
||||||
? Object.entries(value1 as string).length === 0
|
|
||||||
: false) ||
|
|
||||||
(isDateObject(value1) && isDateInvalid(value1))
|
|
||||||
),
|
|
||||||
regex: (value1: NodeParameterValue, value2: NodeParameterValue) => {
|
|
||||||
const regexMatch = (value2 || '').toString().match(new RegExp('^/(.*?)/([gimusy]*)$'));
|
|
||||||
|
|
||||||
let regex: RegExp;
|
|
||||||
if (!regexMatch) {
|
|
||||||
regex = new RegExp((value2 || '').toString());
|
|
||||||
} else if (regexMatch.length === 1) {
|
|
||||||
regex = new RegExp(regexMatch[1]);
|
|
||||||
} else {
|
|
||||||
regex = new RegExp(regexMatch[1], regexMatch[2]);
|
|
||||||
}
|
|
||||||
|
|
||||||
return !!(value1 || '').toString().match(regex);
|
|
||||||
},
|
|
||||||
notRegex: (value1: NodeParameterValue, value2: NodeParameterValue) => {
|
|
||||||
const regexMatch = (value2 || '').toString().match(new RegExp('^/(.*?)/([gimusy]*)$'));
|
|
||||||
|
|
||||||
let regex: RegExp;
|
|
||||||
if (!regexMatch) {
|
|
||||||
regex = new RegExp((value2 || '').toString());
|
|
||||||
} else if (regexMatch.length === 1) {
|
|
||||||
regex = new RegExp(regexMatch[1]);
|
|
||||||
} else {
|
|
||||||
regex = new RegExp(regexMatch[1], regexMatch[2]);
|
|
||||||
}
|
|
||||||
|
|
||||||
return !(value1 || '').toString().match(regex);
|
|
||||||
},
|
|
||||||
};
|
};
|
||||||
|
|
||||||
// Converts the input data of a dateTime into a number for easy compare
|
const nodeVersions: IVersionedNodeType['nodeVersions'] = {
|
||||||
const convertDateTime = (value: NodeParameterValue): number => {
|
1: new IfV1(baseDescription),
|
||||||
let returnValue: number | undefined = undefined;
|
2: new IfV2(baseDescription),
|
||||||
if (typeof value === 'string') {
|
|
||||||
returnValue = new Date(value).getTime();
|
|
||||||
} else if (typeof value === 'number') {
|
|
||||||
returnValue = value;
|
|
||||||
}
|
|
||||||
if (moment.isMoment(value)) {
|
|
||||||
returnValue = value.unix();
|
|
||||||
}
|
|
||||||
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;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
// The different dataTypes to check the values in
|
super(nodeVersions, baseDescription);
|
||||||
const dataTypes = ['boolean', 'dateTime', 'number', 'string'];
|
|
||||||
|
|
||||||
// Iterate over all items to check which ones should be output as via output "true" and
|
|
||||||
// which ones via output "false"
|
|
||||||
let dataType: string;
|
|
||||||
let compareOperationResult: boolean;
|
|
||||||
let value1: NodeParameterValue, value2: NodeParameterValue;
|
|
||||||
itemLoop: for (let itemIndex = 0; itemIndex < items.length; itemIndex++) {
|
|
||||||
item = items[itemIndex];
|
|
||||||
|
|
||||||
let compareData: INodeParameters;
|
|
||||||
|
|
||||||
combineOperation = this.getNodeParameter('combineOperation', itemIndex) as string;
|
|
||||||
|
|
||||||
// Check all the values of the different dataTypes
|
|
||||||
for (dataType of dataTypes) {
|
|
||||||
// Check all the values of the current dataType
|
|
||||||
for (compareData of this.getNodeParameter(
|
|
||||||
`conditions.${dataType}`,
|
|
||||||
itemIndex,
|
|
||||||
[],
|
|
||||||
) as INodeParameters[]) {
|
|
||||||
// Check if the values passes
|
|
||||||
|
|
||||||
value1 = compareData.value1 as NodeParameterValue;
|
|
||||||
value2 = compareData.value2 as NodeParameterValue;
|
|
||||||
|
|
||||||
if (dataType === 'dateTime') {
|
|
||||||
value1 = convertDateTime(value1);
|
|
||||||
value2 = convertDateTime(value2);
|
|
||||||
}
|
|
||||||
|
|
||||||
compareOperationResult = compareOperationFunctions[compareData.operation as string](
|
|
||||||
value1,
|
|
||||||
value2,
|
|
||||||
);
|
|
||||||
|
|
||||||
if (compareOperationResult && combineOperation === 'any') {
|
|
||||||
// If it passes and the operation is "any" we do not have to check any
|
|
||||||
// other ones as it should pass anyway. So go on with the next item.
|
|
||||||
returnDataTrue.push(item);
|
|
||||||
continue itemLoop;
|
|
||||||
} else if (!compareOperationResult && combineOperation === 'all') {
|
|
||||||
// If it fails and the operation is "all" we do not have to check any
|
|
||||||
// other ones as it should be not pass anyway. So go on with the next item.
|
|
||||||
returnDataFalse.push(item);
|
|
||||||
continue itemLoop;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (item.pairedItem === undefined) {
|
|
||||||
item.pairedItem = [{ item: itemIndex }];
|
|
||||||
}
|
|
||||||
|
|
||||||
if (combineOperation === 'all') {
|
|
||||||
// If the operation is "all" it means the item did match all conditions
|
|
||||||
// so it passes.
|
|
||||||
returnDataTrue.push(item);
|
|
||||||
} else {
|
|
||||||
// If the operation is "any" it means the the item did not match any condition.
|
|
||||||
returnDataFalse.push(item);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return [returnDataTrue, returnDataFalse];
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
486
packages/nodes-base/nodes/If/V1/IfV1.node.ts
Normal file
486
packages/nodes-base/nodes/If/V1/IfV1.node.ts
Normal file
|
@ -0,0 +1,486 @@
|
||||||
|
import moment from 'moment';
|
||||||
|
import type {
|
||||||
|
IExecuteFunctions,
|
||||||
|
INodeExecutionData,
|
||||||
|
INodeParameters,
|
||||||
|
INodeType,
|
||||||
|
INodeTypeBaseDescription,
|
||||||
|
INodeTypeDescription,
|
||||||
|
NodeParameterValue,
|
||||||
|
} from 'n8n-workflow';
|
||||||
|
import { NodeOperationError } from 'n8n-workflow';
|
||||||
|
|
||||||
|
export class IfV1 implements INodeType {
|
||||||
|
description: INodeTypeDescription;
|
||||||
|
|
||||||
|
constructor(baseDescription: INodeTypeBaseDescription) {
|
||||||
|
this.description = {
|
||||||
|
...baseDescription,
|
||||||
|
version: 1,
|
||||||
|
defaults: {
|
||||||
|
name: 'If',
|
||||||
|
color: '#408000',
|
||||||
|
},
|
||||||
|
inputs: ['main'],
|
||||||
|
// eslint-disable-next-line n8n-nodes-base/node-class-description-outputs-wrong
|
||||||
|
outputs: ['main', 'main'],
|
||||||
|
outputNames: ['true', 'false'],
|
||||||
|
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',
|
||||||
|
name: 'combineOperation',
|
||||||
|
type: 'options',
|
||||||
|
options: [
|
||||||
|
{
|
||||||
|
name: 'ALL',
|
||||||
|
description: 'Only if all conditions are met it goes into "true" branch',
|
||||||
|
value: 'all',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'ANY',
|
||||||
|
description: 'If any of the conditions is met it goes into "true" branch',
|
||||||
|
value: 'any',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
default: 'all',
|
||||||
|
description:
|
||||||
|
'If multiple rules got set this settings decides if it is true as soon as ANY condition matches or only if ALL get meet',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
async execute(this: IExecuteFunctions): Promise<INodeExecutionData[][]> {
|
||||||
|
const returnDataTrue: INodeExecutionData[] = [];
|
||||||
|
const returnDataFalse: INodeExecutionData[] = [];
|
||||||
|
|
||||||
|
const items = this.getInputData();
|
||||||
|
|
||||||
|
let item: INodeExecutionData;
|
||||||
|
let combineOperation: string;
|
||||||
|
|
||||||
|
const isDateObject = (value: NodeParameterValue) =>
|
||||||
|
Object.prototype.toString.call(value) === '[object Date]';
|
||||||
|
const isDateInvalid = (value: NodeParameterValue) => value?.toString() === 'Invalid Date';
|
||||||
|
|
||||||
|
// 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),
|
||||||
|
isEmpty: (value1: NodeParameterValue) =>
|
||||||
|
[undefined, null, '', NaN].includes(value1 as string) ||
|
||||||
|
(typeof value1 === 'object' && value1 !== null && !isDateObject(value1)
|
||||||
|
? Object.entries(value1 as string).length === 0
|
||||||
|
: false) ||
|
||||||
|
(isDateObject(value1) && isDateInvalid(value1)),
|
||||||
|
isNotEmpty: (value1: NodeParameterValue) =>
|
||||||
|
!(
|
||||||
|
[undefined, null, '', NaN].includes(value1 as string) ||
|
||||||
|
(typeof value1 === 'object' && value1 !== null && !isDateObject(value1)
|
||||||
|
? Object.entries(value1 as string).length === 0
|
||||||
|
: false) ||
|
||||||
|
(isDateObject(value1) && isDateInvalid(value1))
|
||||||
|
),
|
||||||
|
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 (moment.isMoment(value)) {
|
||||||
|
returnValue = value.unix();
|
||||||
|
}
|
||||||
|
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;
|
||||||
|
};
|
||||||
|
|
||||||
|
// The different dataTypes to check the values in
|
||||||
|
const dataTypes = ['boolean', 'dateTime', 'number', 'string'];
|
||||||
|
|
||||||
|
// Iterate over all items to check which ones should be output as via output "true" and
|
||||||
|
// which ones via output "false"
|
||||||
|
let dataType: string;
|
||||||
|
let compareOperationResult: boolean;
|
||||||
|
let value1: NodeParameterValue, value2: NodeParameterValue;
|
||||||
|
itemLoop: for (let itemIndex = 0; itemIndex < items.length; itemIndex++) {
|
||||||
|
item = items[itemIndex];
|
||||||
|
|
||||||
|
let compareData: INodeParameters;
|
||||||
|
|
||||||
|
combineOperation = this.getNodeParameter('combineOperation', itemIndex) as string;
|
||||||
|
|
||||||
|
// Check all the values of the different dataTypes
|
||||||
|
for (dataType of dataTypes) {
|
||||||
|
// Check all the values of the current dataType
|
||||||
|
for (compareData of this.getNodeParameter(
|
||||||
|
`conditions.${dataType}`,
|
||||||
|
itemIndex,
|
||||||
|
[],
|
||||||
|
) as INodeParameters[]) {
|
||||||
|
// Check if the values passes
|
||||||
|
|
||||||
|
value1 = compareData.value1 as NodeParameterValue;
|
||||||
|
value2 = compareData.value2 as NodeParameterValue;
|
||||||
|
|
||||||
|
if (dataType === 'dateTime') {
|
||||||
|
value1 = convertDateTime(value1);
|
||||||
|
value2 = convertDateTime(value2);
|
||||||
|
}
|
||||||
|
|
||||||
|
compareOperationResult = compareOperationFunctions[compareData.operation as string](
|
||||||
|
value1,
|
||||||
|
value2,
|
||||||
|
);
|
||||||
|
|
||||||
|
if (compareOperationResult && combineOperation === 'any') {
|
||||||
|
// If it passes and the operation is "any" we do not have to check any
|
||||||
|
// other ones as it should pass anyway. So go on with the next item.
|
||||||
|
returnDataTrue.push(item);
|
||||||
|
continue itemLoop;
|
||||||
|
} else if (!compareOperationResult && combineOperation === 'all') {
|
||||||
|
// If it fails and the operation is "all" we do not have to check any
|
||||||
|
// other ones as it should be not pass anyway. So go on with the next item.
|
||||||
|
returnDataFalse.push(item);
|
||||||
|
continue itemLoop;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (combineOperation === 'all') {
|
||||||
|
// If the operation is "all" it means the item did match all conditions
|
||||||
|
// so it passes.
|
||||||
|
returnDataTrue.push(item);
|
||||||
|
} else {
|
||||||
|
// If the operation is "any" it means the the item did not match any condition.
|
||||||
|
returnDataFalse.push(item);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return [returnDataTrue, returnDataFalse];
|
||||||
|
}
|
||||||
|
}
|
111
packages/nodes-base/nodes/If/V2/IfV2.node.ts
Normal file
111
packages/nodes-base/nodes/If/V2/IfV2.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 IfV2 implements INodeType {
|
||||||
|
description: INodeTypeDescription;
|
||||||
|
|
||||||
|
constructor(baseDescription: INodeTypeBaseDescription) {
|
||||||
|
this.description = {
|
||||||
|
...baseDescription,
|
||||||
|
version: 2,
|
||||||
|
defaults: {
|
||||||
|
name: 'If',
|
||||||
|
color: '#408000',
|
||||||
|
},
|
||||||
|
inputs: ['main'],
|
||||||
|
outputs: ['main', 'main'],
|
||||||
|
outputNames: ['true', 'false'],
|
||||||
|
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 trueItems: INodeExecutionData[] = [];
|
||||||
|
const falseItems: 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) {
|
||||||
|
trueItems.push(item);
|
||||||
|
} else {
|
||||||
|
falseItems.push(item);
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
if (this.continueOnFail()) {
|
||||||
|
falseItems.push(item);
|
||||||
|
} else {
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return [trueItems, falseItems];
|
||||||
|
}
|
||||||
|
}
|
165
packages/nodes-base/nodes/If/test/v2/IfV2.date-time.json
Normal file
165
packages/nodes-base/nodes/If/test/v2/IfV2.date-time.json
Normal file
|
@ -0,0 +1,165 @@
|
||||||
|
{
|
||||||
|
"name": "Filter test",
|
||||||
|
"nodes": [
|
||||||
|
{
|
||||||
|
"parameters": {},
|
||||||
|
"id": "f332a7d1-31b4-4e78-b31e-9e8db945bf3f",
|
||||||
|
"name": "When clicking \"Execute Workflow\"",
|
||||||
|
"type": "n8n-nodes-base.manualTrigger",
|
||||||
|
"typeVersion": 1,
|
||||||
|
"position": [
|
||||||
|
-60,
|
||||||
|
480
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"parameters": {
|
||||||
|
"jsCode": "return [\n {\n \"date\": \"2023-10-26T16:45:52.367Z\",\n \"label\": \"Apple\",\n },\n {\n \"date\": \"2023-10-20T16:45:52.367Z\",\n \"label\": \"Banana\"\n },\n {\n \"date\": \"2023-10-20T16:45:52.367Z\",\n \"label\": \"Kiwi\"\n },\n {\n \"date\": \"2023-10-20T16:45:52.367Z\",\n \"label\": \"Orange\"\n }\n]"
|
||||||
|
},
|
||||||
|
"id": "60697c7f-3948-4790-97ba-8aba03d02ac2",
|
||||||
|
"name": "Code",
|
||||||
|
"type": "n8n-nodes-base.code",
|
||||||
|
"typeVersion": 2,
|
||||||
|
"position": [
|
||||||
|
160,
|
||||||
|
480
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"parameters": {
|
||||||
|
"conditions": {
|
||||||
|
"options": {
|
||||||
|
"caseSensitive": true,
|
||||||
|
"leftValue": ""
|
||||||
|
},
|
||||||
|
"conditions": [
|
||||||
|
{
|
||||||
|
"leftValue": "={{ $json.date }}",
|
||||||
|
"rightValue": "2023-10-21",
|
||||||
|
"operator": {
|
||||||
|
"type": "dateTime",
|
||||||
|
"operation": "before"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"combinator": "or"
|
||||||
|
},
|
||||||
|
"options": {}
|
||||||
|
},
|
||||||
|
"id": "7531191b-5ac3-45dc-8afb-27ae83d8f33a",
|
||||||
|
"name": "If",
|
||||||
|
"type": "n8n-nodes-base.if",
|
||||||
|
"typeVersion": 2,
|
||||||
|
"position": [
|
||||||
|
380,
|
||||||
|
480
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"parameters": {},
|
||||||
|
"id": "d8c614ea-0bbf-4b12-ad7d-c9ebe09ce583",
|
||||||
|
"name": "Then",
|
||||||
|
"type": "n8n-nodes-base.noOp",
|
||||||
|
"typeVersion": 1,
|
||||||
|
"position": [
|
||||||
|
600,
|
||||||
|
400
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"parameters": {},
|
||||||
|
"id": "69364770-60d2-4ef4-9f29-9570718a9a10",
|
||||||
|
"name": "Else",
|
||||||
|
"type": "n8n-nodes-base.noOp",
|
||||||
|
"typeVersion": 1,
|
||||||
|
"position": [
|
||||||
|
600,
|
||||||
|
580
|
||||||
|
]
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"pinData": {
|
||||||
|
"Then": [
|
||||||
|
{
|
||||||
|
"json": {
|
||||||
|
"date": "2023-10-20T16:45:52.367Z",
|
||||||
|
"label": "Banana"
|
||||||
|
}
|
||||||
|
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"json": {
|
||||||
|
"date": "2023-10-20T16:45:52.367Z",
|
||||||
|
"label": "Kiwi"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"json": {
|
||||||
|
"date": "2023-10-20T16:45:52.367Z",
|
||||||
|
"label": "Orange"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"Else": [
|
||||||
|
{
|
||||||
|
"json": {
|
||||||
|
"date": "2023-10-26T16:45:52.367Z",
|
||||||
|
"label": "Apple"
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"connections": {
|
||||||
|
"When clicking \"Execute Workflow\"": {
|
||||||
|
"main": [
|
||||||
|
[
|
||||||
|
{
|
||||||
|
"node": "Code",
|
||||||
|
"type": "main",
|
||||||
|
"index": 0
|
||||||
|
}
|
||||||
|
]
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"Code": {
|
||||||
|
"main": [
|
||||||
|
[
|
||||||
|
{
|
||||||
|
"node": "If",
|
||||||
|
"type": "main",
|
||||||
|
"index": 0
|
||||||
|
}
|
||||||
|
]
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"If": {
|
||||||
|
"main": [
|
||||||
|
[
|
||||||
|
{
|
||||||
|
"node": "Then",
|
||||||
|
"type": "main",
|
||||||
|
"index": 0
|
||||||
|
}
|
||||||
|
],
|
||||||
|
[
|
||||||
|
{
|
||||||
|
"node": "Else",
|
||||||
|
"type": "main",
|
||||||
|
"index": 0
|
||||||
|
}
|
||||||
|
]
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"active": false,
|
||||||
|
"settings": {
|
||||||
|
"executionOrder": "v1"
|
||||||
|
},
|
||||||
|
"versionId": "ce7ed9ae-704e-4da8-b178-24556f720b2a",
|
||||||
|
"id": "BWUTRs5RHxVgQ4uT",
|
||||||
|
"meta": {
|
||||||
|
"instanceId": "78577815012af39cf16dad7a787b0898c42fb7514b8a7f99b2136862c2af502c"
|
||||||
|
},
|
||||||
|
"tags": []
|
||||||
|
}
|
5
packages/nodes-base/nodes/If/test/v2/IfV2.node.test.ts
Normal file
5
packages/nodes-base/nodes/If/test/v2/IfV2.node.test.ts
Normal file
|
@ -0,0 +1,5 @@
|
||||||
|
import { testWorkflows, getWorkflowFilenames } from '@test/nodes/Helpers';
|
||||||
|
|
||||||
|
const workflows = getWorkflowFilenames(__dirname);
|
||||||
|
|
||||||
|
describe('Test IF v2 Node', () => testWorkflows(workflows));
|
165
packages/nodes-base/nodes/If/test/v2/IfV2.number.json
Normal file
165
packages/nodes-base/nodes/If/test/v2/IfV2.number.json
Normal file
|
@ -0,0 +1,165 @@
|
||||||
|
{
|
||||||
|
"name": "Filter test",
|
||||||
|
"nodes": [
|
||||||
|
{
|
||||||
|
"parameters": {},
|
||||||
|
"id": "f332a7d1-31b4-4e78-b31e-9e8db945bf3f",
|
||||||
|
"name": "When clicking \"Execute Workflow\"",
|
||||||
|
"type": "n8n-nodes-base.manualTrigger",
|
||||||
|
"typeVersion": 1,
|
||||||
|
"position": [
|
||||||
|
-60,
|
||||||
|
480
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"parameters": {
|
||||||
|
"jsCode": "return [\n {\n \"count\": 1,\n \"label\": \"Apple\",\n },\n {\n \"count\": 5,\n \"label\": \"Banana\"\n },\n {\n \"count\": 12,\n \"label\": \"Kiwi\"\n }\n]"
|
||||||
|
},
|
||||||
|
"id": "60697c7f-3948-4790-97ba-8aba03d02ac2",
|
||||||
|
"name": "Code",
|
||||||
|
"type": "n8n-nodes-base.code",
|
||||||
|
"typeVersion": 2,
|
||||||
|
"position": [
|
||||||
|
160,
|
||||||
|
480
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"parameters": {
|
||||||
|
"conditions": {
|
||||||
|
"options": {
|
||||||
|
"caseSensitive": true,
|
||||||
|
"leftValue": ""
|
||||||
|
},
|
||||||
|
"conditions": [
|
||||||
|
{
|
||||||
|
"leftValue": "={{ $json.count }}",
|
||||||
|
"rightValue": "1",
|
||||||
|
"operator": {
|
||||||
|
"type": "number",
|
||||||
|
"operation": "equals"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"leftValue": "={{ $json.count }}",
|
||||||
|
"rightValue": "10",
|
||||||
|
"operator": {
|
||||||
|
"type": "number",
|
||||||
|
"operation": "gt"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"combinator": "or"
|
||||||
|
},
|
||||||
|
"options": {}
|
||||||
|
},
|
||||||
|
"id": "7531191b-5ac3-45dc-8afb-27ae83d8f33a",
|
||||||
|
"name": "If",
|
||||||
|
"type": "n8n-nodes-base.if",
|
||||||
|
"typeVersion": 2,
|
||||||
|
"position": [
|
||||||
|
380,
|
||||||
|
480
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"parameters": {},
|
||||||
|
"id": "d8c614ea-0bbf-4b12-ad7d-c9ebe09ce583",
|
||||||
|
"name": "Then",
|
||||||
|
"type": "n8n-nodes-base.noOp",
|
||||||
|
"typeVersion": 1,
|
||||||
|
"position": [
|
||||||
|
600,
|
||||||
|
400
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"parameters": {},
|
||||||
|
"id": "69364770-60d2-4ef4-9f29-9570718a9a10",
|
||||||
|
"name": "Else",
|
||||||
|
"type": "n8n-nodes-base.noOp",
|
||||||
|
"typeVersion": 1,
|
||||||
|
"position": [
|
||||||
|
600,
|
||||||
|
580
|
||||||
|
]
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"pinData": {
|
||||||
|
"Then": [
|
||||||
|
{
|
||||||
|
"json": {
|
||||||
|
"count": 1,
|
||||||
|
"label": "Apple"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"json": {
|
||||||
|
"count": 12,
|
||||||
|
"label": "Kiwi"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"Else": [
|
||||||
|
{
|
||||||
|
"json": {
|
||||||
|
"count": 5,
|
||||||
|
"label": "Banana"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"connections": {
|
||||||
|
"When clicking \"Execute Workflow\"": {
|
||||||
|
"main": [
|
||||||
|
[
|
||||||
|
{
|
||||||
|
"node": "Code",
|
||||||
|
"type": "main",
|
||||||
|
"index": 0
|
||||||
|
}
|
||||||
|
]
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"Code": {
|
||||||
|
"main": [
|
||||||
|
[
|
||||||
|
{
|
||||||
|
"node": "If",
|
||||||
|
"type": "main",
|
||||||
|
"index": 0
|
||||||
|
}
|
||||||
|
]
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"If": {
|
||||||
|
"main": [
|
||||||
|
[
|
||||||
|
{
|
||||||
|
"node": "Then",
|
||||||
|
"type": "main",
|
||||||
|
"index": 0
|
||||||
|
}
|
||||||
|
],
|
||||||
|
[
|
||||||
|
{
|
||||||
|
"node": "Else",
|
||||||
|
"type": "main",
|
||||||
|
"index": 0
|
||||||
|
}
|
||||||
|
]
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"active": false,
|
||||||
|
"settings": {
|
||||||
|
"executionOrder": "v1"
|
||||||
|
},
|
||||||
|
"versionId": "78d54316-4f39-4012-bbb8-c789f1f8865b",
|
||||||
|
"id": "BWUTRs5RHxVgQ4uT",
|
||||||
|
"meta": {
|
||||||
|
"instanceId": "78577815012af39cf16dad7a787b0898c42fb7514b8a7f99b2136862c2af502c"
|
||||||
|
},
|
||||||
|
"tags": []
|
||||||
|
}
|
182
packages/nodes-base/nodes/If/test/v2/IfV2.other.json
Normal file
182
packages/nodes-base/nodes/If/test/v2/IfV2.other.json
Normal file
|
@ -0,0 +1,182 @@
|
||||||
|
{
|
||||||
|
"name": "Filter test",
|
||||||
|
"nodes": [
|
||||||
|
{
|
||||||
|
"parameters": {},
|
||||||
|
"id": "f332a7d1-31b4-4e78-b31e-9e8db945bf3f",
|
||||||
|
"name": "When clicking \"Execute Workflow\"",
|
||||||
|
"type": "n8n-nodes-base.manualTrigger",
|
||||||
|
"typeVersion": 1,
|
||||||
|
"position": [
|
||||||
|
-60,
|
||||||
|
480
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"parameters": {
|
||||||
|
"jsCode": "return [\n {\n \"label\": \"Apple\",\n tags: [],\n meta: {foo: 'bar'}\n },\n {\n \"label\": \"Banana\",\n tags: ['exotic'],\n meta: {}\n },\n {\n \"label\": \"Kiwi\",\n tags: ['exotic'],\n meta: {}\n },\n {\n \"label\": \"Orange\",\n meta: {}\n }\n]"
|
||||||
|
},
|
||||||
|
"id": "60697c7f-3948-4790-97ba-8aba03d02ac2",
|
||||||
|
"name": "Code",
|
||||||
|
"type": "n8n-nodes-base.code",
|
||||||
|
"typeVersion": 2,
|
||||||
|
"position": [
|
||||||
|
160,
|
||||||
|
480
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"parameters": {
|
||||||
|
"conditions": {
|
||||||
|
"options": {
|
||||||
|
"caseSensitive": true,
|
||||||
|
"leftValue": ""
|
||||||
|
},
|
||||||
|
"conditions": [
|
||||||
|
{
|
||||||
|
"leftValue": "={{ $json.tags }}",
|
||||||
|
"rightValue": "exotic",
|
||||||
|
"operator": {
|
||||||
|
"type": "array",
|
||||||
|
"operation": "contains",
|
||||||
|
"rightType": "any"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"leftValue": "={{ $json.meta }}",
|
||||||
|
"rightValue": "",
|
||||||
|
"operator": {
|
||||||
|
"type": "object",
|
||||||
|
"operation": "notEmpty",
|
||||||
|
"singleValue": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"combinator": "or"
|
||||||
|
},
|
||||||
|
"options": {}
|
||||||
|
},
|
||||||
|
"id": "7531191b-5ac3-45dc-8afb-27ae83d8f33a",
|
||||||
|
"name": "If",
|
||||||
|
"type": "n8n-nodes-base.if",
|
||||||
|
"typeVersion": 2,
|
||||||
|
"position": [
|
||||||
|
380,
|
||||||
|
480
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"parameters": {},
|
||||||
|
"id": "d8c614ea-0bbf-4b12-ad7d-c9ebe09ce583",
|
||||||
|
"name": "Then",
|
||||||
|
"type": "n8n-nodes-base.noOp",
|
||||||
|
"typeVersion": 1,
|
||||||
|
"position": [
|
||||||
|
600,
|
||||||
|
400
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"parameters": {},
|
||||||
|
"id": "69364770-60d2-4ef4-9f29-9570718a9a10",
|
||||||
|
"name": "Else",
|
||||||
|
"type": "n8n-nodes-base.noOp",
|
||||||
|
"typeVersion": 1,
|
||||||
|
"position": [
|
||||||
|
600,
|
||||||
|
580
|
||||||
|
]
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"pinData": {
|
||||||
|
"Then": [
|
||||||
|
{
|
||||||
|
"json": {
|
||||||
|
"label": "Apple",
|
||||||
|
"tags": [],
|
||||||
|
"meta": {
|
||||||
|
"foo": "bar"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"json": {
|
||||||
|
"label": "Banana",
|
||||||
|
"tags": [
|
||||||
|
"exotic"
|
||||||
|
],
|
||||||
|
"meta": {}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"json": {
|
||||||
|
"label": "Kiwi",
|
||||||
|
"tags": [
|
||||||
|
"exotic"
|
||||||
|
],
|
||||||
|
"meta": {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"Else": [
|
||||||
|
{
|
||||||
|
"json": {
|
||||||
|
"label": "Orange",
|
||||||
|
"meta": {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"connections": {
|
||||||
|
"When clicking \"Execute Workflow\"": {
|
||||||
|
"main": [
|
||||||
|
[
|
||||||
|
{
|
||||||
|
"node": "Code",
|
||||||
|
"type": "main",
|
||||||
|
"index": 0
|
||||||
|
}
|
||||||
|
]
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"Code": {
|
||||||
|
"main": [
|
||||||
|
[
|
||||||
|
{
|
||||||
|
"node": "If",
|
||||||
|
"type": "main",
|
||||||
|
"index": 0
|
||||||
|
}
|
||||||
|
]
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"If": {
|
||||||
|
"main": [
|
||||||
|
[
|
||||||
|
{
|
||||||
|
"node": "Then",
|
||||||
|
"type": "main",
|
||||||
|
"index": 0
|
||||||
|
}
|
||||||
|
],
|
||||||
|
[
|
||||||
|
{
|
||||||
|
"node": "Else",
|
||||||
|
"type": "main",
|
||||||
|
"index": 0
|
||||||
|
}
|
||||||
|
]
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"active": false,
|
||||||
|
"settings": {
|
||||||
|
"executionOrder": "v1"
|
||||||
|
},
|
||||||
|
"versionId": "a6249f48-d88f-4b80-9ed9-79555e522d48",
|
||||||
|
"id": "BWUTRs5RHxVgQ4uT",
|
||||||
|
"meta": {
|
||||||
|
"instanceId": "78577815012af39cf16dad7a787b0898c42fb7514b8a7f99b2136862c2af502c"
|
||||||
|
},
|
||||||
|
"tags": []
|
||||||
|
}
|
178
packages/nodes-base/nodes/If/test/v2/IfV2.string.json
Normal file
178
packages/nodes-base/nodes/If/test/v2/IfV2.string.json
Normal file
|
@ -0,0 +1,178 @@
|
||||||
|
{
|
||||||
|
"name": "Filter test",
|
||||||
|
"nodes": [
|
||||||
|
{
|
||||||
|
"parameters": {},
|
||||||
|
"id": "96490c63-a3f4-4923-b969-8f9adcbb1bbb",
|
||||||
|
"name": "When clicking \"Execute Workflow\"",
|
||||||
|
"type": "n8n-nodes-base.manualTrigger",
|
||||||
|
"typeVersion": 1,
|
||||||
|
"position": [
|
||||||
|
-160,
|
||||||
|
300
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"parameters": {
|
||||||
|
"conditions": {
|
||||||
|
"options": {
|
||||||
|
"caseSensitive": false,
|
||||||
|
"leftValue": ""
|
||||||
|
},
|
||||||
|
"conditions": [
|
||||||
|
{
|
||||||
|
"leftValue": "={{ $json.firstname }}",
|
||||||
|
"rightValue": "s",
|
||||||
|
"operator": {
|
||||||
|
"type": "string",
|
||||||
|
"operation": "startsWith"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"leftValue": "={{ $json.lastname }}",
|
||||||
|
"rightValue": "",
|
||||||
|
"operator": {
|
||||||
|
"type": "any",
|
||||||
|
"operation": "exists"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"leftValue": "={{ $json.email }}",
|
||||||
|
"rightValue": "@yahoo.com",
|
||||||
|
"operator": {
|
||||||
|
"type": "string",
|
||||||
|
"operation": "endsWith"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"combinator": "and"
|
||||||
|
},
|
||||||
|
"options": {
|
||||||
|
"caseSensitive": false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"id": "48399b31-219a-42fa-bb5b-380dbb4a2e7d",
|
||||||
|
"name": "If",
|
||||||
|
"type": "n8n-nodes-base.if",
|
||||||
|
"typeVersion": 2,
|
||||||
|
"position": [
|
||||||
|
260,
|
||||||
|
300
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"parameters": {},
|
||||||
|
"id": "8a59a941-bafd-46a6-8692-878de715d912",
|
||||||
|
"name": "false",
|
||||||
|
"type": "n8n-nodes-base.noOp",
|
||||||
|
"typeVersion": 1,
|
||||||
|
"position": [
|
||||||
|
560,
|
||||||
|
440
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"parameters": {},
|
||||||
|
"id": "b82546fa-f9e9-4fa3-9dcb-e2c94a3784de",
|
||||||
|
"name": "Then",
|
||||||
|
"type": "n8n-nodes-base.noOp",
|
||||||
|
"typeVersion": 1,
|
||||||
|
"position": [
|
||||||
|
560,
|
||||||
|
140
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"parameters": {
|
||||||
|
"jsCode": "return [\n {\n \"email\": \"Shane@yahoo.com\",\n \"firstname\": \"Shane\",\n \"lastname\": \"Martin\"\n },\n {\n \"email\": \"Sharon@yahoo.com\",\n \"firstname\": \"Sharon\",\n \"lastname\": \"Tromp\"\n },\n {\n \"email\": \"sarah@gmail.com\",\n \"firstname\": \"Sarah\",\n \"lastname\": \"Dawson\"\n }\n]"
|
||||||
|
},
|
||||||
|
"id": "674c5688-ac03-49a7-83fb-62460a10cc10",
|
||||||
|
"name": "Code",
|
||||||
|
"type": "n8n-nodes-base.code",
|
||||||
|
"typeVersion": 2,
|
||||||
|
"position": [
|
||||||
|
60,
|
||||||
|
300
|
||||||
|
]
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"pinData": {
|
||||||
|
"Then": [
|
||||||
|
{
|
||||||
|
"json": {
|
||||||
|
"email": "Shane@yahoo.com",
|
||||||
|
"firstname": "Shane",
|
||||||
|
"lastname": "Martin"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"json": {
|
||||||
|
"email": "Sharon@yahoo.com",
|
||||||
|
"firstname": "Sharon",
|
||||||
|
"lastname": "Tromp"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"false": [
|
||||||
|
{
|
||||||
|
"json": {
|
||||||
|
"email": "sarah@gmail.com",
|
||||||
|
"firstname": "Sarah",
|
||||||
|
"lastname": "Dawson"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"connections": {
|
||||||
|
"When clicking \"Execute Workflow\"": {
|
||||||
|
"main": [
|
||||||
|
[
|
||||||
|
{
|
||||||
|
"node": "Code",
|
||||||
|
"type": "main",
|
||||||
|
"index": 0
|
||||||
|
}
|
||||||
|
]
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"If": {
|
||||||
|
"main": [
|
||||||
|
[
|
||||||
|
{
|
||||||
|
"node": "Then",
|
||||||
|
"type": "main",
|
||||||
|
"index": 0
|
||||||
|
}
|
||||||
|
],
|
||||||
|
[
|
||||||
|
{
|
||||||
|
"node": "false",
|
||||||
|
"type": "main",
|
||||||
|
"index": 0
|
||||||
|
}
|
||||||
|
]
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"Code": {
|
||||||
|
"main": [
|
||||||
|
[
|
||||||
|
{
|
||||||
|
"node": "If",
|
||||||
|
"type": "main",
|
||||||
|
"index": 0
|
||||||
|
}
|
||||||
|
]
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"active": false,
|
||||||
|
"settings": {
|
||||||
|
"executionOrder": "v1"
|
||||||
|
},
|
||||||
|
"versionId": "48c6a584-e79b-4ce4-ab4b-2b4ab663b89d",
|
||||||
|
"id": "BWUTRs5RHxVgQ4uT",
|
||||||
|
"meta": {
|
||||||
|
"instanceId": "78577815012af39cf16dad7a787b0898c42fb7514b8a7f99b2136862c2af502c"
|
||||||
|
},
|
||||||
|
"tags": []
|
||||||
|
}
|
|
@ -1071,6 +1071,7 @@ export type NodePropertyTypes =
|
||||||
| 'resourceLocator'
|
| 'resourceLocator'
|
||||||
| 'curlImport'
|
| 'curlImport'
|
||||||
| 'resourceMapper'
|
| 'resourceMapper'
|
||||||
|
| 'filter'
|
||||||
| 'credentials';
|
| 'credentials';
|
||||||
|
|
||||||
export type CodeAutocompleteTypes = 'function' | 'functionItem';
|
export type CodeAutocompleteTypes = 'function' | 'functionItem';
|
||||||
|
@ -1118,6 +1119,7 @@ export interface INodePropertyTypeOptions {
|
||||||
sortable?: boolean; // Supported when "multipleValues" set to true
|
sortable?: boolean; // Supported when "multipleValues" set to true
|
||||||
expirable?: boolean; // Supported by: hidden (only in the credentials)
|
expirable?: boolean; // Supported by: hidden (only in the credentials)
|
||||||
resourceMapper?: ResourceMapperTypeOptions;
|
resourceMapper?: ResourceMapperTypeOptions;
|
||||||
|
filter?: FilterTypeOptions;
|
||||||
[key: string]: any;
|
[key: string]: any;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1137,6 +1139,18 @@ export interface ResourceMapperTypeOptions {
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type NonEmptyArray<T> = [T, ...T[]];
|
||||||
|
|
||||||
|
export type FilterTypeCombinator = 'and' | 'or';
|
||||||
|
|
||||||
|
export type FilterTypeOptions = Partial<{
|
||||||
|
caseSensitive: boolean | string; // default = true
|
||||||
|
leftValue: string; // when set, user can't edit left side of condition
|
||||||
|
allowedCombinators: NonEmptyArray<FilterTypeCombinator>; // default = ['and', 'or']
|
||||||
|
maxConditions: number; // default = 10
|
||||||
|
typeValidation: 'strict' | 'loose' | {}; // default = strict, `| {}` is a TypeScript trick to allow custom strings, but still give autocomplete
|
||||||
|
}>;
|
||||||
|
|
||||||
export interface IDisplayOptions {
|
export interface IDisplayOptions {
|
||||||
hide?: {
|
hide?: {
|
||||||
[key: string]: NodeParameterValue[] | undefined;
|
[key: string]: NodeParameterValue[] | undefined;
|
||||||
|
@ -2209,6 +2223,42 @@ export type ResourceMapperValue = {
|
||||||
matchingColumns: string[];
|
matchingColumns: string[];
|
||||||
schema: ResourceMapperField[];
|
schema: ResourceMapperField[];
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export type FilterOperatorType =
|
||||||
|
| 'string'
|
||||||
|
| 'number'
|
||||||
|
| 'boolean'
|
||||||
|
| 'array'
|
||||||
|
| 'object'
|
||||||
|
| 'dateTime'
|
||||||
|
| 'any';
|
||||||
|
|
||||||
|
export interface FilterOperatorValue {
|
||||||
|
type: FilterOperatorType;
|
||||||
|
operation: string;
|
||||||
|
rightType?: FilterOperatorType;
|
||||||
|
singleValue?: boolean; // default = false
|
||||||
|
}
|
||||||
|
|
||||||
|
export type FilterConditionValue = {
|
||||||
|
id: string;
|
||||||
|
leftValue: unknown;
|
||||||
|
operator: FilterOperatorValue;
|
||||||
|
rightValue: unknown;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type FilterOptionsValue = {
|
||||||
|
caseSensitive: boolean;
|
||||||
|
leftValue: string;
|
||||||
|
typeValidation: 'strict' | 'loose';
|
||||||
|
};
|
||||||
|
|
||||||
|
export type FilterValue = {
|
||||||
|
options: FilterOptionsValue;
|
||||||
|
conditions: FilterConditionValue[];
|
||||||
|
combinator: FilterTypeCombinator;
|
||||||
|
};
|
||||||
|
|
||||||
export interface ExecutionOptions {
|
export interface ExecutionOptions {
|
||||||
limit?: number;
|
limit?: number;
|
||||||
}
|
}
|
||||||
|
|
|
@ -36,18 +36,22 @@ import type {
|
||||||
IWorkflowExecuteAdditionalData,
|
IWorkflowExecuteAdditionalData,
|
||||||
NodeParameterValue,
|
NodeParameterValue,
|
||||||
ResourceMapperValue,
|
ResourceMapperValue,
|
||||||
ValidationResult,
|
|
||||||
ConnectionTypes,
|
ConnectionTypes,
|
||||||
INodeTypeDescription,
|
INodeTypeDescription,
|
||||||
INodeOutputConfiguration,
|
INodeOutputConfiguration,
|
||||||
INodeInputConfiguration,
|
INodeInputConfiguration,
|
||||||
GenericValue,
|
GenericValue,
|
||||||
} from './Interfaces';
|
} from './Interfaces';
|
||||||
import { isResourceMapperValue, isValidResourceLocatorParameterValue } from './type-guards';
|
import {
|
||||||
|
isFilterValue,
|
||||||
|
isResourceMapperValue,
|
||||||
|
isValidResourceLocatorParameterValue,
|
||||||
|
} from './type-guards';
|
||||||
import { deepCopy } from './utils';
|
import { deepCopy } from './utils';
|
||||||
|
|
||||||
import { DateTime } from 'luxon';
|
|
||||||
import type { Workflow } from './Workflow';
|
import type { Workflow } from './Workflow';
|
||||||
|
import { validateFilterParameter } from './NodeParameters/FilterParameter';
|
||||||
|
import { validateFieldType } from './TypeValidation';
|
||||||
import { ApplicationError } from './errors/application.error';
|
import { ApplicationError } from './errors/application.error';
|
||||||
|
|
||||||
export const cronNodeOptions: INodePropertyCollection[] = [
|
export const cronNodeOptions: INodePropertyCollection[] = [
|
||||||
|
@ -1186,188 +1190,6 @@ export function nodeIssuesToString(issues: INodeIssues, node?: INode): string[]
|
||||||
return nodeIssues;
|
return nodeIssues;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Validates field against the schema and tries to parse it to the correct type
|
|
||||||
export const validateFieldType = (
|
|
||||||
fieldName: string,
|
|
||||||
value: unknown,
|
|
||||||
type: FieldType,
|
|
||||||
options?: INodePropertyOptions[],
|
|
||||||
): ValidationResult => {
|
|
||||||
if (value === null || value === undefined) return { valid: true };
|
|
||||||
const defaultErrorMessage = `'${fieldName}' expects a ${type} but we got '${String(value)}'`;
|
|
||||||
switch (type.toLowerCase()) {
|
|
||||||
case 'number': {
|
|
||||||
try {
|
|
||||||
return { valid: true, newValue: tryToParseNumber(value) };
|
|
||||||
} catch (e) {
|
|
||||||
return { valid: false, errorMessage: defaultErrorMessage };
|
|
||||||
}
|
|
||||||
}
|
|
||||||
case 'boolean': {
|
|
||||||
try {
|
|
||||||
return { valid: true, newValue: tryToParseBoolean(value) };
|
|
||||||
} catch (e) {
|
|
||||||
return { valid: false, errorMessage: defaultErrorMessage };
|
|
||||||
}
|
|
||||||
}
|
|
||||||
case 'datetime': {
|
|
||||||
try {
|
|
||||||
return { valid: true, newValue: tryToParseDateTime(value) };
|
|
||||||
} catch (e) {
|
|
||||||
const luxonDocsURL =
|
|
||||||
'https://moment.github.io/luxon/api-docs/index.html#datetimefromformat';
|
|
||||||
const errorMessage = `${defaultErrorMessage} <br/><br/> Consider using <a href="${luxonDocsURL}" target="_blank"><code>DateTime.fromFormat</code></a> to work with custom date formats.`;
|
|
||||||
return { valid: false, errorMessage };
|
|
||||||
}
|
|
||||||
}
|
|
||||||
case 'time': {
|
|
||||||
try {
|
|
||||||
return { valid: true, newValue: tryToParseTime(value) };
|
|
||||||
} catch (e) {
|
|
||||||
return {
|
|
||||||
valid: false,
|
|
||||||
errorMessage: `'${fieldName}' expects time (hh:mm:(:ss)) but we got '${String(value)}'.`,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
|
||||||
case 'object': {
|
|
||||||
try {
|
|
||||||
return { valid: true, newValue: tryToParseObject(value) };
|
|
||||||
} catch (e) {
|
|
||||||
return { valid: false, errorMessage: defaultErrorMessage };
|
|
||||||
}
|
|
||||||
}
|
|
||||||
case 'array': {
|
|
||||||
try {
|
|
||||||
return { valid: true, newValue: tryToParseArray(value) };
|
|
||||||
} catch (e) {
|
|
||||||
return { valid: false, errorMessage: defaultErrorMessage };
|
|
||||||
}
|
|
||||||
}
|
|
||||||
case 'options': {
|
|
||||||
const validOptions = options?.map((option) => option.value).join(', ') || '';
|
|
||||||
const isValidOption = options?.some((option) => option.value === value) || false;
|
|
||||||
|
|
||||||
if (!isValidOption) {
|
|
||||||
return {
|
|
||||||
valid: false,
|
|
||||||
errorMessage: `'${fieldName}' expects one of the following values: [${validOptions}] but we got '${String(
|
|
||||||
value,
|
|
||||||
)}'`,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
return { valid: true, newValue: value };
|
|
||||||
}
|
|
||||||
default: {
|
|
||||||
return { valid: true, newValue: value };
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
export const tryToParseNumber = (value: unknown): number => {
|
|
||||||
const isValidNumber = !isNaN(Number(value));
|
|
||||||
|
|
||||||
if (!isValidNumber) {
|
|
||||||
throw new ApplicationError('Failed to parse value to number', { extra: { value } });
|
|
||||||
}
|
|
||||||
return Number(value);
|
|
||||||
};
|
|
||||||
|
|
||||||
export const tryToParseBoolean = (value: unknown): value is boolean => {
|
|
||||||
if (typeof value === 'boolean') {
|
|
||||||
return value;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (typeof value === 'string' && ['true', 'false'].includes(value.toLowerCase())) {
|
|
||||||
return value.toLowerCase() === 'true';
|
|
||||||
}
|
|
||||||
|
|
||||||
// If value is not a empty string, try to parse it to a number
|
|
||||||
if (!(typeof value === 'string' && value.trim() === '')) {
|
|
||||||
const num = Number(value);
|
|
||||||
if (num === 0) {
|
|
||||||
return false;
|
|
||||||
} else if (num === 1) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
throw new ApplicationError('Failed to parse value as boolean', {
|
|
||||||
extra: { value },
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
export const tryToParseDateTime = (value: unknown): DateTime => {
|
|
||||||
const dateString = String(value).trim();
|
|
||||||
|
|
||||||
// Rely on luxon to parse different date formats
|
|
||||||
const isoDate = DateTime.fromISO(dateString, { setZone: true });
|
|
||||||
if (isoDate.isValid) {
|
|
||||||
return isoDate;
|
|
||||||
}
|
|
||||||
const httpDate = DateTime.fromHTTP(dateString, { setZone: true });
|
|
||||||
if (httpDate.isValid) {
|
|
||||||
return httpDate;
|
|
||||||
}
|
|
||||||
const rfc2822Date = DateTime.fromRFC2822(dateString, { setZone: true });
|
|
||||||
if (rfc2822Date.isValid) {
|
|
||||||
return rfc2822Date;
|
|
||||||
}
|
|
||||||
const sqlDate = DateTime.fromSQL(dateString, { setZone: true });
|
|
||||||
if (sqlDate.isValid) {
|
|
||||||
return sqlDate;
|
|
||||||
}
|
|
||||||
|
|
||||||
throw new ApplicationError('Value is not a valid date', { extra: { dateString } });
|
|
||||||
};
|
|
||||||
|
|
||||||
export const tryToParseTime = (value: unknown): string => {
|
|
||||||
const isTimeInput = /^\d{2}:\d{2}(:\d{2})?((\-|\+)\d{4})?((\-|\+)\d{1,2}(:\d{2})?)?$/s.test(
|
|
||||||
String(value),
|
|
||||||
);
|
|
||||||
if (!isTimeInput) {
|
|
||||||
throw new ApplicationError('Value is not a valid time', { extra: { value } });
|
|
||||||
}
|
|
||||||
return String(value);
|
|
||||||
};
|
|
||||||
|
|
||||||
export const tryToParseArray = (value: unknown): unknown[] => {
|
|
||||||
try {
|
|
||||||
if (typeof value === 'object' && Array.isArray(value)) {
|
|
||||||
return value;
|
|
||||||
}
|
|
||||||
|
|
||||||
let parsed;
|
|
||||||
try {
|
|
||||||
parsed = JSON.parse(String(value));
|
|
||||||
} catch (e) {
|
|
||||||
parsed = JSON.parse(String(value).replace(/'/g, '"'));
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!Array.isArray(parsed)) {
|
|
||||||
throw new ApplicationError('Value is not a valid array', { extra: { value } });
|
|
||||||
}
|
|
||||||
return parsed;
|
|
||||||
} catch (e) {
|
|
||||||
throw new ApplicationError('Value is not a valid array', { extra: { value } });
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
export const tryToParseObject = (value: unknown): object => {
|
|
||||||
if (value && typeof value === 'object' && !Array.isArray(value)) {
|
|
||||||
return value;
|
|
||||||
}
|
|
||||||
try {
|
|
||||||
const o = JSON.parse(String(value));
|
|
||||||
if (typeof o !== 'object' || Array.isArray(o)) {
|
|
||||||
throw new ApplicationError('Value is not a valid object', { extra: { value } });
|
|
||||||
}
|
|
||||||
return o;
|
|
||||||
} catch (e) {
|
|
||||||
throw new ApplicationError('Value is not a valid object', { extra: { value } });
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
/*
|
/*
|
||||||
* Validates resource locator node parameters based on validation ruled defined in each parameter mode
|
* Validates resource locator node parameters based on validation ruled defined in each parameter mode
|
||||||
*
|
*
|
||||||
|
@ -1423,7 +1245,9 @@ export const validateResourceMapperParameter = (
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (!fieldValue?.toString().startsWith('=') && field.type) {
|
if (!fieldValue?.toString().startsWith('=') && field.type) {
|
||||||
const validationResult = validateFieldType(field.id, fieldValue, field.type, field.options);
|
const validationResult = validateFieldType(field.id, fieldValue, field.type, {
|
||||||
|
valueOptions: field.options,
|
||||||
|
});
|
||||||
if (!validationResult.valid && validationResult.errorMessage) {
|
if (!validationResult.valid && validationResult.errorMessage) {
|
||||||
fieldErrors.push(validationResult.errorMessage);
|
fieldErrors.push(validationResult.errorMessage);
|
||||||
}
|
}
|
||||||
|
@ -1444,12 +1268,9 @@ export const validateParameter = (
|
||||||
const options = type === 'options' ? nodeProperties.options : undefined;
|
const options = type === 'options' ? nodeProperties.options : undefined;
|
||||||
|
|
||||||
if (!value?.toString().startsWith('=')) {
|
if (!value?.toString().startsWith('=')) {
|
||||||
const validationResult = validateFieldType(
|
const validationResult = validateFieldType(nodeName, value, type, {
|
||||||
nodeName,
|
valueOptions: options as INodePropertyOptions[],
|
||||||
value,
|
});
|
||||||
type,
|
|
||||||
options as INodePropertyOptions[],
|
|
||||||
);
|
|
||||||
|
|
||||||
if (!validationResult.valid && validationResult.errorMessage) {
|
if (!validationResult.valid && validationResult.errorMessage) {
|
||||||
return validationResult.errorMessage;
|
return validationResult.errorMessage;
|
||||||
|
@ -1583,6 +1404,14 @@ export function getParameterIssues(
|
||||||
foundIssues.parameters = { ...foundIssues.parameters, ...issues };
|
foundIssues.parameters = { ...foundIssues.parameters, ...issues };
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
} else if (nodeProperties.type === 'filter' && isDisplayed) {
|
||||||
|
const value = getParameterValueByPath(nodeValues, nodeProperties.name, path);
|
||||||
|
if (isFilterValue(value)) {
|
||||||
|
const issues = validateFilterParameter(nodeProperties, value);
|
||||||
|
if (Object.keys(issues).length > 0) {
|
||||||
|
foundIssues.parameters = { ...foundIssues.parameters, ...issues };
|
||||||
|
}
|
||||||
|
}
|
||||||
} else if (nodeProperties.validateType) {
|
} else if (nodeProperties.validateType) {
|
||||||
const value = getParameterValueByPath(nodeValues, nodeProperties.name, path);
|
const value = getParameterValueByPath(nodeValues, nodeProperties.name, path);
|
||||||
const error = validateParameter(nodeProperties, value, nodeProperties.validateType);
|
const error = validateParameter(nodeProperties, value, nodeProperties.validateType);
|
||||||
|
|
345
packages/workflow/src/NodeParameters/FilterParameter.ts
Normal file
345
packages/workflow/src/NodeParameters/FilterParameter.ts
Normal file
|
@ -0,0 +1,345 @@
|
||||||
|
import type { DateTime } from 'luxon';
|
||||||
|
import type {
|
||||||
|
FilterConditionValue,
|
||||||
|
FilterOperatorType,
|
||||||
|
FilterOptionsValue,
|
||||||
|
FilterValue,
|
||||||
|
INodeProperties,
|
||||||
|
ValidationResult,
|
||||||
|
} from '../Interfaces';
|
||||||
|
import { validateFieldType } from '../TypeValidation';
|
||||||
|
import * as LoggerProxy from '../LoggerProxy';
|
||||||
|
|
||||||
|
type Result<T, E> = { ok: true; result: T } | { ok: false; error: E };
|
||||||
|
|
||||||
|
type FilterConditionMetadata = {
|
||||||
|
index: number;
|
||||||
|
unresolvedExpressions: boolean;
|
||||||
|
itemIndex: number;
|
||||||
|
errorFormat: 'full' | 'inline';
|
||||||
|
};
|
||||||
|
|
||||||
|
export class FilterError extends Error {
|
||||||
|
constructor(
|
||||||
|
message: string,
|
||||||
|
readonly description: string,
|
||||||
|
) {
|
||||||
|
super(message);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function parseSingleFilterValue(
|
||||||
|
value: unknown,
|
||||||
|
type: FilterOperatorType,
|
||||||
|
strict = false,
|
||||||
|
): ValidationResult {
|
||||||
|
return type === 'any' || value === null || value === undefined || value === ''
|
||||||
|
? ({ valid: true, newValue: value } as ValidationResult)
|
||||||
|
: validateFieldType('filter', value, type, { strict, parseStrings: true });
|
||||||
|
}
|
||||||
|
|
||||||
|
function parseFilterConditionValues(
|
||||||
|
condition: FilterConditionValue,
|
||||||
|
options: FilterOptionsValue,
|
||||||
|
metadata: Partial<FilterConditionMetadata>,
|
||||||
|
): Result<{ left: unknown; right: unknown }, FilterError> {
|
||||||
|
const index = metadata.index ?? 0;
|
||||||
|
const itemIndex = metadata.itemIndex ?? 0;
|
||||||
|
const errorFormat = metadata.errorFormat ?? 'full';
|
||||||
|
const strict = options.typeValidation === 'strict';
|
||||||
|
const { operator } = condition;
|
||||||
|
const rightType = operator.rightType ?? operator.type;
|
||||||
|
const parsedLeftValue = parseSingleFilterValue(condition.leftValue, operator.type, strict);
|
||||||
|
const parsedRightValue = parseSingleFilterValue(condition.rightValue, rightType, strict);
|
||||||
|
const leftValid =
|
||||||
|
parsedLeftValue.valid ||
|
||||||
|
(metadata.unresolvedExpressions &&
|
||||||
|
typeof condition.leftValue === 'string' &&
|
||||||
|
condition.leftValue.startsWith('='));
|
||||||
|
const rightValid =
|
||||||
|
parsedRightValue.valid ||
|
||||||
|
!!operator.singleValue ||
|
||||||
|
(metadata.unresolvedExpressions &&
|
||||||
|
typeof condition.rightValue === 'string' &&
|
||||||
|
condition.rightValue.startsWith('='));
|
||||||
|
const leftValueString = String(condition.leftValue);
|
||||||
|
const rightValueString = String(condition.rightValue);
|
||||||
|
const errorDescription = 'Try to change the operator, or change the type with an expression';
|
||||||
|
const inCondition = errorFormat === 'full' ? ` in condition ${index + 1} ` : ' ';
|
||||||
|
const itemSuffix = `[item ${itemIndex}]`;
|
||||||
|
|
||||||
|
if (!leftValid && !rightValid) {
|
||||||
|
const providedValues = 'The provided values';
|
||||||
|
let types = `'${operator.type}'`;
|
||||||
|
if (rightType !== operator.type) {
|
||||||
|
types = `'${operator.type}' and '${rightType}' respectively`;
|
||||||
|
}
|
||||||
|
if (strict) {
|
||||||
|
return {
|
||||||
|
ok: false,
|
||||||
|
error: new FilterError(
|
||||||
|
`${providedValues} '${leftValueString}' and '${rightValueString}'${inCondition}are not of the expected type ${types} ${itemSuffix}`,
|
||||||
|
errorDescription,
|
||||||
|
),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
ok: false,
|
||||||
|
error: new FilterError(
|
||||||
|
`${providedValues} '${leftValueString}' and '${rightValueString}'${inCondition}cannot be converted to the expected type ${types} ${itemSuffix}`,
|
||||||
|
errorDescription,
|
||||||
|
),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
const composeInvalidTypeMessage = (field: 'left' | 'right', type: string, value: string) => {
|
||||||
|
const fieldNumber = field === 'left' ? 1 : 2;
|
||||||
|
|
||||||
|
if (strict) {
|
||||||
|
return `The provided value ${fieldNumber} '${value}'${inCondition}is not of the expected type '${type}' ${itemSuffix}`;
|
||||||
|
}
|
||||||
|
return `The provided value ${fieldNumber} '${value}'${inCondition}cannot be converted to the expected type '${type}' ${itemSuffix}`;
|
||||||
|
};
|
||||||
|
|
||||||
|
if (!leftValid) {
|
||||||
|
return {
|
||||||
|
ok: false,
|
||||||
|
error: new FilterError(
|
||||||
|
composeInvalidTypeMessage('left', operator.type, leftValueString),
|
||||||
|
errorDescription,
|
||||||
|
),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!rightValid) {
|
||||||
|
return {
|
||||||
|
ok: false,
|
||||||
|
error: new FilterError(
|
||||||
|
composeInvalidTypeMessage('right', rightType, rightValueString),
|
||||||
|
errorDescription,
|
||||||
|
),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
return { ok: true, result: { left: parsedLeftValue.newValue, right: parsedRightValue.newValue } };
|
||||||
|
}
|
||||||
|
|
||||||
|
export function executeFilterCondition(
|
||||||
|
condition: FilterConditionValue,
|
||||||
|
filterOptions: FilterOptionsValue,
|
||||||
|
metadata: Partial<FilterConditionMetadata> = {},
|
||||||
|
): boolean {
|
||||||
|
const ignoreCase = !filterOptions.caseSensitive;
|
||||||
|
const { operator } = condition;
|
||||||
|
const parsedValues = parseFilterConditionValues(condition, filterOptions, metadata);
|
||||||
|
|
||||||
|
if (!parsedValues.ok) {
|
||||||
|
throw parsedValues.error;
|
||||||
|
}
|
||||||
|
|
||||||
|
let { left: leftValue, right: rightValue } = parsedValues.result;
|
||||||
|
|
||||||
|
const exists = leftValue !== undefined && leftValue !== null;
|
||||||
|
if (condition.operator.operation === 'exists') {
|
||||||
|
return exists;
|
||||||
|
} else if (condition.operator.operation === 'notExists') {
|
||||||
|
return !exists;
|
||||||
|
}
|
||||||
|
|
||||||
|
switch (operator.type) {
|
||||||
|
case 'string': {
|
||||||
|
if (ignoreCase) {
|
||||||
|
if (typeof leftValue === 'string') {
|
||||||
|
leftValue = leftValue.toLocaleLowerCase();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (
|
||||||
|
typeof rightValue === 'string' &&
|
||||||
|
!(condition.operator.operation === 'regex' || condition.operator.operation === 'notRegex')
|
||||||
|
) {
|
||||||
|
rightValue = rightValue.toLocaleLowerCase();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const left = (leftValue ?? '') as string;
|
||||||
|
const right = (rightValue ?? '') as string;
|
||||||
|
|
||||||
|
switch (condition.operator.operation) {
|
||||||
|
case 'equals':
|
||||||
|
return left === right;
|
||||||
|
case 'notEquals':
|
||||||
|
return left !== right;
|
||||||
|
case 'contains':
|
||||||
|
return left.includes(right);
|
||||||
|
case 'notContains':
|
||||||
|
return !left.includes(right);
|
||||||
|
case 'startsWith':
|
||||||
|
return left.startsWith(right);
|
||||||
|
case 'notStartsWith':
|
||||||
|
return !left.startsWith(right);
|
||||||
|
case 'endsWith':
|
||||||
|
return left.endsWith(right);
|
||||||
|
case 'notEndsWith':
|
||||||
|
return !left.endsWith(right);
|
||||||
|
case 'regex':
|
||||||
|
return new RegExp(right).test(left);
|
||||||
|
case 'notRegex':
|
||||||
|
return !new RegExp(right).test(left);
|
||||||
|
}
|
||||||
|
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case 'number': {
|
||||||
|
const left = leftValue as number;
|
||||||
|
const right = rightValue as number;
|
||||||
|
|
||||||
|
switch (condition.operator.operation) {
|
||||||
|
case 'equals':
|
||||||
|
return left === right;
|
||||||
|
case 'notEquals':
|
||||||
|
return left !== right;
|
||||||
|
case 'gt':
|
||||||
|
return left > right;
|
||||||
|
case 'lt':
|
||||||
|
return left < right;
|
||||||
|
case 'gte':
|
||||||
|
return left >= right;
|
||||||
|
case 'lte':
|
||||||
|
return left <= right;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
case 'dateTime': {
|
||||||
|
const left = leftValue as DateTime;
|
||||||
|
const right = rightValue as DateTime;
|
||||||
|
|
||||||
|
if (!left || !right) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
switch (condition.operator.operation) {
|
||||||
|
case 'equals':
|
||||||
|
return left.toMillis() === right.toMillis();
|
||||||
|
case 'notEquals':
|
||||||
|
return left.toMillis() !== right.toMillis();
|
||||||
|
case 'after':
|
||||||
|
return left.toMillis() > right.toMillis();
|
||||||
|
case 'before':
|
||||||
|
return left.toMillis() < right.toMillis();
|
||||||
|
case 'afterOrEquals':
|
||||||
|
return left.toMillis() >= right.toMillis();
|
||||||
|
case 'beforeOrEquals':
|
||||||
|
return left.toMillis() <= right.toMillis();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
case 'boolean': {
|
||||||
|
const left = leftValue as boolean;
|
||||||
|
const right = rightValue as boolean;
|
||||||
|
|
||||||
|
switch (condition.operator.operation) {
|
||||||
|
case 'true':
|
||||||
|
return left;
|
||||||
|
case 'false':
|
||||||
|
return !left;
|
||||||
|
case 'equals':
|
||||||
|
return left === right;
|
||||||
|
case 'notEquals':
|
||||||
|
return left !== right;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
case 'array': {
|
||||||
|
const left = (leftValue ?? []) as unknown[];
|
||||||
|
const rightNumber = rightValue as number;
|
||||||
|
|
||||||
|
switch (condition.operator.operation) {
|
||||||
|
case 'contains':
|
||||||
|
if (ignoreCase && typeof rightValue === 'string') {
|
||||||
|
rightValue = rightValue.toLocaleLowerCase();
|
||||||
|
}
|
||||||
|
return left.includes(rightValue);
|
||||||
|
case 'notContains':
|
||||||
|
if (ignoreCase && typeof rightValue === 'string') {
|
||||||
|
rightValue = rightValue.toLocaleLowerCase();
|
||||||
|
}
|
||||||
|
return !left.includes(rightValue);
|
||||||
|
case 'lengthEquals':
|
||||||
|
return left.length === rightNumber;
|
||||||
|
case 'lengthNotEquals':
|
||||||
|
return left.length !== rightNumber;
|
||||||
|
case 'lengthGt':
|
||||||
|
return left.length > rightNumber;
|
||||||
|
case 'lengthLt':
|
||||||
|
return left.length < rightNumber;
|
||||||
|
case 'lengthGte':
|
||||||
|
return left.length >= rightNumber;
|
||||||
|
case 'lengthLte':
|
||||||
|
return left.length <= rightNumber;
|
||||||
|
case 'empty':
|
||||||
|
return left.length === 0;
|
||||||
|
case 'notEmpty':
|
||||||
|
return left.length !== 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
case 'object': {
|
||||||
|
const left = leftValue;
|
||||||
|
|
||||||
|
switch (condition.operator.operation) {
|
||||||
|
case 'empty':
|
||||||
|
return !!left && Object.keys(left).length === 0;
|
||||||
|
case 'notEmpty':
|
||||||
|
return !!left && Object.keys(left).length !== 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
LoggerProxy.warn(`Unknown filter parameter operator "${operator.type}:${operator.operation}"`);
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
type ExecuteFilterOptions = {
|
||||||
|
itemIndex?: number;
|
||||||
|
};
|
||||||
|
export function executeFilter(
|
||||||
|
value: FilterValue,
|
||||||
|
{ itemIndex }: ExecuteFilterOptions = {},
|
||||||
|
): boolean {
|
||||||
|
const conditionPass = (condition: FilterConditionValue, index: number) =>
|
||||||
|
executeFilterCondition(condition, value.options, { index, itemIndex });
|
||||||
|
|
||||||
|
if (value.combinator === 'and') {
|
||||||
|
return value.conditions.every(conditionPass);
|
||||||
|
} else if (value.combinator === 'or') {
|
||||||
|
return value.conditions.some(conditionPass);
|
||||||
|
}
|
||||||
|
|
||||||
|
LoggerProxy.warn(`Unknown filter combinator "${value.combinator as string}"`);
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const validateFilterParameter = (
|
||||||
|
nodeProperties: INodeProperties,
|
||||||
|
value: FilterValue,
|
||||||
|
): Record<string, string[]> => {
|
||||||
|
return value.conditions.reduce(
|
||||||
|
(issues, condition, index) => {
|
||||||
|
const key = `${nodeProperties.name}.${index}`;
|
||||||
|
|
||||||
|
try {
|
||||||
|
parseFilterConditionValues(condition, value.options, {
|
||||||
|
index,
|
||||||
|
unresolvedExpressions: true,
|
||||||
|
errorFormat: 'inline',
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
if (error instanceof FilterError) {
|
||||||
|
issues[key].push(error.message);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return issues;
|
||||||
|
},
|
||||||
|
{} as Record<string, string[]>,
|
||||||
|
);
|
||||||
|
};
|
232
packages/workflow/src/TypeValidation.ts
Normal file
232
packages/workflow/src/TypeValidation.ts
Normal file
|
@ -0,0 +1,232 @@
|
||||||
|
import { DateTime } from 'luxon';
|
||||||
|
import type { FieldType, INodePropertyOptions, ValidationResult } from './Interfaces';
|
||||||
|
import isObject from 'lodash/isObject';
|
||||||
|
import { ApplicationError } from './errors';
|
||||||
|
|
||||||
|
export const tryToParseNumber = (value: unknown): number => {
|
||||||
|
const isValidNumber = !isNaN(Number(value));
|
||||||
|
|
||||||
|
if (!isValidNumber) {
|
||||||
|
throw new ApplicationError('Failed to parse value to number', { extra: { value } });
|
||||||
|
}
|
||||||
|
return Number(value);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const tryToParseString = (value: unknown): string => {
|
||||||
|
if (typeof value === 'object') return JSON.stringify(value);
|
||||||
|
if (typeof value === 'undefined') return '';
|
||||||
|
if (
|
||||||
|
typeof value === 'string' ||
|
||||||
|
typeof value === 'bigint' ||
|
||||||
|
typeof value === 'boolean' ||
|
||||||
|
typeof value === 'number'
|
||||||
|
) {
|
||||||
|
return value.toString();
|
||||||
|
}
|
||||||
|
|
||||||
|
return String(value);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const tryToParseBoolean = (value: unknown): value is boolean => {
|
||||||
|
if (typeof value === 'boolean') {
|
||||||
|
return value;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (typeof value === 'string' && ['true', 'false'].includes(value.toLowerCase())) {
|
||||||
|
return value.toLowerCase() === 'true';
|
||||||
|
}
|
||||||
|
|
||||||
|
// If value is not a empty string, try to parse it to a number
|
||||||
|
if (!(typeof value === 'string' && value.trim() === '')) {
|
||||||
|
const num = Number(value);
|
||||||
|
if (num === 0) {
|
||||||
|
return false;
|
||||||
|
} else if (num === 1) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
throw new ApplicationError('Failed to parse value as boolean', {
|
||||||
|
extra: { value },
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
export const tryToParseDateTime = (value: unknown): DateTime => {
|
||||||
|
const dateString = String(value).trim();
|
||||||
|
|
||||||
|
// Rely on luxon to parse different date formats
|
||||||
|
const isoDate = DateTime.fromISO(dateString, { setZone: true });
|
||||||
|
if (isoDate.isValid) {
|
||||||
|
return isoDate;
|
||||||
|
}
|
||||||
|
const httpDate = DateTime.fromHTTP(dateString, { setZone: true });
|
||||||
|
if (httpDate.isValid) {
|
||||||
|
return httpDate;
|
||||||
|
}
|
||||||
|
const rfc2822Date = DateTime.fromRFC2822(dateString, { setZone: true });
|
||||||
|
if (rfc2822Date.isValid) {
|
||||||
|
return rfc2822Date;
|
||||||
|
}
|
||||||
|
const sqlDate = DateTime.fromSQL(dateString, { setZone: true });
|
||||||
|
if (sqlDate.isValid) {
|
||||||
|
return sqlDate;
|
||||||
|
}
|
||||||
|
|
||||||
|
throw new ApplicationError('Value is not a valid date', { extra: { dateString } });
|
||||||
|
};
|
||||||
|
|
||||||
|
export const tryToParseTime = (value: unknown): string => {
|
||||||
|
const isTimeInput = /^\d{2}:\d{2}(:\d{2})?((\-|\+)\d{4})?((\-|\+)\d{1,2}(:\d{2})?)?$/s.test(
|
||||||
|
String(value),
|
||||||
|
);
|
||||||
|
if (!isTimeInput) {
|
||||||
|
throw new ApplicationError('Value is not a valid time', { extra: { value } });
|
||||||
|
}
|
||||||
|
return String(value);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const tryToParseArray = (value: unknown): unknown[] => {
|
||||||
|
try {
|
||||||
|
if (typeof value === 'object' && Array.isArray(value)) {
|
||||||
|
return value;
|
||||||
|
}
|
||||||
|
|
||||||
|
let parsed: unknown[];
|
||||||
|
try {
|
||||||
|
parsed = JSON.parse(String(value)) as unknown[];
|
||||||
|
} catch (e) {
|
||||||
|
parsed = JSON.parse(String(value).replace(/'/g, '"')) as unknown[];
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!Array.isArray(parsed)) {
|
||||||
|
throw new ApplicationError('Value is not a valid array', { extra: { value } });
|
||||||
|
}
|
||||||
|
return parsed;
|
||||||
|
} catch (e) {
|
||||||
|
throw new ApplicationError('Value is not a valid array', { extra: { value } });
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
export const tryToParseObject = (value: unknown): object => {
|
||||||
|
if (value && typeof value === 'object' && !Array.isArray(value)) {
|
||||||
|
return value;
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
const o = JSON.parse(String(value)) as object;
|
||||||
|
if (typeof o !== 'object' || Array.isArray(o)) {
|
||||||
|
throw new ApplicationError('Value is not a valid object', { extra: { value } });
|
||||||
|
}
|
||||||
|
return o;
|
||||||
|
} catch (e) {
|
||||||
|
throw new ApplicationError('Value is not a valid object', { extra: { value } });
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
type ValidateFieldTypeOptions = Partial<{
|
||||||
|
valueOptions: INodePropertyOptions[];
|
||||||
|
strict: boolean;
|
||||||
|
parseStrings: boolean;
|
||||||
|
}>;
|
||||||
|
// Validates field against the schema and tries to parse it to the correct type
|
||||||
|
export const validateFieldType = (
|
||||||
|
fieldName: string,
|
||||||
|
value: unknown,
|
||||||
|
type: FieldType,
|
||||||
|
options: ValidateFieldTypeOptions = {},
|
||||||
|
): ValidationResult => {
|
||||||
|
if (value === null || value === undefined) return { valid: true };
|
||||||
|
const strict = options.strict ?? false;
|
||||||
|
const valueOptions = options.valueOptions ?? [];
|
||||||
|
const parseStrings = options.parseStrings ?? false;
|
||||||
|
const defaultErrorMessage = `'${fieldName}' expects a ${type} but we got '${String(value)}'`;
|
||||||
|
switch (type.toLowerCase()) {
|
||||||
|
case 'string': {
|
||||||
|
if (!parseStrings) return { valid: true, newValue: value };
|
||||||
|
try {
|
||||||
|
if (strict && typeof value !== 'string') {
|
||||||
|
return { valid: false, errorMessage: defaultErrorMessage };
|
||||||
|
}
|
||||||
|
return { valid: true, newValue: tryToParseString(value) };
|
||||||
|
} catch (e) {
|
||||||
|
return { valid: false, errorMessage: defaultErrorMessage };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
case 'number': {
|
||||||
|
try {
|
||||||
|
if (strict && typeof value !== 'number') {
|
||||||
|
return { valid: false, errorMessage: defaultErrorMessage };
|
||||||
|
}
|
||||||
|
return { valid: true, newValue: tryToParseNumber(value) };
|
||||||
|
} catch (e) {
|
||||||
|
return { valid: false, errorMessage: defaultErrorMessage };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
case 'boolean': {
|
||||||
|
try {
|
||||||
|
if (strict && typeof value !== 'boolean') {
|
||||||
|
return { valid: false, errorMessage: defaultErrorMessage };
|
||||||
|
}
|
||||||
|
return { valid: true, newValue: tryToParseBoolean(value) };
|
||||||
|
} catch (e) {
|
||||||
|
return { valid: false, errorMessage: defaultErrorMessage };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
case 'datetime': {
|
||||||
|
try {
|
||||||
|
return { valid: true, newValue: tryToParseDateTime(value) };
|
||||||
|
} catch (e) {
|
||||||
|
const luxonDocsURL =
|
||||||
|
'https://moment.github.io/luxon/api-docs/index.html#datetimefromformat';
|
||||||
|
const errorMessage = `${defaultErrorMessage} <br/><br/> Consider using <a href="${luxonDocsURL}" target="_blank"><code>DateTime.fromFormat</code></a> to work with custom date formats.`;
|
||||||
|
return { valid: false, errorMessage };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
case 'time': {
|
||||||
|
try {
|
||||||
|
return { valid: true, newValue: tryToParseTime(value) };
|
||||||
|
} catch (e) {
|
||||||
|
return {
|
||||||
|
valid: false,
|
||||||
|
errorMessage: `'${fieldName}' expects time (hh:mm:(:ss)) but we got '${String(value)}'.`,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
case 'object': {
|
||||||
|
try {
|
||||||
|
if (strict && !isObject(value)) {
|
||||||
|
return { valid: false, errorMessage: defaultErrorMessage };
|
||||||
|
}
|
||||||
|
return { valid: true, newValue: tryToParseObject(value) };
|
||||||
|
} catch (e) {
|
||||||
|
return { valid: false, errorMessage: defaultErrorMessage };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
case 'array': {
|
||||||
|
if (strict && !Array.isArray(value)) {
|
||||||
|
return { valid: false, errorMessage: defaultErrorMessage };
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
return { valid: true, newValue: tryToParseArray(value) };
|
||||||
|
} catch (e) {
|
||||||
|
return { valid: false, errorMessage: defaultErrorMessage };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
case 'options': {
|
||||||
|
const validOptions = valueOptions.map((option) => option.value).join(', ');
|
||||||
|
const isValidOption = valueOptions.some((option) => option.value === value);
|
||||||
|
|
||||||
|
if (!isValidOption) {
|
||||||
|
return {
|
||||||
|
valid: false,
|
||||||
|
errorMessage: `'${fieldName}' expects one of the following values: [${validOptions}] but we got '${String(
|
||||||
|
value,
|
||||||
|
)}'`,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
return { valid: true, newValue: value };
|
||||||
|
}
|
||||||
|
default: {
|
||||||
|
return { valid: true, newValue: value };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
|
@ -21,6 +21,7 @@ export * from './Workflow';
|
||||||
export * from './WorkflowDataProxy';
|
export * from './WorkflowDataProxy';
|
||||||
export * from './WorkflowHooks';
|
export * from './WorkflowHooks';
|
||||||
export * from './VersionedNodeType';
|
export * from './VersionedNodeType';
|
||||||
|
export * from './TypeValidation';
|
||||||
export { LoggerProxy, NodeHelpers, ObservableObject, TelemetryHelpers };
|
export { LoggerProxy, NodeHelpers, ObservableObject, TelemetryHelpers };
|
||||||
export {
|
export {
|
||||||
isObjectEmpty,
|
isObjectEmpty,
|
||||||
|
@ -40,11 +41,13 @@ export {
|
||||||
isINodePropertyCollectionList,
|
isINodePropertyCollectionList,
|
||||||
isINodePropertyOptionsList,
|
isINodePropertyOptionsList,
|
||||||
isResourceMapperValue,
|
isResourceMapperValue,
|
||||||
|
isFilterValue,
|
||||||
} from './type-guards';
|
} from './type-guards';
|
||||||
|
|
||||||
export { ExpressionExtensions } from './Extensions';
|
export { ExpressionExtensions } from './Extensions';
|
||||||
export * as ExpressionParser from './Extensions/ExpressionParser';
|
export * as ExpressionParser from './Extensions/ExpressionParser';
|
||||||
export { NativeMethods } from './NativeMethods';
|
export { NativeMethods } from './NativeMethods';
|
||||||
|
export * from './NodeParameters/FilterParameter';
|
||||||
|
|
||||||
export type { DocMetadata, NativeDoc } from './Extensions';
|
export type { DocMetadata, NativeDoc } from './Extensions';
|
||||||
|
|
||||||
|
|
|
@ -4,6 +4,7 @@ import type {
|
||||||
INodePropertyCollection,
|
INodePropertyCollection,
|
||||||
INodeParameterResourceLocator,
|
INodeParameterResourceLocator,
|
||||||
ResourceMapperValue,
|
ResourceMapperValue,
|
||||||
|
FilterValue,
|
||||||
} from './Interfaces';
|
} from './Interfaces';
|
||||||
|
|
||||||
export const isINodeProperties = (
|
export const isINodeProperties = (
|
||||||
|
@ -54,3 +55,9 @@ export const isResourceMapperValue = (value: unknown): value is ResourceMapperVa
|
||||||
'value' in value
|
'value' in value
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const isFilterValue = (value: unknown): value is FilterValue => {
|
||||||
|
return (
|
||||||
|
typeof value === 'object' && value !== null && 'conditions' in value && 'combinator' in value
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
1012
packages/workflow/test/FilterParameter.test.ts
Normal file
1012
packages/workflow/test/FilterParameter.test.ts
Normal file
File diff suppressed because it is too large
Load diff
|
@ -1,4 +1,4 @@
|
||||||
import { validateFieldType } from '@/NodeHelpers';
|
import { validateFieldType } from '@/TypeValidation';
|
||||||
import type { DateTime } from 'luxon';
|
import type { DateTime } from 'luxon';
|
||||||
|
|
||||||
const VALID_ISO_DATES = [
|
const VALID_ISO_DATES = [
|
||||||
|
@ -174,16 +174,20 @@ describe('Type Validation', () => {
|
||||||
|
|
||||||
it('should validate options properly', () => {
|
it('should validate options properly', () => {
|
||||||
expect(
|
expect(
|
||||||
validateFieldType('options', 'oranges', 'options', [
|
validateFieldType('options', 'oranges', 'options', {
|
||||||
{ name: 'apples', value: 'apples' },
|
valueOptions: [
|
||||||
{ name: 'oranges', value: 'oranges' },
|
{ name: 'apples', value: 'apples' },
|
||||||
]).valid,
|
{ name: 'oranges', value: 'oranges' },
|
||||||
|
],
|
||||||
|
}).valid,
|
||||||
).toEqual(true);
|
).toEqual(true);
|
||||||
expect(
|
expect(
|
||||||
validateFieldType('options', 'something else', 'options', [
|
validateFieldType('options', 'something else', 'options', {
|
||||||
{ name: 'apples', value: 'apples' },
|
valueOptions: [
|
||||||
{ name: 'oranges', value: 'oranges' },
|
{ name: 'apples', value: 'apples' },
|
||||||
]).valid,
|
{ name: 'oranges', value: 'oranges' },
|
||||||
|
],
|
||||||
|
}).valid,
|
||||||
).toEqual(false);
|
).toEqual(false);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -202,4 +206,24 @@ describe('Type Validation', () => {
|
||||||
expect(validateFieldType('time', '23:23:', 'time').valid).toEqual(false);
|
expect(validateFieldType('time', '23:23:', 'time').valid).toEqual(false);
|
||||||
expect(validateFieldType('time', '23::23::23', 'time').valid).toEqual(false);
|
expect(validateFieldType('time', '23::23::23', 'time').valid).toEqual(false);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe('options', () => {
|
||||||
|
describe('strict=true', () => {
|
||||||
|
it('should not convert/cast types', () => {
|
||||||
|
const options = { strict: true };
|
||||||
|
expect(validateFieldType('test', '42', 'number', options).valid).toBe(false);
|
||||||
|
expect(validateFieldType('test', 'true', 'boolean', options).valid).toBe(false);
|
||||||
|
expect(validateFieldType('test', [], 'object', options).valid).toBe(false);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('parseStrings=true', () => {
|
||||||
|
it('should parse strings from other types', () => {
|
||||||
|
const options = { parseStrings: true };
|
||||||
|
expect(validateFieldType('test', 42, 'string').newValue).toBe(42);
|
||||||
|
expect(validateFieldType('test', 42, 'string', options).newValue).toBe('42');
|
||||||
|
expect(validateFieldType('test', true, 'string', options).newValue).toBe('true');
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
Loading…
Reference in a new issue