fix(editor): Improve dragndrop of input pills with spaces (#9656)

This commit is contained in:
Elias Meire
2024-06-07 15:37:30 +02:00
committed by GitHub
parent bb7227d18d
commit 291d46af15
5 changed files with 124 additions and 48 deletions

View File

@@ -13,7 +13,8 @@ import { computed, reactive, watch } from 'vue';
import DropArea from '../DropArea/DropArea.vue'; import DropArea from '../DropArea/DropArea.vue';
import ParameterOptions from '../ParameterOptions.vue'; import ParameterOptions from '../ParameterOptions.vue';
import Assignment from './Assignment.vue'; import Assignment from './Assignment.vue';
import { inputDataToAssignments, nameFromExpression, typeFromExpression } from './utils'; import { inputDataToAssignments, typeFromExpression } from './utils';
import { propertyNameFromExpression } from '@/utils/mappingUtils';
interface Props { interface Props {
parameter: INodeProperties; parameter: INodeProperties;
@@ -49,7 +50,7 @@ const issues = computed(() => {
}); });
const empty = computed(() => state.paramValue.assignments.length === 0); const empty = computed(() => state.paramValue.assignments.length === 0);
const activeDragField = computed(() => nameFromExpression(ndvStore.draggableData)); const activeDragField = computed(() => propertyNameFromExpression(ndvStore.draggableData));
const inputData = computed(() => ndvStore.ndvInputData?.[0]?.json); const inputData = computed(() => ndvStore.ndvInputData?.[0]?.json);
const actions = computed(() => { const actions = computed(() => {
return [ return [
@@ -82,7 +83,7 @@ function addAssignment(): void {
function dropAssignment(expression: string): void { function dropAssignment(expression: string): void {
state.paramValue.assignments.push({ state.paramValue.assignments.push({
id: uuid(), id: uuid(),
name: nameFromExpression(expression), name: propertyNameFromExpression(expression),
value: `=${expression}`, value: `=${expression}`,
type: typeFromExpression(expression), type: typeFromExpression(expression),
}); });

View File

@@ -1,15 +0,0 @@
import { nameFromExpression } from '../utils';
describe('AssignmentCollection > utils', () => {
describe('nameFromExpression', () => {
test('should extract assignment name from previous node', () => {
expect(nameFromExpression('{{ $json.foo.bar }}')).toBe('foo.bar');
});
test('should extract assignment name from another node', () => {
expect(nameFromExpression("{{ $('Node's \"Name\" (copy)').item.json.foo.bar }}")).toBe(
'foo.bar',
);
});
});
});

View File

@@ -3,13 +3,6 @@ import type { AssignmentValue, IDataObject } from 'n8n-workflow';
import { resolveParameter } from '@/composables/useWorkflowHelpers'; import { resolveParameter } from '@/composables/useWorkflowHelpers';
import { v4 as uuid } from 'uuid'; import { v4 as uuid } from 'uuid';
export function nameFromExpression(expression: string): string {
return expression
.replace(/^{{\s*|\s*}}$/g, '')
.replace('$json.', '')
.replace(/^\$\(.*\)(\.item\.json)?\.(.*)/, '$2');
}
export function inferAssignmentType(value: unknown): string { export function inferAssignmentType(value: unknown): string {
if (typeof value === 'boolean') return 'boolean'; if (typeof value === 'boolean') return 'boolean';
if (typeof value === 'number') return 'number'; if (typeof value === 'number') return 'number';

View File

@@ -1,5 +1,10 @@
import type { INodeProperties } from 'n8n-workflow'; import type { INodeProperties } from 'n8n-workflow';
import { getMappedResult, getMappedExpression, escapeMappingString } from '../mappingUtils'; import {
getMappedResult,
getMappedExpression,
escapeMappingString,
propertyNameFromExpression,
} from '../mappingUtils';
const RLC_PARAM: INodeProperties = { const RLC_PARAM: INodeProperties = {
displayName: 'Base', displayName: 'Base',
@@ -146,7 +151,7 @@ describe('Mapping Utils', () => {
it('sets data path, replacing if expecting single path', () => { it('sets data path, replacing if expecting single path', () => {
expect( expect(
getMappedResult(SINGLE_DATA_PATH_PARAM, '{{ $json["Readable date"] }}', '={{$json.test}}'), getMappedResult(SINGLE_DATA_PATH_PARAM, '{{ $json["Readable date"] }}', '={{$json.test}}'),
).toEqual('["Readable date"]'); ).toEqual('Readable date');
expect( expect(
getMappedResult(SINGLE_DATA_PATH_PARAM, '{{ $json.path }}', '={{$json.test}}'), getMappedResult(SINGLE_DATA_PATH_PARAM, '{{ $json.path }}', '={{$json.test}}'),
@@ -159,18 +164,26 @@ describe('Mapping Utils', () => {
).toEqual('path, ["Readable date"]'); ).toEqual('path, ["Readable date"]');
}); });
it('replaces existing dadata path if multiple and is empty expression', () => { it('replaces existing data path if multiple and is empty expression', () => {
expect(getMappedResult(MULTIPLE_DATA_PATH_PARAM, '{{ $json.test }}', '=')).toEqual('test'); expect(getMappedResult(MULTIPLE_DATA_PATH_PARAM, '{{ $json.test }}', '=')).toEqual('test');
}); });
it('handles data when dragging from grand-parent nodes', () => { it('handles data when dragging from grand-parent nodes, replacing if expecting single path', () => {
expect( expect(
getMappedResult( getMappedResult(
MULTIPLE_DATA_PATH_PARAM, MULTIPLE_DATA_PATH_PARAM,
'{{ $node["Schedule Trigger"].json["Day of week"] }}', '{{ $node["Schedule Trigger"].json["Day of week"] }}',
'', '',
), ),
).toEqual('={{ $node["Schedule Trigger"].json["Day of week"] }}'); ).toEqual('["Day of week"]');
expect(
getMappedResult(
MULTIPLE_DATA_PATH_PARAM,
'{{ $node["Schedule Trigger"].json["Day of week"] }}',
'=data',
),
).toEqual('=data, ["Day of week"]');
expect( expect(
getMappedResult( getMappedResult(
@@ -178,7 +191,7 @@ describe('Mapping Utils', () => {
'{{ $node["Schedule Trigger"].json["Day of week"] }}', '{{ $node["Schedule Trigger"].json["Day of week"] }}',
'=data', '=data',
), ),
).toEqual('=data {{ $node["Schedule Trigger"].json["Day of week"] }}'); ).toEqual('Day of week');
expect( expect(
getMappedResult( getMappedResult(
@@ -186,7 +199,7 @@ describe('Mapping Utils', () => {
'{{ $node["Schedule Trigger"].json["Day of week"] }}', '{{ $node["Schedule Trigger"].json["Day of week"] }}',
'= ', '= ',
), ),
).toEqual('= {{ $node["Schedule Trigger"].json["Day of week"] }}'); ).toEqual('Day of week');
}); });
it('handles RLC values', () => { it('handles RLC values', () => {
@@ -195,6 +208,7 @@ describe('Mapping Utils', () => {
expect(getMappedResult(RLC_PARAM, '{{ test }}', '=test')).toEqual('=test {{ test }}'); expect(getMappedResult(RLC_PARAM, '{{ test }}', '=test')).toEqual('=test {{ test }}');
}); });
}); });
describe('getMappedExpression', () => { describe('getMappedExpression', () => {
it('should generate a mapped expression with simple array path', () => { it('should generate a mapped expression with simple array path', () => {
const input = { const input = {
@@ -274,6 +288,68 @@ describe('Mapping Utils', () => {
); );
}); });
}); });
describe('propertyNameFromExpression', () => {
describe('dot access', () => {
test('should extract property name from previous node', () => {
expect(propertyNameFromExpression('{{ $json.foo.bar }}')).toBe('foo.bar');
});
test('should extract property name from another node', () => {
expect(
propertyNameFromExpression("{{ $('Node's \"Name\" (copy)').item.json.foo.bar }}"),
).toBe('foo.bar');
});
});
describe('bracket access', () => {
test('should extract property name from previous node (root)', () => {
expect(propertyNameFromExpression("{{ $json['with spaces\\' here'] }}")).toBe(
"with spaces' here",
);
});
test('should extract property name from previous node (nested)', () => {
expect(propertyNameFromExpression("{{ $json.foo['with spaces\\' here'] }}")).toBe(
"foo['with spaces\\' here']",
);
});
test('should extract property name from another node (root)', () => {
expect(
propertyNameFromExpression(
"{{ $('Node's \"Name\" (copy)').item.json['with spaces\\' here'] }}",
),
).toBe("with spaces' here");
});
test('should extract property name from another node (nested)', () => {
expect(
propertyNameFromExpression(
"{{ $('Node's \"Name\" (copy)').item.json.foo['with spaces\\' here'] }}",
),
).toBe("foo['with spaces\\' here']");
});
test('should handle nested bracket access', () => {
expect(
propertyNameFromExpression(
"{{ $('Node's \"Name\" (copy)').item.json['First with spaces']['Second with spaces'] }}",
),
).toBe("['First with spaces']['Second with spaces']");
});
test('should handle forceBracketAccess=true', () => {
expect(
propertyNameFromExpression(
"{{ $('Node's \"Name\" (copy)').item.json['First with spaces'] }}",
true,
),
).toBe("['First with spaces']");
});
});
});
describe('escapeMappingString', () => { describe('escapeMappingString', () => {
test.each([ test.each([
{ input: 'Normal node name (here)', output: 'Normal node name (here)' }, { input: 'Normal node name (here)', output: 'Normal node name (here)' },

View File

@@ -1,5 +1,6 @@
import type { INodeProperties, NodeParameterValueType } from 'n8n-workflow'; import type { INodeProperties, NodeParameterValueType } from 'n8n-workflow';
import { isResourceLocatorValue } from 'n8n-workflow'; import { isResourceLocatorValue } from 'n8n-workflow';
import { isExpression } from './expressions';
const validJsIdNameRegex = /^[a-zA-Z_$][a-zA-Z0-9_$]*$/; const validJsIdNameRegex = /^[a-zA-Z_$][a-zA-Z0-9_$]*$/;
@@ -46,34 +47,54 @@ export function getMappedExpression({
return `{{ ${generatePath(root, path)} }}`; return `{{ ${generatePath(root, path)} }}`;
} }
const unquote = (str: string) => {
if (str.startsWith('"') && str.endsWith('"')) {
return str.slice(1, -1).replace(/\\"/g, '"');
}
if (str.startsWith("'") && str.endsWith("'")) {
return str.slice(1, -1).replace(/\\'/g, "'");
}
return str;
};
export function propertyNameFromExpression(expression: string, forceBracketAccess = false): string {
const propPath = expression
.replace(/^{{\s*|\s*}}$/g, '')
.replace(/^(\$\(.*\)\.item\.json|\$json|\$node\[.*\]\.json)\.?(.*)/, '$2');
const isSingleBracketAccess = propPath.startsWith('[') && !propPath.slice(1).includes('[');
if (isSingleBracketAccess && !forceBracketAccess) {
// "['Key with spaces']" -> "Key with spaces"
return unquote(propPath.slice(1, -1));
}
return propPath;
}
export function getMappedResult( export function getMappedResult(
parameter: INodeProperties, parameter: INodeProperties,
newParamValue: string, newParamValue: string,
prevParamValue: NodeParameterValueType, prevParamValue: NodeParameterValueType,
): string { ): string {
const useDataPath = !!parameter.requiresDataPath && newParamValue.startsWith('{{ $json'); // ignore when mapping from grand-parent-node
const prevValue = const prevValue =
parameter.type === 'resourceLocator' && isResourceLocatorValue(prevParamValue) parameter.type === 'resourceLocator' && isResourceLocatorValue(prevParamValue)
? prevParamValue.value ? prevParamValue.value
: prevParamValue; : prevParamValue;
if (useDataPath) { if (parameter.requiresDataPath) {
const newValue = newParamValue if (parameter.requiresDataPath === 'multiple') {
.replace('{{ $json', '') const propertyName = propertyNameFromExpression(newParamValue, true);
.replace(new RegExp('^\\.'), '') if (typeof prevValue === 'string' && (prevValue.trim() === '=' || prevValue.trim() === '')) {
.replace(new RegExp('}}$'), '') return propertyName;
.trim();
if (prevValue && parameter.requiresDataPath === 'multiple') {
if (typeof prevValue === 'string' && prevValue.trim() === '=') {
return newValue;
} else {
return `${prevValue}, ${newValue}`;
} }
} else {
return newValue; return `${prevValue}, ${propertyName}`;
} }
} else if (typeof prevValue === 'string' && prevValue.startsWith('=') && prevValue.length > 1) {
return propertyNameFromExpression(newParamValue);
} else if (typeof prevValue === 'string' && isExpression(prevValue) && prevValue.length > 1) {
return `${prevValue} ${newParamValue}`; return `${prevValue} ${newParamValue}`;
} else if (prevValue && ['string', 'json'].includes(parameter.type)) { } else if (prevValue && ['string', 'json'].includes(parameter.type)) {
return prevValue === '=' ? `=${newParamValue}` : `=${prevValue} ${newParamValue}`; return prevValue === '=' ? `=${newParamValue}` : `=${prevValue} ${newParamValue}`;