fix(editor): Escape node names with quotes in autocomplete and drag'n'drop (#8663)

This commit is contained in:
Elias Meire 2024-02-21 10:43:34 +01:00 committed by GitHub
parent c346002cc6
commit 890c2bd52b
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
5 changed files with 35 additions and 9 deletions

View file

@ -5,6 +5,7 @@ import type { Completion, CompletionContext, CompletionResult } from '@codemirro
import type { INodeUi } from '@/Interface'; import type { INodeUi } from '@/Interface';
import { mapStores } from 'pinia'; import { mapStores } from 'pinia';
import { useWorkflowsStore } from '@/stores/workflows.store'; import { useWorkflowsStore } from '@/stores/workflows.store';
import { escapeMappingString } from '@/utils/mappingUtils';
function getAutoCompletableNodeNames(nodes: INodeUi[]) { function getAutoCompletableNodeNames(nodes: INodeUi[]) {
return nodes return nodes
@ -98,7 +99,7 @@ export const baseCompletions = defineComponent({
options.push( options.push(
...getAutoCompletableNodeNames(this.workflowsStore.allNodes).map((nodeName) => { ...getAutoCompletableNodeNames(this.workflowsStore.allNodes).map((nodeName) => {
return { return {
label: `${prefix}('${nodeName}')`, label: `${prefix}('${escapeMappingString(nodeName)}')`,
type: 'variable', type: 'variable',
info: this.$locale.baseText('codeNodeEditor.completer.$()', { info: this.$locale.baseText('codeNodeEditor.completer.$()', {
interpolate: { nodeName }, interpolate: { nodeName },
@ -138,7 +139,7 @@ export const baseCompletions = defineComponent({
const options: Completion[] = getAutoCompletableNodeNames(this.workflowsStore.allNodes).map( const options: Completion[] = getAutoCompletableNodeNames(this.workflowsStore.allNodes).map(
(nodeName) => { (nodeName) => {
return { return {
label: `${prefix}('${nodeName}')`, label: `${prefix}('${escapeMappingString(nodeName)}')`,
type: 'variable', type: 'variable',
info: this.$locale.baseText('codeNodeEditor.completer.$()', { info: this.$locale.baseText('codeNodeEditor.completer.$()', {
interpolate: { nodeName }, interpolate: { nodeName },

View file

@ -50,6 +50,7 @@ import { useRootStore } from '@/stores/n8nRoot.store';
import { useNDVStore } from '@/stores/ndv.store'; import { useNDVStore } from '@/stores/ndv.store';
import { useRouter } from 'vue-router'; import { useRouter } from 'vue-router';
import { useWorkflowHelpers } from '@/composables/useWorkflowHelpers'; import { useWorkflowHelpers } from '@/composables/useWorkflowHelpers';
import { escapeMappingString } from '@/utils/mappingUtils';
// Node types that should not be displayed in variable selector // Node types that should not be displayed in variable selector
const SKIPPED_NODE_TYPES = [STICKY_NODE_TYPE]; const SKIPPED_NODE_TYPES = [STICKY_NODE_TYPE];
@ -398,7 +399,9 @@ export default defineComponent({
// Get json data // Get json data
if (outputData.hasOwnProperty('json')) { if (outputData.hasOwnProperty('json')) {
const jsonPropertyPrefix = useShort ? '$json' : `$('${nodeName}').item.json`; const jsonPropertyPrefix = useShort
? '$json'
: `$('${escapeMappingString(nodeName)}').item.json`;
const jsonDataOptions: IVariableSelectorOption[] = []; const jsonDataOptions: IVariableSelectorOption[] = [];
for (const propertyName of Object.keys(outputData.json)) { for (const propertyName of Object.keys(outputData.json)) {
@ -423,7 +426,9 @@ export default defineComponent({
// Get binary data // Get binary data
if (outputData.hasOwnProperty('binary')) { if (outputData.hasOwnProperty('binary')) {
const binaryPropertyPrefix = useShort ? '$binary' : `$('${nodeName}').item.binary`; const binaryPropertyPrefix = useShort
? '$binary'
: `$('${escapeMappingString(nodeName)}').item.binary`;
const binaryData = []; const binaryData = [];
let binaryPropertyData = []; let binaryPropertyData = [];
@ -537,7 +542,7 @@ export default defineComponent({
returnData.push({ returnData.push({
name: key, name: key,
key: `$('${nodeName}').context["${key}"]`, key: `$('${escapeMappingString(nodeName)}').context['${escapeMappingString(key)}']`,
// @ts-ignore // @ts-ignore
value: nodeContext[key], value: nodeContext[key],
}); });
@ -793,7 +798,12 @@ export default defineComponent({
{ {
name: this.$locale.baseText('variableSelector.parameters'), name: this.$locale.baseText('variableSelector.parameters'),
options: this.sortOptions( options: this.sortOptions(
this.getNodeParameters(nodeName, `$('${nodeName}').params`, undefined, filterText), this.getNodeParameters(
nodeName,
`$('${escapeMappingString(nodeName)}').params`,
undefined,
filterText,
),
), ),
} as IVariableSelectorOption, } as IVariableSelectorOption,
]; ];

View file

@ -11,6 +11,7 @@ import {
} from './utils'; } from './utils';
import type { Completion, CompletionContext, CompletionResult } from '@codemirror/autocomplete'; import type { Completion, CompletionContext, CompletionResult } from '@codemirror/autocomplete';
import { useExternalSecretsStore } from '@/stores/externalSecrets.ee.store'; import { useExternalSecretsStore } from '@/stores/externalSecrets.ee.store';
import { escapeMappingString } from '@/utils/mappingUtils';
/** /**
* Completions offered at the dollar position: `$|` * Completions offered at the dollar position: `$|`
@ -90,7 +91,7 @@ export function dollarOptions() {
}) })
.concat( .concat(
autocompletableNodeNames().map((nodeName) => ({ autocompletableNodeNames().map((nodeName) => ({
label: `$('${nodeName}')`, label: `$('${escapeMappingString(nodeName)}')`,
type: 'keyword', type: 'keyword',
info: i18n.baseText('codeNodeEditor.completer.$()', { interpolate: { nodeName } }), info: i18n.baseText('codeNodeEditor.completer.$()', { interpolate: { nodeName } }),
})), })),

View file

@ -1,5 +1,5 @@
import type { INodeProperties } from 'n8n-workflow'; import type { INodeProperties } from 'n8n-workflow';
import { getMappedResult, getMappedExpression } from '../mappingUtils'; import { getMappedResult, getMappedExpression, escapeMappingString } from '../mappingUtils';
const RLC_PARAM: INodeProperties = { const RLC_PARAM: INodeProperties = {
displayName: 'Base', displayName: 'Base',
@ -273,4 +273,12 @@ describe('Mapping Utils', () => {
); );
}); });
}); });
describe('escapeMappingString', () => {
test.each([
{ input: 'Normal node name (here)', output: 'Normal node name (here)' },
{ input: "'Should es'ape quotes here'", output: "\\'Should es\\'ape quotes here\\'" },
])('should escape "$input" to "$output"', ({ input, output }) => {
expect(escapeMappingString(input)).toEqual(output);
});
});
}); });

View file

@ -18,6 +18,10 @@ export function generatePath(root: string, path: Array<string | number>): string
}, root); }, root);
} }
export function escapeMappingString(str: string): string {
return str.replace(/\'/g, "\\'");
}
export function getMappedExpression({ export function getMappedExpression({
nodeName, nodeName,
distanceFromActive, distanceFromActive,
@ -28,7 +32,9 @@ export function getMappedExpression({
path: Array<string | number> | string; path: Array<string | number> | string;
}) { }) {
const root = const root =
distanceFromActive === 1 ? '$json' : generatePath(`$('${nodeName}')`, ['item', 'json']); distanceFromActive === 1
? '$json'
: generatePath(`$('${escapeMappingString(nodeName)}')`, ['item', 'json']);
if (typeof path === 'string') { if (typeof path === 'string') {
return `{{ ${root}${path} }}`; return `{{ ${root}${path} }}`;