diff --git a/packages/frontend/editor-ui/src/components/CodeNodeEditor/CodeNodeEditor.vue b/packages/frontend/editor-ui/src/components/CodeNodeEditor/CodeNodeEditor.vue index bfd16bb586..5ca1e300c5 100644 --- a/packages/frontend/editor-ui/src/components/CodeNodeEditor/CodeNodeEditor.vue +++ b/packages/frontend/editor-ui/src/components/CodeNodeEditor/CodeNodeEditor.vue @@ -21,12 +21,14 @@ import { useSettingsStore } from '@/stores/settings.store'; import { dropInCodeEditor } from '@/plugins/codemirror/dragAndDrop'; import type { TargetNodeParameterContext } from '@/Interface'; +export type CodeNodeLanguageOption = CodeNodeEditorLanguage | 'pythonNative'; + type Props = { mode: CodeExecutionMode; modelValue: string; aiButtonEnabled?: boolean; fillParent?: boolean; - language?: CodeNodeEditorLanguage; + language?: CodeNodeLanguageOption; isReadOnly?: boolean; rows?: number; id?: string; @@ -63,7 +65,7 @@ const settingsStore = useSettingsStore(); const linter = useLinter( () => props.mode, - () => props.language, + () => (props.language === 'pythonNative' ? 'python' : props.language), ); const extensions = computed(() => [linter.value]); const placeholder = computed(() => CODE_PLACEHOLDERS[props.language]?.[props.mode] ?? ''); diff --git a/packages/frontend/editor-ui/src/components/CodeNodeEditor/completer.ts b/packages/frontend/editor-ui/src/components/CodeNodeEditor/completer.ts index 7e5a96b13c..cf45ad972c 100644 --- a/packages/frontend/editor-ui/src/components/CodeNodeEditor/completer.ts +++ b/packages/frontend/editor-ui/src/components/CodeNodeEditor/completer.ts @@ -24,7 +24,20 @@ export const useCompleter = ( mode: MaybeRefOrGetter, editor: MaybeRefOrGetter, ) => { - 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 const { baseCompletions, itemCompletions, nodeSelectorCompletions } = useBaseCompletions( toValue(mode), diff --git a/packages/frontend/editor-ui/src/components/CodeNodeEditor/constants.ts b/packages/frontend/editor-ui/src/components/CodeNodeEditor/constants.ts index 2f648e41d2..0c6ffca575 100644 --- a/packages/frontend/editor-ui/src/components/CodeNodeEditor/constants.ts +++ b/packages/frontend/editor-ui/src/components/CodeNodeEditor/constants.ts @@ -1,6 +1,7 @@ import { STICKY_NODE_TYPE } from '@/constants'; 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]; @@ -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 CODE_PLACEHOLDERS: Partial< - Record> + Record> > = { javaScript: { runOnceForAllItems: ` @@ -63,4 +64,15 @@ return _input.all()`.trim(), _input.item.json.myNewField = 1 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(), + }, }; diff --git a/packages/frontend/editor-ui/src/components/ParameterInput.vue b/packages/frontend/editor-ui/src/components/ParameterInput.vue index f89912887b..8aef938222 100644 --- a/packages/frontend/editor-ui/src/components/ParameterInput.vue +++ b/packages/frontend/editor-ui/src/components/ParameterInput.vue @@ -11,7 +11,6 @@ import type { } from '@/Interface'; import type { CodeExecutionMode, - CodeNodeEditorLanguage, EditorType, IDataObject, ILoadOptions, @@ -24,6 +23,7 @@ import type { } 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 CredentialsSelect from '@/components/CredentialsSelect.vue'; import ExpressionEditModal from '@/components/ExpressionEditModal.vue'; @@ -259,10 +259,12 @@ const editorIsReadOnly = computed(() => { return getTypeOption('editorIsReadOnly') ?? false; }); -const editorLanguage = computed(() => { - if (editorType.value === 'json' || props.parameter.type === 'json') - return 'json' as CodeNodeEditorLanguage; - return getTypeOption('editorLanguage') ?? 'javaScript'; +const editorLanguage = computed(() => { + if (editorType.value === 'json' || props.parameter.type === 'json') return 'json'; + + if (node.value?.parameters?.language === 'pythonNative') return 'pythonNative'; + + return getTypeOption('editorLanguage') ?? 'javaScript'; }); const codeEditorMode = computed(() => { diff --git a/packages/frontend/editor-ui/src/composables/useCodeEditor.ts b/packages/frontend/editor-ui/src/composables/useCodeEditor.ts index c92245aeba..f30caab78f 100644 --- a/packages/frontend/editor-ui/src/composables/useCodeEditor.ts +++ b/packages/frontend/editor-ui/src/composables/useCodeEditor.ts @@ -50,19 +50,21 @@ import { } from 'vue'; import { useCompleter } from '../components/CodeNodeEditor/completer'; 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 { ignoreUpdateAnnotation } from '../utils/forceParse'; import type { TargetNodeParameterContext } from '@/Interface'; +import type { CodeNodeLanguageOption } from '@/components/CodeNodeEditor/CodeNodeEditor.vue'; export type CodeEditorLanguageParamsMap = { json: {}; html: {}; javaScript: { mode: CodeExecutionMode }; python: { mode: CodeExecutionMode }; + pythonNative: { mode: CodeExecutionMode }; }; -export const useCodeEditor = ({ +export const useCodeEditor = ({ editorRef, editorValue, language, @@ -116,7 +118,7 @@ export const useCodeEditor = ({ targetNodeParameterContext, ); - function getInitialLanguageExtensions(lang: CodeEditorLanguage): Extension[] { + function getInitialLanguageExtensions(lang: CodeNodeLanguageOption): Extension[] { switch (lang) { case 'javaScript': return [javascript()]; @@ -128,7 +130,9 @@ export const useCodeEditor = ({ async function getFullLanguageExtensions(): Promise { if (!editor.value) return []; const lang = toValue(language); - const langExtensions: Extension[] = [languageFacet.of(lang)]; + const langExtensions: Extension[] = [ + languageFacet.of(lang === 'pythonNative' ? 'python' : lang), + ]; switch (lang) { case 'javaScript': { @@ -136,9 +140,10 @@ export const useCodeEditor = ({ langExtensions.push(tsExtension); break; } - case 'python': { + case 'python': + case 'pythonNative': { const pythonAutocomplete = useCompleter(mode, editor.value ?? null).autocompletionExtension( - 'python', + lang, ); langExtensions.push([python(), pythonAutocomplete]); break; diff --git a/packages/nodes-base/nodes/Code/descriptions/PythonCodeDescription.ts b/packages/nodes-base/nodes/Code/descriptions/PythonCodeDescription.ts index a15bacd7a8..3a2451c2a6 100644 --- a/packages/nodes-base/nodes/Code/descriptions/PythonCodeDescription.ts +++ b/packages/nodes-base/nodes/Code/descriptions/PythonCodeDescription.ts @@ -48,7 +48,7 @@ export const pythonCodeDescription: INodeProperties[] = [ default: '', }, { - displayName: `${PRINT_INSTRUCTION}

The native Python option does not support _ syntax and helpers, except for _items and _item.`, + displayName: `${PRINT_INSTRUCTION}

The native Python option does not support _ syntax and helpers, except for _items in all-items mode and _item in per-item mode.`, name: 'notice', type: 'notice', displayOptions: { diff --git a/packages/workflow/src/constants.ts b/packages/workflow/src/constants.ts index 92773cf258..8b9dba56e3 100644 --- a/packages/workflow/src/constants.ts +++ b/packages/workflow/src/constants.ts @@ -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 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; // Arbitrary value to represent an empty credential value