diff --git a/packages/editor-ui/src/components/FilterConditions/FilterConditions.vue b/packages/editor-ui/src/components/FilterConditions/FilterConditions.vue index 095015d0c..6b58fb429 100644 --- a/packages/editor-ui/src/components/FilterConditions/FilterConditions.vue +++ b/packages/editor-ui/src/components/FilterConditions/FilterConditions.vue @@ -42,6 +42,8 @@ const i18n = useI18n(); const ndvStore = useNDVStore(); const { debounce } = useDebounce(); +const debouncedEmitChange = debounce(emitChange, { debounceTime: 1000 }); + function createCondition(): FilterConditionValue { return { id: uuid(), leftValue: '', rightValue: '', operator: DEFAULT_OPERATOR_VALUE }; } @@ -86,7 +88,7 @@ watch( try { newOptions = { ...DEFAULT_FILTER_OPTIONS, - ...resolveParameter(typeOptions as NodeParameterValue), + ...resolveParameter(typeOptions as unknown as NodeParameterValue), }; } catch (error) {} @@ -117,8 +119,6 @@ function emitChange() { }); } -const debouncedEmitChange = debounce(emitChange, { debounceTime: 1000 }); - function addCondition(): void { state.paramValue.conditions.push(createCondition()); debouncedEmitChange(); diff --git a/packages/editor-ui/src/components/FilterConditions/constants.ts b/packages/editor-ui/src/components/FilterConditions/constants.ts index 4da26b911..4410191cf 100644 --- a/packages/editor-ui/src/components/FilterConditions/constants.ts +++ b/packages/editor-ui/src/components/FilterConditions/constants.ts @@ -8,6 +8,7 @@ export const DEFAULT_FILTER_OPTIONS: FilterOptionsValue = { caseSensitive: true, leftValue: '', typeValidation: 'strict', + version: 1, }; export const OPERATORS_BY_ID = { diff --git a/packages/nodes-base/nodes/Filter/Filter.node.ts b/packages/nodes-base/nodes/Filter/Filter.node.ts index 7eb36ca4f..01db3b986 100644 --- a/packages/nodes-base/nodes/Filter/Filter.node.ts +++ b/packages/nodes-base/nodes/Filter/Filter.node.ts @@ -13,13 +13,14 @@ export class Filter extends VersionedNodeType { iconColor: 'light-blue', group: ['transform'], description: 'Remove items matching a condition', - defaultVersion: 2.1, + defaultVersion: 2.2, }; const nodeVersions: IVersionedNodeType['nodeVersions'] = { 1: new FilterV1(baseDescription), 2: new FilterV2(baseDescription), 2.1: new FilterV2(baseDescription), + 2.2: new FilterV2(baseDescription), }; super(nodeVersions, baseDescription); diff --git a/packages/nodes-base/nodes/Filter/V2/FilterV2.node.ts b/packages/nodes-base/nodes/Filter/V2/FilterV2.node.ts index 0bb9e0505..46c32d591 100644 --- a/packages/nodes-base/nodes/Filter/V2/FilterV2.node.ts +++ b/packages/nodes-base/nodes/Filter/V2/FilterV2.node.ts @@ -19,7 +19,7 @@ export class FilterV2 implements INodeType { constructor(baseDescription: INodeTypeBaseDescription) { this.description = { ...baseDescription, - version: [2, 2.1], + version: [2, 2.1, 2.2], defaults: { name: 'Filter', color: '#229eff', @@ -39,6 +39,7 @@ export class FilterV2 implements INodeType { filter: { caseSensitive: '={{!$parameter.options.ignoreCase}}', typeValidation: getTypeValidationStrictness(2.1), + version: '={{ $nodeVersion >= 2.2 ? 2 : 1 }}', }, }, }, diff --git a/packages/nodes-base/nodes/If/If.node.ts b/packages/nodes-base/nodes/If/If.node.ts index 4942fdebf..48732ea75 100644 --- a/packages/nodes-base/nodes/If/If.node.ts +++ b/packages/nodes-base/nodes/If/If.node.ts @@ -13,13 +13,14 @@ export class If extends VersionedNodeType { iconColor: 'green', group: ['transform'], description: 'Route items to different branches (true/false)', - defaultVersion: 2.1, + defaultVersion: 2.2, }; const nodeVersions: IVersionedNodeType['nodeVersions'] = { 1: new IfV1(baseDescription), 2: new IfV2(baseDescription), 2.1: new IfV2(baseDescription), + 2.2: new IfV2(baseDescription), }; super(nodeVersions, baseDescription); diff --git a/packages/nodes-base/nodes/If/V2/IfV2.node.ts b/packages/nodes-base/nodes/If/V2/IfV2.node.ts index 4eab5d4f3..6ef2dcb62 100644 --- a/packages/nodes-base/nodes/If/V2/IfV2.node.ts +++ b/packages/nodes-base/nodes/If/V2/IfV2.node.ts @@ -19,7 +19,7 @@ export class IfV2 implements INodeType { constructor(baseDescription: INodeTypeBaseDescription) { this.description = { ...baseDescription, - version: [2, 2.1], + version: [2, 2.1, 2.2], defaults: { name: 'If', color: '#408000', @@ -39,6 +39,7 @@ export class IfV2 implements INodeType { filter: { caseSensitive: '={{!$parameter.options.ignoreCase}}', typeValidation: getTypeValidationStrictness(2.1), + version: '={{ $nodeVersion >= 2.2 ? 2 : 1 }}', }, }, }, diff --git a/packages/nodes-base/nodes/If/test/v2/IfV2.boolean.json b/packages/nodes-base/nodes/If/test/v2/IfV2.boolean.json new file mode 100644 index 000000000..3579d00c4 --- /dev/null +++ b/packages/nodes-base/nodes/If/test/v2/IfV2.boolean.json @@ -0,0 +1,174 @@ +{ + "name": "Filter test: boolean", + "nodes": [ + { + "parameters": {}, + "id": "9e2c2dc5-bd37-460b-a5a4-943860dcc03e", + "name": "When clicking \"Execute Workflow\"", + "type": "n8n-nodes-base.manualTrigger", + "typeVersion": 1, + "position": [ + -720, + 160 + ] + }, + { + "parameters": {}, + "id": "3184fda2-b1d0-400a-a882-5844bbe99ae3", + "name": "false", + "type": "n8n-nodes-base.noOp", + "typeVersion": 1, + "position": [ + 0, + 260 + ] + }, + { + "parameters": { + "jsCode": "return [\n {\n email: \"shane@yahoo.com\",\n admin: false\n },\n {\n email: \"sharon@yahoo.com\",\n admin: true\n },\n {\n email: \"sarah@gmail.com\",\n admin: 'false'\n },\n {\n email: \"tom@gmail.com\",\n admin: '0'\n },\n {\n email: \"jane@gmail.com\",\n admin: 1\n }\n]" + }, + "id": "85de5f5c-0a4c-4da1-805b-9e056089bcd5", + "name": "Code", + "type": "n8n-nodes-base.code", + "typeVersion": 2, + "position": [ + -500, + 160 + ] + }, + { + "parameters": {}, + "id": "8577ab3b-b9f8-4c4d-a3a1-abb6a9269473", + "name": "true", + "type": "n8n-nodes-base.noOp", + "typeVersion": 1, + "position": [ + 0, + 100 + ] + }, + { + "parameters": { + "conditions": { + "options": { + "caseSensitive": true, + "leftValue": "", + "typeValidation": "loose", + "version": 2 + }, + "conditions": [ + { + "id": "307e4ea0-3a82-4722-aca6-68d882115e8b", + "leftValue": "={{ $json.admin }}", + "rightValue": "", + "operator": { + "type": "boolean", + "operation": "true", + "singleValue": true + } + } + ], + "combinator": "and" + }, + "looseTypeValidation": true, + "options": {} + }, + "type": "n8n-nodes-base.if", + "typeVersion": 2.2, + "position": [ + -280, + 160 + ], + "id": "d5d17556-45e6-44a1-8580-a08395ca38c4", + "name": "loose" + } + ], + "pinData": { + "true": [ + { + "json": { + "email": "sharon@yahoo.com", + "admin": true + } + }, + { + "json": { + "email": "jane@gmail.com", + "admin": 1 + } + } + ], + "false": [ + { + "json": { + "email": "shane@yahoo.com", + "admin": false + } + }, + { + "json": { + "email": "sarah@gmail.com", + "admin": "false" + } + }, + { + "json": { + "email": "tom@gmail.com", + "admin": "0" + } + } + ] + }, + "connections": { + "When clicking \"Execute Workflow\"": { + "main": [ + [ + { + "node": "Code", + "type": "main", + "index": 0 + } + ] + ] + }, + "Code": { + "main": [ + [ + { + "node": "loose", + "type": "main", + "index": 0 + } + ] + ] + }, + "loose": { + "main": [ + [ + { + "node": "true", + "type": "main", + "index": 0 + } + ], + [ + { + "node": "false", + "type": "main", + "index": 0 + } + ] + ] + } + }, + "active": false, + "settings": { + "executionOrder": "v1" + }, + "versionId": "35631b37-dc5e-4155-a54f-41b38584f38e", + "meta": { + "instanceId": "27cc9b56542ad45b38725555722c50a1c3fee1670bbb67980558314ee08517c4" + }, + "id": "JQsdJ4gnZtuDb7Oo", + "tags": [] +} diff --git a/packages/nodes-base/nodes/Switch/Switch.node.ts b/packages/nodes-base/nodes/Switch/Switch.node.ts index fb7262f74..1a85852a1 100644 --- a/packages/nodes-base/nodes/Switch/Switch.node.ts +++ b/packages/nodes-base/nodes/Switch/Switch.node.ts @@ -14,7 +14,7 @@ export class Switch extends VersionedNodeType { iconColor: 'light-blue', group: ['transform'], description: 'Route items depending on defined expression or rules', - defaultVersion: 3.1, + defaultVersion: 3.2, }; const nodeVersions: IVersionedNodeType['nodeVersions'] = { @@ -22,6 +22,7 @@ export class Switch extends VersionedNodeType { 2: new SwitchV2(baseDescription), 3: new SwitchV3(baseDescription), 3.1: new SwitchV3(baseDescription), + 3.2: new SwitchV3(baseDescription), }; super(nodeVersions, baseDescription); diff --git a/packages/nodes-base/nodes/Switch/V3/SwitchV3.node.ts b/packages/nodes-base/nodes/Switch/V3/SwitchV3.node.ts index 6892bc2d2..ccc634597 100644 --- a/packages/nodes-base/nodes/Switch/V3/SwitchV3.node.ts +++ b/packages/nodes-base/nodes/Switch/V3/SwitchV3.node.ts @@ -50,7 +50,7 @@ export class SwitchV3 implements INodeType { this.description = { ...baseDescription, subtitle: `=mode: {{(${capitalize})($parameter["mode"])}}`, - version: [3, 3.1], + version: [3, 3.1, 3.2], defaults: { name: 'Switch', color: '#506000', @@ -160,6 +160,7 @@ export class SwitchV3 implements INodeType { filter: { caseSensitive: '={{!$parameter.options.ignoreCase}}', typeValidation: getTypeValidationStrictness(3.1), + version: '={{ $nodeVersion >= 3.2 ? 2 : 1 }}', }, }, }, diff --git a/packages/nodes-base/utils/constants.ts b/packages/nodes-base/utils/constants.ts index b41e2bd05..592f23d90 100644 --- a/packages/nodes-base/utils/constants.ts +++ b/packages/nodes-base/utils/constants.ts @@ -4,4 +4,4 @@ export const NODE_RAN_MULTIPLE_TIMES_WARNING = export const LOCALHOST = '127.0.0.1'; export const ENABLE_LESS_STRICT_TYPE_VALIDATION = - "Try changing the type of comparison. Alternatively you can enable 'Less Strict Type Validation' in the options."; + "Try changing the type of comparison. Alternatively you can enable 'Convert Value Types'."; diff --git a/packages/nodes-base/utils/descriptions.ts b/packages/nodes-base/utils/descriptions.ts index 75a1116c8..6921ca8c6 100644 --- a/packages/nodes-base/utils/descriptions.ts +++ b/packages/nodes-base/utils/descriptions.ts @@ -34,7 +34,7 @@ export const returnAllOrLimit: INodeProperties[] = [ ]; export const looseTypeValidationProperty: INodeProperties = { - displayName: 'Less Strict Type Validation', + displayName: 'Convert Value Types', description: 'Whether to try casting value types based on the selected operator', name: 'looseTypeValidation', type: 'boolean', diff --git a/packages/workflow/src/Interfaces.ts b/packages/workflow/src/Interfaces.ts index ae6ce96f7..c8e3f307c 100644 --- a/packages/workflow/src/Interfaces.ts +++ b/packages/workflow/src/Interfaces.ts @@ -1292,13 +1292,14 @@ type NonEmptyArray = [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; // 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 type FilterTypeOptions = { + version: 1 | 2 | {}; // required so nodes are pinned on a version + caseSensitive?: boolean | string; // default = true + leftValue?: string; // when set, user can't edit left side of condition + allowedCombinators?: NonEmptyArray; // default = ['and', 'or'] + maxConditions?: number; // default = 10 + typeValidation?: 'strict' | 'loose' | {}; // default = strict, `| {}` is a TypeScript trick to allow custom strings (expressions), but still give autocomplete +}; export type AssignmentTypeOptions = Partial<{ hideType?: boolean; // visible by default @@ -2554,6 +2555,7 @@ export type FilterOptionsValue = { caseSensitive: boolean; leftValue: string; typeValidation: 'strict' | 'loose'; + version: 1 | 2; }; export type FilterValue = { diff --git a/packages/workflow/src/NodeParameters/FilterParameter.ts b/packages/workflow/src/NodeParameters/FilterParameter.ts index 2c2fabf96..39ca0af9d 100644 --- a/packages/workflow/src/NodeParameters/FilterParameter.ts +++ b/packages/workflow/src/NodeParameters/FilterParameter.ts @@ -32,12 +32,18 @@ function parseSingleFilterValue( value: unknown, type: FilterOperatorType, strict = false, + version: FilterOptionsValue['version'] = 1, ): ValidationResult { if (type === 'any' || value === null || value === undefined) { return { valid: true, newValue: value } as ValidationResult; } if (type === 'boolean' && !strict) { + if (version >= 2) { + const result = validateFieldType('filter', value, type); + if (result.valid) return result; + } + return { valid: true, newValue: Boolean(value) }; } @@ -53,6 +59,7 @@ const withIndefiniteArticle = (noun: string): string => { return `${article} ${noun}`; }; +// eslint-disable-next-line complexity function parseFilterConditionValues( condition: FilterConditionValue, options: FilterOptionsValue, @@ -62,10 +69,16 @@ function parseFilterConditionValues( const itemIndex = metadata.itemIndex ?? 0; const errorFormat = metadata.errorFormat ?? 'full'; const strict = options.typeValidation === 'strict'; + const version = options.version ?? 1; 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 parsedLeftValue = parseSingleFilterValue( + condition.leftValue, + operator.type, + strict, + version, + ); + const parsedRightValue = parseSingleFilterValue(condition.rightValue, rightType, strict, version); const leftValid = parsedLeftValue.valid || (metadata.unresolvedExpressions && @@ -96,7 +109,7 @@ function parseFilterConditionValues( const getTypeDescription = (isStrict: boolean) => { if (isStrict) - return 'Try changing the type of the comparison, or enabling less strict type validation.'; + return "Try changing the type of comparison. Alternatively you can enable 'Convert Value Types'."; return 'Try changing the type of the comparison.'; }; @@ -122,7 +135,7 @@ function parseFilterConditionValues( return `

Try either:

    -
  1. Enabling less strict type validation
  2. +
  3. Enabling 'Convert Value Types'
  4. Converting the ${valuePosition} field to ${expectedType}${suggestFunction}
`; diff --git a/packages/workflow/test/FilterParameter.test.ts b/packages/workflow/test/FilterParameter.test.ts index c99dc59ba..b22460f99 100644 --- a/packages/workflow/test/FilterParameter.test.ts +++ b/packages/workflow/test/FilterParameter.test.ts @@ -19,6 +19,7 @@ const filterFactory = (data: DeepPartial = {}): FilterValue => combinator: 'and', conditions: [], options: { + version: 1, leftValue: '', caseSensitive: false, typeValidation: 'strict', @@ -234,6 +235,48 @@ describe('FilterParameter', () => { }); }); + describe('options.version', () => { + describe('version 1', () => { + it('should parse "false" as true', () => { + expect( + executeFilter( + filterFactory({ + conditions: [ + { + id: '1', + leftValue: 'false', + rightValue: false, + operator: { operation: 'equals', type: 'boolean' }, + }, + ], + options: { typeValidation: 'loose', version: 1 }, + }), + ), + ).toEqual(false); + }); + }); + + describe('version 2', () => { + it('should parse "false" as false', () => { + expect( + executeFilter( + filterFactory({ + conditions: [ + { + id: '1', + leftValue: 'false', + rightValue: false, + operator: { operation: 'equals', type: 'boolean' }, + }, + ], + options: { typeValidation: 'loose', version: 2 }, + }), + ), + ).toEqual(true); + }); + }); + }); + describe('operators', () => { describe('exists', () => { it.each([