From 95b39bcb767539fedc5db31c748ce79bd10cb6ed Mon Sep 17 00:00:00 2001 From: Elias Meire Date: Mon, 16 Oct 2023 15:08:44 +0200 Subject: [PATCH 01/39] Add filter component --- .../components/Filter/CombinatorSelect.vue | 38 +++++++++ .../src/components/Filter/Condition.vue | 71 ++++++++++++++++ .../src/components/Filter/Filter.vue | 57 +++++++++++++ .../src/components/Filter/OperatorSelect.vue | 40 +++++++++ .../src/components/Filter/constants.ts | 82 +++++++++++++++++++ .../editor-ui/src/components/Filter/types.ts | 21 +++++ .../src/plugins/i18n/locales/en.json | 42 +++++++++- packages/workflow/src/Interfaces.ts | 44 ++++++++-- 8 files changed, 385 insertions(+), 10 deletions(-) create mode 100644 packages/editor-ui/src/components/Filter/CombinatorSelect.vue create mode 100644 packages/editor-ui/src/components/Filter/Condition.vue create mode 100644 packages/editor-ui/src/components/Filter/Filter.vue create mode 100644 packages/editor-ui/src/components/Filter/OperatorSelect.vue create mode 100644 packages/editor-ui/src/components/Filter/constants.ts create mode 100644 packages/editor-ui/src/components/Filter/types.ts diff --git a/packages/editor-ui/src/components/Filter/CombinatorSelect.vue b/packages/editor-ui/src/components/Filter/CombinatorSelect.vue new file mode 100644 index 0000000000000..e1565775dfb0b --- /dev/null +++ b/packages/editor-ui/src/components/Filter/CombinatorSelect.vue @@ -0,0 +1,38 @@ + + + + + diff --git a/packages/editor-ui/src/components/Filter/Condition.vue b/packages/editor-ui/src/components/Filter/Condition.vue new file mode 100644 index 0000000000000..241546b70cc9b --- /dev/null +++ b/packages/editor-ui/src/components/Filter/Condition.vue @@ -0,0 +1,71 @@ + + + + + diff --git a/packages/editor-ui/src/components/Filter/Filter.vue b/packages/editor-ui/src/components/Filter/Filter.vue new file mode 100644 index 0000000000000..0b35e77c67333 --- /dev/null +++ b/packages/editor-ui/src/components/Filter/Filter.vue @@ -0,0 +1,57 @@ + + + + + diff --git a/packages/editor-ui/src/components/Filter/OperatorSelect.vue b/packages/editor-ui/src/components/Filter/OperatorSelect.vue new file mode 100644 index 0000000000000..cf38d80d76ccf --- /dev/null +++ b/packages/editor-ui/src/components/Filter/OperatorSelect.vue @@ -0,0 +1,40 @@ + + + + + diff --git a/packages/editor-ui/src/components/Filter/constants.ts b/packages/editor-ui/src/components/Filter/constants.ts new file mode 100644 index 0000000000000..b9bd8e154729f --- /dev/null +++ b/packages/editor-ui/src/components/Filter/constants.ts @@ -0,0 +1,82 @@ +/* eslint-disable @typescript-eslint/naming-convention */ +import type { FilterOperator, FilterOperatorGroup } from './types'; + +export const DEFAULT_MAX_CONDITIONS = 5; + +export const OPERATORS_BY_ID = { + 'any:exists': { id: 'any:exists', name: 'filter.operator.exists', singleValue: true }, + 'any:notExists': { id: 'any:notExists', name: 'filter.operator.notExists', singleValue: true }, + 'string:equals': { id: 'string:equals', name: 'filter.operator.equals' }, + 'string:notEquals': { id: 'string:notEquals', name: 'filter.operator.notEquals' }, + 'string:contains': { id: 'string:contains', name: 'filter.operator.contains' }, + 'string:notContains': { id: 'string:notContains', name: 'filter.operator.notContains' }, + 'string:startsWith': { id: 'string:startsWith', name: 'filter.operator.startsWith' }, + 'string:notStartsWith': { id: 'string:notStartsWith', name: 'filter.operator.notStartsWith' }, + 'string:endsWith': { id: 'string:endsWith', name: 'filter.operator.endsWith' }, + 'string:notEndsWith': { id: 'string:notEndsWith', name: 'filter.operator.notEndsWith' }, + 'string:regex': { id: 'string:regex', name: 'filter.operator.regex' }, + 'string:notRegex': { id: 'string:notRegex', name: 'filter.operator.notRegex' }, + 'number:equals': { id: 'number:equals', name: 'filter.operator.equals' }, + 'number:notEquals': { id: 'number:notEquals', name: 'filter.operator.notEquals' }, + 'number:gt': { id: 'number:gt', name: 'filter.operator.gt' }, + 'number:lt': { id: 'number:lt', name: 'filter.operator.lt' }, + 'number:gte': { id: 'number:gte', name: 'filter.operator.gte' }, + 'number:lte': { id: 'number:lte', name: 'filter.operator.lte' }, + 'date:equals': { id: 'date:equals', name: 'filter.operator.equals' }, + 'date:notEquals': { id: 'date:notEquals', name: 'filter.operator.notEquals' }, + 'date:after': { id: 'date:after', name: 'filter.operator.after' }, + 'date:before': { id: 'date:before', name: 'filter.operator.before' }, + 'date:afterOrEquals': { id: 'date:afterOrEquals', name: 'filter.operator.afterOrEquals' }, + 'date:beforeOrEquals': { id: 'date:beforeOrEquals', name: 'filter.operator.beforeOrEquals' }, + 'boolean:true': { id: 'boolean:true', name: 'filter.operator.true', singleValue: true }, + 'boolean:false': { id: 'boolean:false', name: 'filter.operator.false', singleValue: true }, + 'boolean:equals': { id: 'boolean:equals', name: 'filter.operator.equals' }, + 'boolean:notEquals': { id: 'boolean:notEquals', name: 'filter.operator.notEquals' }, + 'array:contains': { id: 'array:contains', name: 'filter.operator.contains' }, + 'array:notContains': { id: 'array:notContains', name: 'filter.operator.notContains' }, + 'array:lengthEquals': { id: 'array:lengthEquals', name: 'filter.operator.lengthEquals' }, + 'array:lengthNotEquals': { id: 'array:lengthNotEquals', name: 'filter.operator.lengthNotEquals' }, + 'array:lengthGt': { id: 'array:lengthGt', name: 'filter.operator.lengthGt' }, + 'array:lengthLt': { id: 'array:lengthLt', name: 'filter.operator.lengthLt' }, + 'array:lengthGte': { id: 'array:lengthGte', name: 'filter.operator.lengthGte' }, + 'array:lengthLte': { id: 'array:lengthLte', name: 'filter.operator.lengthLte' }, + 'object:empty': { id: 'object:empty', name: 'filter.operator.empty', singleValue: true }, + 'object:notEmpty': { id: 'object:notEmpty', name: 'filter.operator.notEmpty', singleValue: true }, +} as const satisfies Record; + +export const OPERATORS = Object.values(OPERATORS_BY_ID); + +export type FilterOperatorId = keyof typeof OPERATORS_BY_ID; + +export const DEFAULT_OPERATOR: FilterOperatorId = 'any:exists'; + +export const OPERATOR_GROUPS: FilterOperatorGroup[] = [ + { + name: 'filter.operatorGroup.basic', + children: OPERATORS.filter((operator) => operator.id.startsWith('any')), + }, + { + name: 'filter.operatorGroup.string', + children: OPERATORS.filter((operator) => operator.id.startsWith('string')), + }, + { + name: 'filter.operatorGroup.number', + children: OPERATORS.filter((operator) => operator.id.startsWith('number')), + }, + { + name: 'filter.operatorGroup.date', + children: OPERATORS.filter((operator) => operator.id.startsWith('date')), + }, + { + name: 'filter.operatorGroup.boolean', + children: OPERATORS.filter((operator) => operator.id.startsWith('boolean')), + }, + { + name: 'filter.operatorGroup.array', + children: OPERATORS.filter((operator) => operator.id.startsWith('array')), + }, + { + name: 'filter.operatorGroup.object', + children: OPERATORS.filter((operator) => operator.id.startsWith('object')), + }, +]; diff --git a/packages/editor-ui/src/components/Filter/types.ts b/packages/editor-ui/src/components/Filter/types.ts new file mode 100644 index 0000000000000..848780d10056a --- /dev/null +++ b/packages/editor-ui/src/components/Filter/types.ts @@ -0,0 +1,21 @@ +import type { BaseTextKey } from '@/plugins/i18n'; +export type OperatorSubjectType = + | 'string' + | 'number' + | 'boolean' + | 'array' + | 'object' + | 'date' + | 'other' + | 'any'; + +export interface FilterOperator { + id: `${OperatorSubjectType}:${string}`; + name: BaseTextKey; + singleValue?: boolean; // default = false +} + +export interface FilterOperatorGroup { + name: BaseTextKey; + children: Array; +} diff --git a/packages/editor-ui/src/plugins/i18n/locales/en.json b/packages/editor-ui/src/plugins/i18n/locales/en.json index 3d4739bd728a2..12aeb1fff63be 100644 --- a/packages/editor-ui/src/plugins/i18n/locales/en.json +++ b/packages/editor-ui/src/plugins/i18n/locales/en.json @@ -2197,5 +2197,45 @@ "executionUsage.label.executions": "Executions", "executionUsage.button.upgrade": "Upgrade plan", "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": "Text", + "filter.operatorGroup.number": "Numbers", + "filter.operatorGroup.date": "Dates", + "filter.operatorGroup.boolean": "Boolean", + "filter.operatorGroup.array": "Array", + "filter.operatorGroup.object": "Object", + "filter.operator.equals": "Equal to", + "filter.operator.notEquals": "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 pattern", + "filter.operator.notRegex": "Does not match pattern", + "filter.operator.gt": "Greater than", + "filter.operator.lt": "Less than", + "filter.operator.gte": "Greater than or equal", + "filter.operator.lte": "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": "True", + "filter.operator.false": "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": "Empty", + "filter.operator.notEmpty": "Not empty", + "filter.combinator.or": "Or", + "filter.combinator.and": "And", + "filter.addCondition": "Add condition" } diff --git a/packages/workflow/src/Interfaces.ts b/packages/workflow/src/Interfaces.ts index 595b4b65f24b7..1873441be6b87 100644 --- a/packages/workflow/src/Interfaces.ts +++ b/packages/workflow/src/Interfaces.ts @@ -2,23 +2,23 @@ import type * as express from 'express'; import type FormData from 'form-data'; +import type { PathLike } from 'fs'; import type { IncomingHttpHeaders } from 'http'; -import type { Readable } from 'stream'; -import type { URLSearchParams } from 'url'; import type { OptionsWithUri, OptionsWithUrl } from 'request'; import type { RequestPromiseOptions } from 'request-promise-native'; -import type { PathLike } from 'fs'; +import type { Readable } from 'stream'; +import type { URLSearchParams } from 'url'; +import type { AuthenticationMethod } from './Authentication'; import type { CODE_EXECUTION_MODES, CODE_LANGUAGES } from './Constants'; import type { IDeferredPromise } from './DeferredPromise'; +import type { ExecutionStatus } from './ExecutionStatus'; +import type { ExpressionError } from './ExpressionError'; +import type { NodeApiError, NodeOperationError } from './NodeErrors'; import type { Workflow } from './Workflow'; -import type { WorkflowHooks } from './WorkflowHooks'; import type { WorkflowActivationError } from './WorkflowActivationError'; import type { WorkflowOperationError } from './WorkflowErrors'; -import type { NodeApiError, NodeOperationError } from './NodeErrors'; -import type { ExpressionError } from './ExpressionError'; -import type { ExecutionStatus } from './ExecutionStatus'; -import type { AuthenticationMethod } from './Authentication'; +import type { WorkflowHooks } from './WorkflowHooks'; export interface IAdditionalCredentialOptions { oauth2?: IOAuth2Options; @@ -1047,7 +1047,8 @@ export type NodePropertyTypes = | 'credentialsSelect' | 'resourceLocator' | 'curlImport' - | 'resourceMapper'; + | 'resourceMapper' + | 'filter'; export type CodeAutocompleteTypes = 'function' | 'functionItem'; @@ -1093,6 +1094,7 @@ export interface INodePropertyTypeOptions { sortable?: boolean; // Supported when "multipleValues" set to true expirable?: boolean; // Supported by: hidden (only in the credentials) resourceMapper?: ResourceMapperTypeOptions; + filter?: FilterTypeOptions; [key: string]: any; } @@ -1112,6 +1114,17 @@ export interface ResourceMapperTypeOptions { }; } +type NonEmptyArray = [T, ...T[]]; + +export type FilterTypeCombinator = 'and' | 'or'; + +export interface FilterTypeOptions { + caseSensitive?: boolean; // default = false + leftValue?: string; // when set, user can't edit left side of condition + allowedCombinators?: NonEmptyArray; // default = ['and', 'or'] + maxConditions?: number; // default = 5 +} + export interface IDisplayOptions { hide?: { [key: string]: NodeParameterValue[] | undefined; @@ -2174,6 +2187,19 @@ export type ResourceMapperValue = { matchingColumns: string[]; schema: ResourceMapperField[]; }; + +export type FilterConditionValue = { + leftValue: string; + rightValue: string; + operator: string; +}; + +export type FilterValue = { + value: boolean; + conditions: FilterConditionValue[]; + combinator: FilterTypeCombinator; +}; + export interface ExecutionOptions { limit?: number; } From edadc73e93e596eac13bb128762867ca7554c8f4 Mon Sep 17 00:00:00 2001 From: Elias Meire Date: Mon, 23 Oct 2023 10:10:48 +0200 Subject: [PATCH 02/39] Filter component UI --- .../components/N8nInputLabel/InputLabel.vue | 17 +- .../design-system/src/css/common/var.scss | 21 + packages/design-system/src/css/input.scss | 10 + .../src/components/Filter/Condition.vue | 96 +++- .../src/components/Filter/Filter.vue | 91 +++- .../src/components/Filter/OperatorSelect.vue | 54 +- .../src/components/Filter/constants.ts | 13 + .../editor-ui/src/components/Filter/types.ts | 4 +- .../src/components/ParameterInputFull.vue | 6 +- .../src/components/ParameterInputList.vue | 9 + .../src/components/ParameterInputWrapper.vue | 51 +- .../src/plugins/i18n/locales/en.json | 46 +- packages/nodes-base/nodes/If/If.node.ts | 502 +----------------- packages/nodes-base/nodes/If/V1/IfV1.node.ts | 486 +++++++++++++++++ packages/nodes-base/nodes/If/V2/IfV2.node.ts | 41 ++ 15 files changed, 865 insertions(+), 582 deletions(-) create mode 100644 packages/nodes-base/nodes/If/V1/IfV1.node.ts create mode 100644 packages/nodes-base/nodes/If/V2/IfV2.node.ts diff --git a/packages/design-system/src/components/N8nInputLabel/InputLabel.vue b/packages/design-system/src/components/N8nInputLabel/InputLabel.vue index f029f16722a9f..11278557c3364 100644 --- a/packages/design-system/src/components/N8nInputLabel/InputLabel.vue +++ b/packages/design-system/src/components/N8nInputLabel/InputLabel.vue @@ -190,21 +190,20 @@ export default defineComponent({ opacity: 1; } -.heading { - display: flex; -} - .overflow { overflow-x: hidden; overflow-y: clip; } -.small { - margin-bottom: var(--spacing-5xs); -} +.heading { + display: flex; -.medium { - margin-bottom: var(--spacing-2xs); + &.small { + margin-bottom: var(--spacing-5xs); + } + &.medium { + margin-bottom: var(--spacing-2xs); + } } .underline { diff --git a/packages/design-system/src/css/common/var.scss b/packages/design-system/src/css/common/var.scss index 24ea1e8cad5b1..1a3d015d354ca 100644 --- a/packages/design-system/src/css/common/var.scss +++ b/packages/design-system/src/css/common/var.scss @@ -530,6 +530,10 @@ $input-border-color: var(--input-border-color, var(--border-color-base)); $input-border-style: var(--input-border-style, var(--border-style-base)); $input-border-width: var(--border-width-base); $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-font-size: var(--input-font-size, var(--font-size-s)); /// color||Color|0 @@ -540,6 +544,23 @@ $input-width: 140px; $input-height: 40px; /// borderRadius||Border|2 $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; /// color||Color|0 $input-background-color: var(--input-background-color, var(--color-foreground-xlight)); diff --git a/packages/design-system/src/css/input.scss b/packages/design-system/src/css/input.scss index cab4d87b28690..f008565939359 100644 --- a/packages/design-system/src/css/input.scss +++ b/packages/design-system/src/css/input.scss @@ -20,6 +20,11 @@ background-color: var.$input-background-color; background-image: none; 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; &, @@ -108,7 +113,12 @@ background-color: var.$input-background-color; background-image: none; 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-right-color: var.$input-border-right-color; box-sizing: border-box; color: var.$input-font-color; display: inline-block; diff --git a/packages/editor-ui/src/components/Filter/Condition.vue b/packages/editor-ui/src/components/Filter/Condition.vue index 241546b70cc9b..752960702f51b 100644 --- a/packages/editor-ui/src/components/Filter/Condition.vue +++ b/packages/editor-ui/src/components/Filter/Condition.vue @@ -1,63 +1,73 @@