fix(editor): Make inputs dragged to Python Code editor produce working code (#19415)

This commit is contained in:
Jaakko Husso
2025-09-12 11:52:08 +03:00
committed by GitHub
parent b6abd1ef69
commit c5ee969433
3 changed files with 135 additions and 7 deletions

View File

@@ -20,6 +20,7 @@ import { useLinter } from './linter';
import { useSettingsStore } from '@/stores/settings.store';
import { dropInCodeEditor } from '@/plugins/codemirror/dragAndDrop';
import type { TargetNodeParameterContext } from '@/Interface';
import { valueToInsert } from './utils';
export type CodeNodeLanguageOption = CodeNodeEditorLanguage | 'pythonNative';
@@ -203,12 +204,11 @@ function onAiLoadEnd() {
async function onDrop(value: string, event: MouseEvent) {
if (!editor.value) return;
const valueToInsert =
props.mode === 'runOnceForAllItems'
? value.replace('$json', '$input.first().json').replace(/\$\((.*)\)\.item/, '$($1).first()')
: value;
await dropInCodeEditor(toRaw(editor.value), event, valueToInsert);
await dropInCodeEditor(
toRaw(editor.value),
event,
valueToInsert(value, props.language, props.mode),
);
}
defineExpose({

View File

@@ -1,5 +1,5 @@
import * as esprima from 'esprima-next';
import { walk } from './utils';
import { valueToInsert, walk } from './utils';
describe('CodeNodeEditor utils', () => {
describe('walk', () => {
@@ -41,4 +41,88 @@ const y = f({ a: 'c' })
]);
});
});
describe('valueToInsert', () => {
describe('JavaScript', () => {
it('should convert input item correctly, runOnceForAllItems', () => {
expect(
valueToInsert('{{ $json.foo.bar[0].baz }}', 'javaScript', 'runOnceForAllItems'),
).toBe('{{ $input.first().json.foo.bar[0].baz }}');
});
it('should convert input item correctly, runOnceForEachItem', () => {
expect(
valueToInsert('{{ $json.foo.bar[0].baz }}', 'javaScript', 'runOnceForEachItem'),
).toBe('{{ $json.foo.bar[0].baz }}');
});
it('should convert previous node correctly, runOnceForAllItems', () => {
expect(
valueToInsert(
"{{ $('Some Previous Node').item.json.foo.bar[0].baz }}",
'javaScript',
'runOnceForAllItems',
),
).toBe("{{ $('Some Previous Node').first().json.foo.bar[0].baz }}");
});
it('should convert previous node correctly, runOnceForEachItem', () => {
expect(
valueToInsert(
"{{ $('Some Previous Node').item.json.foo.bar[0].baz }}",
'javaScript',
'runOnceForEachItem',
),
).toBe("{{ $('Some Previous Node').item.json.foo.bar[0].baz }}");
});
});
describe('Python (Pyodide)', () => {
it('should convert input item correctly, runOnceForAllItems', () => {
expect(valueToInsert('{{ $json.foo.bar[0].baz }}', 'python', 'runOnceForAllItems')).toBe(
'{{ _input.first().json.foo.bar[0].baz }}',
);
});
it('should convert input item correctly, runOnceForEachItem', () => {
expect(valueToInsert('{{ $json.foo.bar[0].baz }}', 'python', 'runOnceForEachItem')).toBe(
'{{ _input.item.json.foo.bar[0].baz }}',
);
});
it('should convert previous node correctly, runOnceForAllItems', () => {
expect(
valueToInsert(
"{{ $('Some Previous Node').item.json.foo.bar[0].baz }}",
'python',
'runOnceForAllItems',
),
).toBe("{{ _('Some Previous Node').first().json.foo.bar[0].baz }}");
});
it('should convert previous node correctly, runOnceForEachItem', () => {
expect(
valueToInsert(
"{{ $('Some Previous Node').item.json.foo.bar[0].baz }}",
'python',
'runOnceForEachItem',
),
).toBe("{{ _('Some Previous Node').item.json.foo.bar[0].baz }}");
});
});
describe('Python (Native)', () => {
it('should convert input item correctly, runOnceForAllItems', () => {
expect(
valueToInsert('{{ $json.foo.bar[0].baz }}', 'pythonNative', 'runOnceForAllItems'),
).toBe('{{ _items[0]["json"]["foo"]["bar"][0]["baz"] }}');
});
it('should convert input item correctly, runOnceForEachItem', () => {
expect(
valueToInsert('{{ $json.foo.bar[0].baz }}', 'pythonNative', 'runOnceForEachItem'),
).toBe('{{ _item["json"]["foo"]["bar"][0]["baz"] }}');
});
});
});
});

View File

@@ -3,6 +3,8 @@ import type { Completion } from '@codemirror/autocomplete';
import type { RangeNode } from './types';
import { sanitizeHtml } from '@/utils/htmlUtils';
import type { Node } from 'estree';
import type { CodeNodeLanguageOption } from './CodeNodeEditor.vue';
import type { CodeExecutionMode } from 'n8n-workflow';
export function walk<T extends RangeNode>(
node: Node | esprima.Program,
@@ -57,3 +59,45 @@ export const addInfoRenderer = (option: Completion): Completion => {
}
return option;
};
const DOT_CHAINS = /((?:\.[A-Za-z_$][A-Za-z0-9_$]*)+)/g;
const DOT_KEY = /\.(?<key>[A-Za-z_$][A-Za-z0-9_$]*)/g;
// Convert dot notation ".a.b.c" chains -> ["a"]["b"]["c"]
const toBracketNotation = (input: string): string => {
return input.replace(DOT_CHAINS, (chain) => chain.replace(DOT_KEY, '["$<key>"]'));
};
const pythonInsert = (value: string, mode: CodeExecutionMode): string => {
const base =
mode === 'runOnceForAllItems'
? value.replace('$json', '_items[0]["json"]')
: value.replace('$json', '_item["json"]');
return toBracketNotation(base);
};
const pyodideInsert = (value: string, mode: CodeExecutionMode): string => {
return value
.replace('$json', mode === 'runOnceForAllItems' ? '_input.first().json' : '_input.item.json')
.replace(/\$\((.*)\)\.item/, mode === 'runOnceForAllItems' ? '_($1).first()' : '_($1).item');
};
const jsInsertForAllItems = (value: string): string => {
return value.replace('$json', '$input.first().json').replace(/\$\((.*)\)\.item/, '$($1).first()');
};
const isPyodide = (language: CodeNodeLanguageOption) => language === 'python';
const isPython = (language: CodeNodeLanguageOption) => language === 'pythonNative';
export const valueToInsert = (
value: string,
language: CodeNodeLanguageOption,
mode: CodeExecutionMode,
): string => {
if (isPython(language)) return pythonInsert(value, mode);
if (isPyodide(language)) return pyodideInsert(value, mode);
if (mode === 'runOnceForAllItems') return jsInsertForAllItems(value);
return value;
};