refactor(editor): Update Code node editor for native Python runner (#18538)

This commit is contained in:
Iván Ovejero
2025-08-19 13:40:02 +02:00
committed by GitHub
parent 5c53c22d0a
commit fabbddefdc
7 changed files with 52 additions and 18 deletions

View File

@@ -21,12 +21,14 @@ import { useSettingsStore } from '@/stores/settings.store';
import { dropInCodeEditor } from '@/plugins/codemirror/dragAndDrop'; import { dropInCodeEditor } from '@/plugins/codemirror/dragAndDrop';
import type { TargetNodeParameterContext } from '@/Interface'; import type { TargetNodeParameterContext } from '@/Interface';
export type CodeNodeLanguageOption = CodeNodeEditorLanguage | 'pythonNative';
type Props = { type Props = {
mode: CodeExecutionMode; mode: CodeExecutionMode;
modelValue: string; modelValue: string;
aiButtonEnabled?: boolean; aiButtonEnabled?: boolean;
fillParent?: boolean; fillParent?: boolean;
language?: CodeNodeEditorLanguage; language?: CodeNodeLanguageOption;
isReadOnly?: boolean; isReadOnly?: boolean;
rows?: number; rows?: number;
id?: string; id?: string;
@@ -63,7 +65,7 @@ const settingsStore = useSettingsStore();
const linter = useLinter( const linter = useLinter(
() => props.mode, () => props.mode,
() => props.language, () => (props.language === 'pythonNative' ? 'python' : props.language),
); );
const extensions = computed(() => [linter.value]); const extensions = computed(() => [linter.value]);
const placeholder = computed(() => CODE_PLACEHOLDERS[props.language]?.[props.mode] ?? ''); const placeholder = computed(() => CODE_PLACEHOLDERS[props.language]?.[props.mode] ?? '');

View File

@@ -24,7 +24,20 @@ export const useCompleter = (
mode: MaybeRefOrGetter<CodeExecutionMode>, mode: MaybeRefOrGetter<CodeExecutionMode>,
editor: MaybeRefOrGetter<EditorView | null>, editor: MaybeRefOrGetter<EditorView | null>,
) => { ) => {
function autocompletionExtension(language: 'javaScript' | 'python'): Extension { function autocompletionExtension(language: 'javaScript' | 'python' | 'pythonNative'): Extension {
if (language === 'pythonNative') {
const completions = (context: CompletionContext): CompletionResult | null => {
const word = context.matchBefore(/\w*/);
if (!word) return null;
const label = toValue(mode) === 'runOnceForEachItem' ? '_item' : '_items';
return { from: word.from, options: [{ label, type: 'variable' }] };
};
return autocompletion({ icons: false, override: [completions] });
}
// Base completions // Base completions
const { baseCompletions, itemCompletions, nodeSelectorCompletions } = useBaseCompletions( const { baseCompletions, itemCompletions, nodeSelectorCompletions } = useBaseCompletions(
toValue(mode), toValue(mode),

View File

@@ -1,6 +1,7 @@
import { STICKY_NODE_TYPE } from '@/constants'; import { STICKY_NODE_TYPE } from '@/constants';
import type { Diagnostic } from '@codemirror/lint'; import type { Diagnostic } from '@codemirror/lint';
import type { CodeExecutionMode, CodeNodeEditorLanguage } from 'n8n-workflow'; import type { CodeExecutionMode } from 'n8n-workflow';
import type { CodeNodeLanguageOption } from './CodeNodeEditor.vue';
export const NODE_TYPES_EXCLUDED_FROM_AUTOCOMPLETION = [STICKY_NODE_TYPE]; export const NODE_TYPES_EXCLUDED_FROM_AUTOCOMPLETION = [STICKY_NODE_TYPE];
@@ -36,7 +37,7 @@ export const DEFAULT_LINTER_DELAY_IN_MS = 500;
export const OFFSET_FOR_SCRIPT_WRAPPER = 'module.exports = async function() {'.length; export const OFFSET_FOR_SCRIPT_WRAPPER = 'module.exports = async function() {'.length;
export const CODE_PLACEHOLDERS: Partial< export const CODE_PLACEHOLDERS: Partial<
Record<CodeNodeEditorLanguage, Record<CodeExecutionMode, string>> Record<CodeNodeLanguageOption, Record<CodeExecutionMode, string>>
> = { > = {
javaScript: { javaScript: {
runOnceForAllItems: ` runOnceForAllItems: `
@@ -63,4 +64,15 @@ return _input.all()`.trim(),
_input.item.json.myNewField = 1 _input.item.json.myNewField = 1
return _input.item`.trim(), return _input.item`.trim(),
}, },
pythonNative: {
runOnceForAllItems: `
# Loop over input items and add a new field called 'my_new_field' to the JSON of each one
for item in _items:
item["json"]["my_new_field"] = 1
return _items`.trim(),
runOnceForEachItem: `
# Add a new field called 'my_new_field' to the JSON of the item
_item["json"]["my_new_field"] = 1
return _item`.trim(),
},
}; };

View File

@@ -11,7 +11,6 @@ import type {
} from '@/Interface'; } from '@/Interface';
import type { import type {
CodeExecutionMode, CodeExecutionMode,
CodeNodeEditorLanguage,
EditorType, EditorType,
IDataObject, IDataObject,
ILoadOptions, ILoadOptions,
@@ -24,6 +23,7 @@ import type {
} from 'n8n-workflow'; } from 'n8n-workflow';
import { CREDENTIAL_EMPTY_VALUE, isResourceLocatorValue, NodeHelpers } from 'n8n-workflow'; import { CREDENTIAL_EMPTY_VALUE, isResourceLocatorValue, NodeHelpers } from 'n8n-workflow';
import type { CodeNodeLanguageOption } from '@/components/CodeNodeEditor/CodeNodeEditor.vue';
import CodeNodeEditor from '@/components/CodeNodeEditor/CodeNodeEditor.vue'; import CodeNodeEditor from '@/components/CodeNodeEditor/CodeNodeEditor.vue';
import CredentialsSelect from '@/components/CredentialsSelect.vue'; import CredentialsSelect from '@/components/CredentialsSelect.vue';
import ExpressionEditModal from '@/components/ExpressionEditModal.vue'; import ExpressionEditModal from '@/components/ExpressionEditModal.vue';
@@ -259,10 +259,12 @@ const editorIsReadOnly = computed<boolean>(() => {
return getTypeOption<boolean>('editorIsReadOnly') ?? false; return getTypeOption<boolean>('editorIsReadOnly') ?? false;
}); });
const editorLanguage = computed<CodeNodeEditorLanguage>(() => { const editorLanguage = computed<CodeNodeLanguageOption>(() => {
if (editorType.value === 'json' || props.parameter.type === 'json') if (editorType.value === 'json' || props.parameter.type === 'json') return 'json';
return 'json' as CodeNodeEditorLanguage;
return getTypeOption<CodeNodeEditorLanguage>('editorLanguage') ?? 'javaScript'; if (node.value?.parameters?.language === 'pythonNative') return 'pythonNative';
return getTypeOption<CodeNodeLanguageOption>('editorLanguage') ?? 'javaScript';
}); });
const codeEditorMode = computed<CodeExecutionMode>(() => { const codeEditorMode = computed<CodeExecutionMode>(() => {

View File

@@ -50,19 +50,21 @@ import {
} from 'vue'; } from 'vue';
import { useCompleter } from '../components/CodeNodeEditor/completer'; import { useCompleter } from '../components/CodeNodeEditor/completer';
import { mappingDropCursor } from '../plugins/codemirror/dragAndDrop'; import { mappingDropCursor } from '../plugins/codemirror/dragAndDrop';
import { languageFacet, type CodeEditorLanguage } from '../plugins/codemirror/format'; import { languageFacet } from '../plugins/codemirror/format';
import debounce from 'lodash/debounce'; import debounce from 'lodash/debounce';
import { ignoreUpdateAnnotation } from '../utils/forceParse'; import { ignoreUpdateAnnotation } from '../utils/forceParse';
import type { TargetNodeParameterContext } from '@/Interface'; import type { TargetNodeParameterContext } from '@/Interface';
import type { CodeNodeLanguageOption } from '@/components/CodeNodeEditor/CodeNodeEditor.vue';
export type CodeEditorLanguageParamsMap = { export type CodeEditorLanguageParamsMap = {
json: {}; json: {};
html: {}; html: {};
javaScript: { mode: CodeExecutionMode }; javaScript: { mode: CodeExecutionMode };
python: { mode: CodeExecutionMode }; python: { mode: CodeExecutionMode };
pythonNative: { mode: CodeExecutionMode };
}; };
export const useCodeEditor = <L extends CodeEditorLanguage>({ export const useCodeEditor = <L extends CodeNodeLanguageOption>({
editorRef, editorRef,
editorValue, editorValue,
language, language,
@@ -116,7 +118,7 @@ export const useCodeEditor = <L extends CodeEditorLanguage>({
targetNodeParameterContext, targetNodeParameterContext,
); );
function getInitialLanguageExtensions(lang: CodeEditorLanguage): Extension[] { function getInitialLanguageExtensions(lang: CodeNodeLanguageOption): Extension[] {
switch (lang) { switch (lang) {
case 'javaScript': case 'javaScript':
return [javascript()]; return [javascript()];
@@ -128,7 +130,9 @@ export const useCodeEditor = <L extends CodeEditorLanguage>({
async function getFullLanguageExtensions(): Promise<Extension[]> { async function getFullLanguageExtensions(): Promise<Extension[]> {
if (!editor.value) return []; if (!editor.value) return [];
const lang = toValue(language); const lang = toValue(language);
const langExtensions: Extension[] = [languageFacet.of(lang)]; const langExtensions: Extension[] = [
languageFacet.of(lang === 'pythonNative' ? 'python' : lang),
];
switch (lang) { switch (lang) {
case 'javaScript': { case 'javaScript': {
@@ -136,9 +140,10 @@ export const useCodeEditor = <L extends CodeEditorLanguage>({
langExtensions.push(tsExtension); langExtensions.push(tsExtension);
break; break;
} }
case 'python': { case 'python':
case 'pythonNative': {
const pythonAutocomplete = useCompleter(mode, editor.value ?? null).autocompletionExtension( const pythonAutocomplete = useCompleter(mode, editor.value ?? null).autocompletionExtension(
'python', lang,
); );
langExtensions.push([python(), pythonAutocomplete]); langExtensions.push([python(), pythonAutocomplete]);
break; break;

View File

@@ -48,7 +48,7 @@ export const pythonCodeDescription: INodeProperties[] = [
default: '', default: '',
}, },
{ {
displayName: `${PRINT_INSTRUCTION}<br><br>The native Python option does not support <code>_</code> syntax and helpers, except for <code>_items</code> and <code>_item</code>.`, displayName: `${PRINT_INSTRUCTION}<br><br>The native Python option does not support <code>_</code> syntax and helpers, except for <code>_items</code> in all-items mode and <code>_item</code> in per-item mode.`,
name: 'notice', name: 'notice',
type: 'notice', type: 'notice',
displayOptions: { displayOptions: {

View File

@@ -8,7 +8,7 @@ export const WAIT_INDEFINITELY = new Date('3000-01-01T00:00:00.000Z');
export const LOG_LEVELS = ['silent', 'error', 'warn', 'info', 'debug'] as const; export const LOG_LEVELS = ['silent', 'error', 'warn', 'info', 'debug'] as const;
export const CODE_LANGUAGES = ['javaScript', 'python'] as const; export const CODE_LANGUAGES = ['javaScript', 'python', 'json', 'html'] as const;
export const CODE_EXECUTION_MODES = ['runOnceForAllItems', 'runOnceForEachItem'] as const; export const CODE_EXECUTION_MODES = ['runOnceForAllItems', 'runOnceForEachItem'] as const;
// Arbitrary value to represent an empty credential value // Arbitrary value to represent an empty credential value