mirror of
https://github.com/Abdulazizzn/n8n-enterprise-unlocked.git
synced 2025-12-17 18:12:04 +00:00
fix(editor): Improve dragndrop of input pills with spaces (#9656)
This commit is contained in:
@@ -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),
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -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',
|
|
||||||
);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
@@ -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';
|
||||||
|
|||||||
@@ -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)' },
|
||||||
|
|||||||
@@ -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}`;
|
||||||
|
|||||||
Reference in New Issue
Block a user