diff --git a/packages/editor-ui/src/components/ParameterInputFull.vue b/packages/editor-ui/src/components/ParameterInputFull.vue index 3c8e3821ae..f286a7e82f 100644 --- a/packages/editor-ui/src/components/ParameterInputFull.vue +++ b/packages/editor-ui/src/components/ParameterInputFull.vue @@ -229,12 +229,31 @@ export default mixins(showMessage).extend({ } }, onDrop(data: string) { - this.forceShowExpression = true; + const useDataPath = !!this.parameter.requiresDataPath && data.startsWith('{{ $json'); + if (!useDataPath) { + this.forceShowExpression = true; + } setTimeout(() => { if (this.node) { const prevValue = this.isResourceLocator ? this.value.value : this.value; let updatedValue: string; - if (typeof prevValue === 'string' && prevValue.startsWith('=') && prevValue.length > 1) { + if (useDataPath) { + const newValue = data + .replace('{{ $json', '') + .replace(new RegExp('^\\.'), '') + .replace(new RegExp('}}$'), '') + .trim(); + + if (prevValue && this.parameter.requiresDataPath === 'multiple') { + updatedValue = `${prevValue}, ${newValue}`; + } else { + updatedValue = newValue; + } + } else if ( + typeof prevValue === 'string' && + prevValue.startsWith('=') && + prevValue.length > 1 + ) { updatedValue = `${prevValue} ${data}`; } else { updatedValue = `=${data}`; diff --git a/packages/editor-ui/src/components/RunDataJson.vue b/packages/editor-ui/src/components/RunDataJson.vue index e7981a72ca..15a6c0289c 100644 --- a/packages/editor-ui/src/components/RunDataJson.vue +++ b/packages/editor-ui/src/components/RunDataJson.vue @@ -79,6 +79,7 @@ import { externalHooks } from '@/mixins/externalHooks'; import { mapStores } from 'pinia'; import { useNDVStore } from '@/stores/ndv'; import MappingPill from './MappingPill.vue'; +import { getMappedExpression } from '@/utils/mappingUtils'; const runDataJsonActions = () => import('@/components/RunDataJsonActions.vue'); @@ -169,11 +170,13 @@ export default mixins(externalHooks).extend({ return shorten(el.dataset.name || '', 16, 2); }, getJsonParameterPath(path: string): string { - const convertedPath = convertPath(path); - return `{{ ${convertedPath.replace( - /^(\["?\d"?])/, - this.distanceFromActive === 1 ? '$json' : `$node["${this.node!.name}"].json`, - )} }}`; + const subPath = path.replace(/^(\["?\d"?])/, ''); // remove item position + + return getMappedExpression({ + nodeName: this.node.name, + distanceFromActive: this.distanceFromActive, + path: subPath, + }); }, onDragStart(el: HTMLElement) { if (el && el.dataset.path) { diff --git a/packages/editor-ui/src/components/RunDataSchema.test.ts b/packages/editor-ui/src/components/RunDataSchema.test.ts index f9c5fe295d..ff71b2aa00 100644 --- a/packages/editor-ui/src/components/RunDataSchema.test.ts +++ b/packages/editor-ui/src/components/RunDataSchema.test.ts @@ -60,4 +60,23 @@ describe('RunDataJsonSchema.vue', () => { }); expect(container).toMatchSnapshot(); }); + + it('renders schema with spaces and dots', () => { + renderOptions.props.data = [ + { + 'hello world': [ + { + test: { + 'more to think about': 1, + }, + 'test.how': 'ignore', + }, + ], + }, + ]; + const { container } = render(RunDataJsonSchema, renderOptions, (vue) => { + vue.use(PiniaVuePlugin); + }); + expect(container).toMatchSnapshot(); + }); }); diff --git a/packages/editor-ui/src/components/RunDataSchemaItem.vue b/packages/editor-ui/src/components/RunDataSchemaItem.vue index fde51fed48..e6457fe93a 100644 --- a/packages/editor-ui/src/components/RunDataSchemaItem.vue +++ b/packages/editor-ui/src/components/RunDataSchemaItem.vue @@ -2,6 +2,7 @@ import { computed } from 'vue'; import { INodeUi, Schema } from '@/Interface'; import { checkExhaustive, shorten } from '@/utils'; +import { getMappedExpression } from '@/utils/mappingUtils'; type Props = { schema: Schema; @@ -35,7 +36,12 @@ const text = computed(() => ); const getJsonParameterPath = (path: string): string => - `{{ ${props.distanceFromActive === 1 ? '$json' : `$node["${props.node!.name}"].json`}${path} }}`; + getMappedExpression({ + nodeName: props.node!.name, + distanceFromActive: props.distanceFromActive, + path, + }); + const transitionDelay = (i: number) => `${i * 0.033}s`; const getIconBySchemaType = (type: Schema['type']): string => { diff --git a/packages/editor-ui/src/components/RunDataTable.vue b/packages/editor-ui/src/components/RunDataTable.vue index fc103f6e27..3f2c1e5b7c 100644 --- a/packages/editor-ui/src/components/RunDataTable.vue +++ b/packages/editor-ui/src/components/RunDataTable.vue @@ -170,6 +170,7 @@ import { mapStores } from 'pinia'; import { useWorkflowsStore } from '@/stores/workflows'; import { useNDVStore } from '@/stores/ndv'; import MappingPill from './MappingPill.vue'; +import { getMappedExpression } from '@/utils/mappingUtils'; const MAX_COLUMNS_LIMIT = 40; @@ -315,11 +316,11 @@ export default mixins(externalHooks).extend({ return ''; } - if (this.distanceFromActive === 1) { - return `{{ $json["${column}"] }}`; - } - - return `{{ $node["${this.node.name}"].json["${column}"] }}`; + return getMappedExpression({ + nodeName: this.node.name, + distanceFromActive: this.distanceFromActive, + path: [column], + }); }, getPathNameFromTarget(el: HTMLElement) { if (!el) { @@ -343,21 +344,12 @@ export default mixins(externalHooks).extend({ if (!this.node) { return ''; } - - const expr = path.reduce((accu: string, key: string | number) => { - if (typeof key === 'number') { - return `${accu}[${key}]`; - } - - return `${accu}["${key}"]`; - }, ''); const column = this.tableData.columns[colIndex]; - - if (this.distanceFromActive === 1) { - return `{{ $json["${column}"]${expr} }}`; - } - - return `{{ $node["${this.node.name}"].json["${column}"]${expr} }}`; + return getMappedExpression({ + nodeName: this.node.name, + distanceFromActive: this.distanceFromActive, + path: [column, ...path], + }); }, isEmpty(value: unknown): boolean { return ( diff --git a/packages/editor-ui/src/components/__snapshots__/RunDataSchema.test.ts.snap b/packages/editor-ui/src/components/__snapshots__/RunDataSchema.test.ts.snap index ca3faf29b1..aac0e13b91 100644 --- a/packages/editor-ui/src/components/__snapshots__/RunDataSchema.test.ts.snap +++ b/packages/editor-ui/src/components/__snapshots__/RunDataSchema.test.ts.snap @@ -33,9 +33,9 @@ exports[`RunDataJsonSchema.vue > renders schema for data 1`] = ` class="label" data-depth="1" data-name="name" - data-path="[\\"name\\"]" + data-path=".name" data-target="mappable" - data-value="{{ $json[\\"name\\"] }}" + data-value="{{ $json.name }}" > renders schema for data 1`] = ` class="label" data-depth="1" data-name="age" - data-path="[\\"age\\"]" + data-path=".age" data-target="mappable" - data-value="{{ $json[\\"age\\"] }}" + data-value="{{ $json.age }}" > renders schema for data 1`] = ` class="label" data-depth="1" data-name="hobbies" - data-path="[\\"hobbies\\"]" + data-path=".hobbies" data-target="mappable" - data-value="{{ $json[\\"hobbies\\"] }}" + data-value="{{ $json.hobbies }}" > renders schema for data 1`] = ` class="label" data-depth="2" data-name="string[0]" - data-path="[\\"hobbies\\"][0]" + data-path=".hobbies[0]" data-target="mappable" - data-value="{{ $json[\\"hobbies\\"][0] }}" + data-value="{{ $json.hobbies[0] }}" > renders schema for data 1`] = ` class="label" data-depth="2" data-name="string[1]" - data-path="[\\"hobbies\\"][1]" + data-path=".hobbies[1]" data-target="mappable" - data-value="{{ $json[\\"hobbies\\"][1] }}" + data-value="{{ $json.hobbies[1] }}" > renders schema for empty data 1`] = ` `; + +exports[`RunDataJsonSchema.vue > renders schema with spaces 1`] = ` +
+
+
+
+
+ + + + +
+
+
+ + + + + hello world + + +
+ + + +
+
+
+ + + + hello world + + + [0] + + +
+ + + +
+
+
+ + + + + test + + +
+ + + +
+
+
+ + + + + more to think about + + +
+ + 1 + + + + +
+
+
+
+
+ + + + + test.how + + +
+ + ignore + + + + +
+
+
+
+
+
+
+
+ +
+
+`; + +exports[`RunDataJsonSchema.vue > renders schema with spaces and dots 1`] = ` +
+
+
+
+
+ + + + +
+
+
+ + + + + hello world + + +
+ + + +
+
+
+ + + + hello world + + + [0] + + +
+ + + +
+
+
+ + + + + test + + +
+ + + +
+
+
+ + + + + more to think about + + +
+ + 1 + + + + +
+
+
+
+
+ + + + + test.how + + +
+ + ignore + + + + +
+
+
+
+
+
+
+
+ +
+
+`; diff --git a/packages/editor-ui/src/utils/__tests__/typesUtils.test.ts b/packages/editor-ui/src/utils/__tests__/typesUtils.test.ts index 3807d42f6b..569b98d408 100644 --- a/packages/editor-ui/src/utils/__tests__/typesUtils.test.ts +++ b/packages/editor-ui/src/utils/__tests__/typesUtils.test.ts @@ -257,10 +257,31 @@ describe('Utils', () => { type: 'array', key: 'people', value: [ - { type: 'string', value: 'Joe', key: '0', path: '["people"][0]' }, - { type: 'string', value: 'John', key: '1', path: '["people"][1]' }, + { type: 'string', value: 'Joe', key: '0', path: '.people[0]' }, + { type: 'string', value: 'John', key: '1', path: '.people[1]' }, ], - path: '["people"]', + path: '.people', + }, + ], + path: '', + }, + ], + [ + { 'with space': [], 'with.dot': 'test' }, + { + type: 'object', + value: [ + { + type: 'array', + key: 'with space', + value: [], + path: '["with space"]', + }, + { + type: 'string', + key: 'with.dot', + value: 'test', + path: '["with.dot"]', }, ], path: '', @@ -278,8 +299,8 @@ describe('Utils', () => { type: 'object', key: '0', value: [ - { type: 'string', key: 'name', value: 'John', path: '[0]["name"]' }, - { type: 'number', key: 'age', value: '22', path: '[0]["age"]' }, + { type: 'string', key: 'name', value: 'John', path: '[0].name' }, + { type: 'number', key: 'age', value: '22', path: '[0].age' }, ], path: '[0]', }, @@ -287,8 +308,8 @@ describe('Utils', () => { type: 'object', key: '1', value: [ - { type: 'string', key: 'name', value: 'Joe', path: '[1]["name"]' }, - { type: 'number', key: 'age', value: '33', path: '[1]["age"]' }, + { type: 'string', key: 'name', value: 'Joe', path: '[1].name' }, + { type: 'number', key: 'age', value: '33', path: '[1].age' }, ], path: '[1]', }, @@ -308,16 +329,16 @@ describe('Utils', () => { type: 'object', key: '0', value: [ - { type: 'string', key: 'name', value: 'John', path: '[0]["name"]' }, - { type: 'number', key: 'age', value: '22', path: '[0]["age"]' }, + { type: 'string', key: 'name', value: 'John', path: '[0].name' }, + { type: 'number', key: 'age', value: '22', path: '[0].age' }, { type: 'array', key: 'hobbies', value: [ - { type: 'string', key: '0', value: 'surfing', path: '[0]["hobbies"][0]' }, - { type: 'string', key: '1', value: 'traveling', path: '[0]["hobbies"][1]' }, + { type: 'string', key: '0', value: 'surfing', path: '[0].hobbies[0]' }, + { type: 'string', key: '1', value: 'traveling', path: '[0].hobbies[1]' }, ], - path: '[0]["hobbies"]', + path: '[0].hobbies', }, ], path: '[0]', @@ -326,16 +347,16 @@ describe('Utils', () => { type: 'object', key: '1', value: [ - { type: 'string', key: 'name', value: 'Joe', path: '[1]["name"]' }, - { type: 'number', key: 'age', value: '33', path: '[1]["age"]' }, + { type: 'string', key: 'name', value: 'Joe', path: '[1].name' }, + { type: 'number', key: 'age', value: '33', path: '[1].age' }, { type: 'array', key: 'hobbies', value: [ - { type: 'string', key: '0', value: 'skateboarding', path: '[1]["hobbies"][0]' }, - { type: 'string', key: '1', value: 'gaming', path: '[1]["hobbies"][1]' }, + { type: 'string', key: '0', value: 'skateboarding', path: '[1].hobbies[0]' }, + { type: 'string', key: '1', value: 'gaming', path: '[1].hobbies[1]' }, ], - path: '[1]["hobbies"]', + path: '[1].hobbies', }, ], path: '[1]', @@ -381,8 +402,8 @@ describe('Utils', () => { type: 'object', key: '0', value: [ - { type: 'string', key: 'name', value: 'John', path: '[0][0]["name"]' }, - { type: 'number', key: 'age', value: '22', path: '[0][0]["age"]' }, + { type: 'string', key: 'name', value: 'John', path: '[0][0].name' }, + { type: 'number', key: 'age', value: '22', path: '[0][0].age' }, ], path: '[0][0]', }, @@ -390,8 +411,8 @@ describe('Utils', () => { type: 'object', key: '1', value: [ - { type: 'string', key: 'name', value: 'Joe', path: '[0][1]["name"]' }, - { type: 'number', key: 'age', value: '33', path: '[0][1]["age"]' }, + { type: 'string', key: 'name', value: 'Joe', path: '[0][1].name' }, + { type: 'number', key: 'age', value: '33', path: '[0][1].age' }, ], path: '[0][1]', }, @@ -430,16 +451,16 @@ describe('Utils', () => { type: 'string', key: '0', value: '2022-11-22T00:00:00.000Z', - path: '[0]["dates"][0][0]', + path: '[0].dates[0][0]', }, { type: 'string', key: '1', value: '2022-11-23T00:00:00.000Z', - path: '[0]["dates"][0][1]', + path: '[0].dates[0][1]', }, ], - path: '[0]["dates"][0]', + path: '[0].dates[0]', }, { type: 'array', @@ -449,19 +470,19 @@ describe('Utils', () => { type: 'string', key: '0', value: '2022-12-22T00:00:00.000Z', - path: '[0]["dates"][1][0]', + path: '[0].dates[1][0]', }, { type: 'string', key: '1', value: '2022-12-23T00:00:00.000Z', - path: '[0]["dates"][1][1]', + path: '[0].dates[1][1]', }, ], - path: '[0]["dates"][1]', + path: '[0].dates[1]', }, ], - path: '[0]["dates"]', + path: '[0].dates', }, ], path: '[0]', diff --git a/packages/editor-ui/src/utils/mappingUtils.ts b/packages/editor-ui/src/utils/mappingUtils.ts new file mode 100644 index 0000000000..32889597f8 --- /dev/null +++ b/packages/editor-ui/src/utils/mappingUtils.ts @@ -0,0 +1,31 @@ +export function generatePath(root: string, path: Array): string { + return path.reduce((accu: string, part: string | number) => { + if (typeof part === 'number') { + return `${accu}[${part}]`; + } + + if (part.includes(' ') || part.includes('.')) { + return `${accu}["${part}"]`; + } + + return `${accu}.${part}`; + }, root); +} + +export function getMappedExpression({ + nodeName, + distanceFromActive, + path, +}: { + nodeName: string; + distanceFromActive: number; + path: Array | string; +}) { + const root = distanceFromActive === 1 ? '$json' : generatePath('$node', [nodeName, 'json']); + + if (typeof path === 'string') { + return `{{ ${root}${path} }}`; + } + + return `{{ ${generatePath(root, path)} }}`; +} diff --git a/packages/editor-ui/src/utils/typesUtils.ts b/packages/editor-ui/src/utils/typesUtils.ts index b1692cb512..52f3010b1c 100644 --- a/packages/editor-ui/src/utils/typesUtils.ts +++ b/packages/editor-ui/src/utils/typesUtils.ts @@ -2,6 +2,7 @@ import dateformat from 'dateformat'; import { IDataObject, jsonParse } from 'n8n-workflow'; import { Schema, Optional, Primitives } from '@/Interface'; import { isObj } from '@/utils/typeGuards'; +import { generatePath } from '@/utils/mappingUtils'; /* Constants and utility functions than can be used to manipulate different data types and objects @@ -231,7 +232,7 @@ export const getSchema = (input: Optional, path = ''): Sche type: 'object', value: Object.entries(input).map(([k, v]) => ({ key: k, - ...getSchema(v, path + `["${k}"]`), + ...getSchema(v, generatePath(path, [k])), })), path, }; diff --git a/packages/nodes-base/nodes/CompareDatasets/CompareDatasets.node.ts b/packages/nodes-base/nodes/CompareDatasets/CompareDatasets.node.ts index 497e9b9db4..4acc603164 100644 --- a/packages/nodes-base/nodes/CompareDatasets/CompareDatasets.node.ts +++ b/packages/nodes-base/nodes/CompareDatasets/CompareDatasets.node.ts @@ -52,6 +52,7 @@ export class CompareDatasets implements INodeType { // eslint-disable-next-line n8n-nodes-base/node-param-placeholder-miscased-id placeholder: 'e.g. id', hint: ' Enter the field name as text', + requiresDataPath: 'single', }, { displayName: 'Input B Field', @@ -61,6 +62,7 @@ export class CompareDatasets implements INodeType { // eslint-disable-next-line n8n-nodes-base/node-param-placeholder-miscased-id placeholder: 'e.g. id', hint: ' Enter the field name as text', + requiresDataPath: 'single', }, ], }, @@ -126,6 +128,7 @@ export class CompareDatasets implements INodeType { resolve: ['mix'], }, }, + requiresDataPath: 'multiple', }, { displayName: 'Options', @@ -143,6 +146,7 @@ export class CompareDatasets implements INodeType { hint: 'Enter the field names as text, separated by commas', description: "Fields that shouldn't be included when checking whether two items are the same", + requiresDataPath: 'multiple', }, { displayName: 'Fuzzy Compare', diff --git a/packages/nodes-base/nodes/ItemLists/ItemLists.node.ts b/packages/nodes-base/nodes/ItemLists/ItemLists.node.ts index e4fc5d8ea2..d7aa9b3846 100644 --- a/packages/nodes-base/nodes/ItemLists/ItemLists.node.ts +++ b/packages/nodes-base/nodes/ItemLists/ItemLists.node.ts @@ -139,6 +139,7 @@ export class ItemLists implements INodeType { }, }, description: 'The name of the input field to break out into separate items', + requiresDataPath: 'single', }, { displayName: 'Include', @@ -197,6 +198,7 @@ export class ItemLists implements INodeType { // eslint-disable-next-line n8n-nodes-base/node-param-placeholder-miscased-id placeholder: 'e.g. id', hint: ' Enter the field name as text', + requiresDataPath: 'single', }, ], }, @@ -256,6 +258,7 @@ export class ItemLists implements INodeType { // eslint-disable-next-line n8n-nodes-base/node-param-placeholder-miscased-id placeholder: 'e.g. id', hint: ' Enter the field name as text', + requiresDataPath: 'single', }, { displayName: 'Rename Field', @@ -276,6 +279,7 @@ export class ItemLists implements INodeType { default: '', description: 'The name of the field to put the aggregated data in. Leave blank to use the input field name.', + requiresDataPath: 'single', }, ], }, @@ -346,6 +350,7 @@ export class ItemLists implements INodeType { // eslint-disable-next-line n8n-nodes-base/node-param-placeholder-miscased-id placeholder: 'e.g. id', hint: ' Enter the field name as text', + requiresDataPath: 'single', }, ], }, @@ -382,6 +387,7 @@ export class ItemLists implements INodeType { // eslint-disable-next-line n8n-nodes-base/node-param-placeholder-miscased-id placeholder: 'e.g. id', hint: ' Enter the field name as text', + requiresDataPath: 'single', }, ], }, @@ -453,6 +459,7 @@ export class ItemLists implements INodeType { // eslint-disable-next-line n8n-nodes-base/node-param-placeholder-miscased-id placeholder: 'e.g. id', hint: ' Enter the field name as text', + requiresDataPath: 'single', }, ], }, @@ -488,6 +495,7 @@ export class ItemLists implements INodeType { // eslint-disable-next-line n8n-nodes-base/node-param-placeholder-miscased-id placeholder: 'e.g. id', hint: ' Enter the field name as text', + requiresDataPath: 'single', }, ], }, @@ -544,6 +552,7 @@ export class ItemLists implements INodeType { // eslint-disable-next-line n8n-nodes-base/node-param-placeholder-miscased-id placeholder: 'e.g. id', hint: ' Enter the field name as text', + requiresDataPath: 'single', }, { displayName: 'Order', diff --git a/packages/nodes-base/nodes/ItemLists/summarize.operation.ts b/packages/nodes-base/nodes/ItemLists/summarize.operation.ts index 1a793cbfa1..24d8f6fc6a 100644 --- a/packages/nodes-base/nodes/ItemLists/summarize.operation.ts +++ b/packages/nodes-base/nodes/ItemLists/summarize.operation.ts @@ -123,6 +123,7 @@ export const description: INodeProperties[] = [ aggregation: [...NUMERICAL_AGGREGATIONS, 'countUnique', 'count'], }, }, + requiresDataPath: 'single', }, { displayName: 'Field', @@ -138,6 +139,7 @@ export const description: INodeProperties[] = [ aggregation: NUMERICAL_AGGREGATIONS, }, }, + requiresDataPath: 'single', }, { displayName: 'Field', @@ -153,6 +155,7 @@ export const description: INodeProperties[] = [ aggregation: ['countUnique', 'count'], }, }, + requiresDataPath: 'single', }, // ---------------------------------------------------------------------------------------------------------- { @@ -245,6 +248,7 @@ export const description: INodeProperties[] = [ '/options.outputFormat': ['singleItem'], }, }, + requiresDataPath: 'multiple', }, { displayName: 'Fields to Group By', @@ -261,6 +265,7 @@ export const description: INodeProperties[] = [ '/options.outputFormat': ['singleItem'], }, }, + requiresDataPath: 'multiple', }, // ---------------------------------------------------------------------------------------------------------- { diff --git a/packages/nodes-base/nodes/Merge/v2/MergeV2.node.ts b/packages/nodes-base/nodes/Merge/v2/MergeV2.node.ts index 4865c0b196..de3ee69cbd 100644 --- a/packages/nodes-base/nodes/Merge/v2/MergeV2.node.ts +++ b/packages/nodes-base/nodes/Merge/v2/MergeV2.node.ts @@ -122,6 +122,7 @@ const versionDescription: INodeTypeDescription = { // eslint-disable-next-line n8n-nodes-base/node-param-placeholder-miscased-id placeholder: 'e.g. id', hint: ' Enter the field name as text', + requiresDataPath: 'single', }, { displayName: 'Input 2 Field', @@ -131,6 +132,7 @@ const versionDescription: INodeTypeDescription = { // eslint-disable-next-line n8n-nodes-base/node-param-placeholder-miscased-id placeholder: 'e.g. id', hint: ' Enter the field name as text', + requiresDataPath: 'single', }, ], }, diff --git a/packages/workflow/src/Interfaces.ts b/packages/workflow/src/Interfaces.ts index adb7ac8230..eb74598da8 100644 --- a/packages/workflow/src/Interfaces.ts +++ b/packages/workflow/src/Interfaces.ts @@ -1039,6 +1039,7 @@ export interface INodeProperties { >; extractValue?: INodePropertyValueExtractor; modes?: INodePropertyMode[]; + requiresDataPath?: 'single' | 'multiple'; } export interface INodePropertyModeTypeOptions {