Skip to content

Commit

Permalink
feat(Code Node): create Code node (#3965)
Browse files Browse the repository at this point in the history
* Introduce node deprecation (#3930)

:sparkles: Introduce node deprecation

* :construction: Scaffold out Code node

* :shirt: Fix lint

* :blue_book: Create types file

* :truck: Rename theme

* :fire: Remove unneeded prop

* :zap: Override keybindings

* :zap: Expand lintings

* :zap: Create editor content getter

* :truck: Ensure all helpers use `$`

* :sparkles: Add autocompletion

* :zap: Filter out welcome note node

* :zap: Convey error line number

* :zap: Highlight error line

* :zap: Restore logging from node

* :sparkles: More autocompletions

* :zap: Streamline completions

* :pencil2: Update placeholders

* :zap: Update linter to new methods

* :fire: Remove `$nodeItem` completions

* :zap: Re-update placeholders

* :art: Fix formatting

* :package: Update `package-lock.json`

* :zap: Refresh with multi-line empty string

* :zap: Account for syntax errors

* :fire: Remove unneeded variant

* :zap: Minor improvements

* :zap: Add more autocompletions

* :truck: Rename extension

* :fire: Remove outdated comments

* :truck: Rename field

* :sparkles: More autocompletions

* :zap: Fix up error display when empty text

* :fire: Remove logging

* :sparkles: More error validation

* :bug: Fix `pairedItem` to `pairedItem()`

* :zap: Add item to validation info

* :package: Update `package-lock.json`

* :zap: Leftover fixes

* :zap: Set `insertNewlineAndIndent`

* :package: Update `package-lock.json`

* :package: Re-update `package-lock.json`

* :shirt: Add lint exception

* :blue_book: Add type to mixin type

* Clean up comment

* :zap: Refactor completion per new requirements

* :zap: Adjust placeholders

* :zap: Add `json` autocompletions for `$input`

* :art: Set border

* :zap: Restore local completion source

* :zap: Implement autocompletion for imports

* :zap: Add `.*` to follow user typing on autocompletion

* :blue_book: Fix typings in autocompletions

* :shirt: Add linting for use of `item()`

* :package: Update `package-lock.json`

* :bug: Fix for `$items(nodeName)[0]`

* :zap: Filter down built-in modules list

* :zap: Refactor error handling

* :zap: Linter and validation improvements

* :zap: Apply review feedback

* :recycle: More general refactorings

* :zap: Add dot notation utility

* Customize input handler

* :zap: Support `.json.` completions

* :zap: Adjust placeholder

* :zap: Sort imports

* :fire: Remove blank rows addition

* :zap: Add more error validation

* :package: Update `package-lock.json`

* :zap: Make date logging consistent

* :wrench: Adjust linting highlight range

* :zap: Add line numbers to each item mode errors

* :zap: Allow for links in error descriptions

* :zap: More input validation

* :zap: Expand linting to loops

* :zap: Deprecate Function and Function Item nodes

* :bug: Fix placeholder syntax

* :blue_book: Narrow down type

* :truck: Rename using kebab-case

* :fire: Remove `mapGetters`

* :pencil2: Fix casing

* :zap: Adjust import for type

* :pencil2: Fix quotes

* :bug: Fix `activeNode` reference

* :zap: Use constant

* :fire: Remove logging

* :pencil2: Fix typo

* :zap: Add missing `notice`

* :pencil2: Add tags

* :pencil2: Fix alias

* :pencil2: Update copy

* :fire: Remove wrong linting

* :pencil2: Update copy

* :zap: Add validation for `null`

* :zap: Add validation for non-object and non-array

* :zap: Add validation for non-array with json

* :pencil2: Intentionally use wrong spelling

* :zap: More validation

* :pencil2: More copy updates

* :pencil2: Placeholder updates

* :rewind: Restore spelling

* :zap: Fix var name

* :pencil2: More copy updates

* :zap: Add luxon autocompletions

* :zap: Make scrollable

* :zap: Fix comma from merge conflict resolution

* :package: Update `package-lock.json`

* :shirt: Fix lint detail

* :art: Set font family

* :zap: Bring in expressions fix

* :recycle: Address feedback

* :zap: Exclude codemirror packages from render chunks

* :bug: Fix placeholder not showing on first load

* feat(editor-ui): Replace `lezer` with `esprima` in client linter (#4192)

* 🔥 Remove addition from misresolved conflict

* ⚡ Replace `lezer` with `esprima` in client linter

* ⚡ Add missing key

* 📦 Update `package-lock.json`

* ⚡ Match dependencies

* 📦 Update `package-lock.json`

* 📦 Re-update `package-lock.json`

* ⚡ Match whitespace

* 🐛 Fix selection

* ⚡ Expand validation

* 🔥 Remove validation

* ✏️ Update copy

* 🚚 Move to constants

* ⚡ More `null` validation

* ⚡ Support `all()` with index to access item

* ⚡ Gloss over n8n syntax error

* 🎨 Re-style diagnostic button

* 🔥 Remove `item` as `itemAlias`

* ⚡ Add linting for `item.json` in single item mode

* ⚡ Refactor to add label info descriptions

* ⚡ More autocompletions

* 👕 Fix lint

* ⚡ Simplify typings

* feat(nodes-base): Multiline autocompletion for `code-node-editor` (#4220)

* ⚡ Simplify typings

* ⚡ Consolidate helpers in utils

* ⚡ Multiline autocompletion for standalone vars

* 🔥 Remove unneeded mixins

* ✏️ Update copy

* ✏️ Prep TODOs

* ⚡ Multiline completion for `$input.method` + `$input.item`

* 🔥 Remove unused method

* 🔥 Remove another unused method

* 🚚 Move luxon strings to helpers

* ⚡ Multiline autocompletion for methods output

* ⚡ Refactor to use optional chaining

* 👕 Fix lint

* ✏️ Update TODOs

* ⚡ Multiline autocompletion for `json` fields

* 📘 Add typings

* ⚡ De-duplicate callback to forEach

* 🐛 Fix autocompletions not working with leading whitespace

* 🌐 Apply i18n

* 👕 Fix lint

* :constructor: Second-period var usage completions

* 👕 Fix lint

* 👕 Add exception

* ⚡ Add completion telemetry

* 📘 Add typing

* ⚡ Major refactoring to organize

* 🐛 Fix multiline `.all()[index]`

* 🐛 Do not autoclose square brackets prior to `.json`

* 🐛 Fix accessor for multiline `jsonField` completions

* ⚡ Add completions for half-assignments

* 🐛 Fix `jsonField` completions for `x.json`

* ✏️ Improve comments

* 🐛 Fix `.json[field]` for multiline matches

* ⚡ Cleanup

* 📦 Update `package-lock.json`

* 👕 Fix lint

* 🐛 Rely on original value for custom matcher

* ⚡ Create `customMatcherJsonFieldCompletions` to simplify setup

* 🐛 Include selector in `customMatcherJsonField` completions

* ✏️ Make naming consistent

* ✏️ Add docline

* ⚡ Finish self-review cleanup

* 🔥 Remove outdated comment

* 📌 Pin luxon to major-minor

* ✏️ Fix typo

* 📦 Update `package-lock.json`

* 📦 Update `package-lock.json`

* 📦 Re-update `package-lock.json`

* ➕ Add `luxon` for Gmail node

* 📦 Update `package-lock.json`

* ⚡ Replace Function with Code in suggested nodes

* 🐛 Fix `$prevNode` completions

* ✏️ Update `$execution.mode` copy

* ⚡ Separate luxon getters from methods

* ⚡ Adjusting linter to tolerate `.binary`

* ⚡ Adjust top-level item keys check

* ⚡ Anticipate user expecting `item` to pre-exist

* ⚡ Add linting for legacy item access

* ⚡ Add hint for attempted `items` access

* ⚡ Add keybinding for toggling comments

* ✏️ Update copy of `all`, `first`, `last` and `itemMatching`

* 🐛 Make `input.all()` etc act on copies

* 📦 Update `package-lock.json`

* 🐛 Fix guard in `$input.last()`

* ♻️ Address Jan's feedback

* ⬆️ Upgrade `eslint-plugin-n8n-nodes-base`

* 📦 Update `package-lock.json`

* 🔥 Remove unneeded exceptions

* ⚡ Restore placeholder logic

* ⚡ Add placeholders to client

* ⚡ Account for shadow item

* ✏️ More completion info labels

* 👕 Fix lint

* ✏️ Update copy

* ✏️ Update copy

* ✏️ More copy updates

* 📦 Update `package-lock.json`

* ⚡ Add more validation

* ⚡ Add placheolder on first load

* Replace `Cmd` with `Mod`

* 📦 Update `package-lock.json`
  • Loading branch information
ivov authored Oct 13, 2022
1 parent 12e8215 commit 1db4fa2
Show file tree
Hide file tree
Showing 54 changed files with 5,122 additions and 1,395 deletions.
3,081 changes: 1,713 additions & 1,368 deletions package-lock.json

Large diffs are not rendered by default.

4 changes: 4 additions & 0 deletions packages/cli/src/Interfaces.ts
Original file line number Diff line number Diff line change
Expand Up @@ -518,6 +518,10 @@ export interface IN8nUISettings {
type: string;
};
isNpmAvailable: boolean;
allowedModules: {
builtIn?: string;
external?: string;
};
enterprise: {
sharing: boolean;
workflowSharing: boolean;
Expand Down
4 changes: 4 additions & 0 deletions packages/cli/src/Server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -328,6 +328,10 @@ class App {
type: config.getEnv('deployment.type'),
},
isNpmAvailable: false,
allowedModules: {
builtIn: process.env.NODE_FUNCTION_ALLOW_BUILTIN,
external: process.env.NODE_FUNCTION_ALLOW_EXTERNAL,
},
enterprise: {
sharing: false,
workflowSharing: false,
Expand Down
2 changes: 1 addition & 1 deletion packages/cli/src/WorkflowExecuteAdditionalData.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1046,7 +1046,7 @@ export function sendMessageToUI(source: string, messages: any[]) {
pushInstance.send(
'sendConsoleMessage',
{
source: `Node: "${source}"`,
source: `[Node: "${source}"]`,
messages,
},
this.sessionId,
Expand Down
17 changes: 15 additions & 2 deletions packages/core/src/NodeExecuteFunctions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1345,8 +1345,10 @@ export function constructExecutionMetaData(
export function normalizeItems(
executionData: INodeExecutionData | INodeExecutionData[],
): INodeExecutionData[] {
if (typeof executionData === 'object' && !Array.isArray(executionData))
executionData = [{ json: executionData as IDataObject }];
if (typeof executionData === 'object' && !Array.isArray(executionData)) {
executionData = executionData.json ? [executionData] : [{ json: executionData as IDataObject }];
}

if (executionData.every((item) => typeof item === 'object' && 'json' in item))
return executionData;

Expand Down Expand Up @@ -2297,6 +2299,17 @@ export function getExecuteFunctions(
}
try {
if (additionalData.sendMessageToUI) {
args = args.map((arg) => {
// prevent invalid dates from being logged as null
if (arg.isLuxonDateTime && arg.invalidReason) return { ...arg };

// log valid dates in human readable format, as in browser
if (arg.isLuxonDateTime) return new Date(arg.ts).toString();
if (arg instanceof Date) return arg.toString();

return arg;
});

additionalData.sendMessageToUI(node.name, args);
}
} catch (error) {
Expand Down
11 changes: 10 additions & 1 deletion packages/editor-ui/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -25,13 +25,21 @@
"test:dev": "vitest"
},
"dependencies": {
"@codemirror/autocomplete": "^6.1.0",
"@codemirror/commands": "^6.1.0",
"@codemirror/lang-javascript": "^6.0.2",
"@codemirror/language": "^6.2.1",
"@codemirror/lint": "^6.0.0",
"@codemirror/state": "^6.1.1",
"@codemirror/view": "^6.2.1",
"@fontsource/open-sans": "^4.5.0",
"@fortawesome/fontawesome-svg-core": "^1.2.35",
"@fortawesome/free-regular-svg-icons": "^6.1.1",
"@fortawesome/free-solid-svg-icons": "^5.15.3",
"@fortawesome/vue-fontawesome": "^2.0.2",
"axios": "^0.21.1",
"dateformat": "^3.0.3",
"esprima": "^4.0.1",
"fast-json-stable-stringify": "^2.1.0",
"file-saver": "^2.0.2",
"flatted": "^3.2.4",
Expand All @@ -44,7 +52,7 @@
"lodash.get": "^4.4.2",
"lodash.orderby": "^4.6.0",
"lodash.set": "^4.3.2",
"luxon": "^2.3.0",
"luxon": "~2.3.0",
"monaco-editor": "^0.30.1",
"n8n-design-system": "~0.37.0",
"n8n-workflow": "~0.119.0",
Expand Down Expand Up @@ -76,6 +84,7 @@
"@testing-library/jest-dom": "^5.16.5",
"@testing-library/vue": "^6.6.1",
"@types/dateformat": "^3.0.0",
"@types/esprima": "^4.0.3",
"@types/express": "^4.17.6",
"@types/file-saver": "^2.0.1",
"@types/jsonpath": "^0.2.0",
Expand Down
4 changes: 4 additions & 0 deletions packages/editor-ui/src/Interface.ts
Original file line number Diff line number Diff line change
Expand Up @@ -726,6 +726,10 @@ export interface IN8nUISettings {
path: string;
};
onboardingCallPromptEnabled: boolean;
allowedModules: {
builtIn?: string[];
external?: string[];
};
enterprise: Record<string, boolean>;
deployment?: {
type: string;
Expand Down
174 changes: 174 additions & 0 deletions packages/editor-ui/src/components/CodeNodeEditor/CodeNodeEditor.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,174 @@
<template>
<div ref="codeNodeEditor" class="ph-no-capture" />
</template>

<script lang="ts">
import mixins from 'vue-typed-mixins';
import { Compartment, EditorState } from '@codemirror/state';
import { EditorView, ViewUpdate } from '@codemirror/view';
import { javascript } from '@codemirror/lang-javascript';
import { baseExtensions } from './baseExtensions';
import { linterExtension } from './linter';
import { completerExtension } from './completer';
import { CODE_NODE_EDITOR_THEME } from './theme';
import { workflowHelpers } from '../mixins/workflowHelpers'; // for json field completions
import { codeNodeEditorEventBus } from '@/event-bus/code-node-editor-event-bus';
import { CODE_NODE_TYPE } from '@/constants';
import { ALL_ITEMS_PLACEHOLDER, EACH_ITEM_PLACEHOLDER } from './constants';
export default mixins(linterExtension, completerExtension, workflowHelpers).extend({
name: 'code-node-editor',
props: {
mode: {
type: String,
validator: (value: string): boolean =>
['runOnceForAllItems', 'runOnceForEachItem'].includes(value),
},
isReadOnly: {
type: Boolean,
default: false,
},
jsCode: {
type: String,
},
},
data() {
return {
editor: null as EditorView | null,
linterCompartment: new Compartment(),
};
},
watch: {
mode() {
this.reloadLinter();
this.refreshPlaceholder();
},
},
computed: {
content(): string {
if (!this.editor) return '';
return this.editor.state.doc.toString();
},
placeholder(): string {
return {
runOnceForAllItems: ALL_ITEMS_PLACEHOLDER,
runOnceForEachItem: EACH_ITEM_PLACEHOLDER,
}[this.mode];
},
previousPlaceholder(): string {
return {
runOnceForAllItems: EACH_ITEM_PLACEHOLDER,
runOnceForEachItem: ALL_ITEMS_PLACEHOLDER,
}[this.mode];
},
},
methods: {
reloadLinter() {
if (!this.editor) return;
this.editor.dispatch({
effects: this.linterCompartment.reconfigure(this.linterExtension()),
});
},
refreshPlaceholder() {
if (!this.editor) return;
if (!this.content.trim() || this.content.trim() === this.previousPlaceholder) {
this.editor.dispatch({
changes: { from: 0, to: this.content.length, insert: this.placeholder },
});
}
},
highlightLine(line: number | 'final') {
if (!this.editor) return;
if (line === 'final') {
this.editor.dispatch({
selection: { anchor: this.content.trim().length },
});
return;
}
this.editor.dispatch({
selection: { anchor: this.editor.state.doc.line(line).from },
});
},
trackCompletion(viewUpdate: ViewUpdate) {
const completionTx = viewUpdate.transactions.find((tx) => tx.isUserEvent('input.complete'));
if (!completionTx) return;
try {
// @ts-ignore - undocumented fields
const { fromA, toB } = viewUpdate?.changedRanges[0];
const full = this.content.slice(fromA, toB);
const lastDotIndex = full.lastIndexOf('.');
let context = null;
let insertedText = null;
if (lastDotIndex === -1) {
context = '';
insertedText = full;
} else {
context = full.slice(0, lastDotIndex);
insertedText = full.slice(lastDotIndex + 1);
}
this.$telemetry.track('User autocompleted code', {
instance_id: this.$store.getters.instanceId,
node_type: CODE_NODE_TYPE,
field_name: this.mode === 'runOnceForAllItems' ? 'jsCodeAllItems' : 'jsCodeEachItem',
field_type: 'code',
context,
inserted_text: insertedText,
});
} catch (_) {}
},
},
destroyed() {
codeNodeEditorEventBus.$off('error-line-number', this.highlightLine);
},
mounted() {
codeNodeEditorEventBus.$on('error-line-number', this.highlightLine);
const stateBasedExtensions = [
this.linterCompartment.of(this.linterExtension()),
EditorState.readOnly.of(this.isReadOnly),
EditorView.updateListener.of((viewUpdate: ViewUpdate) => {
if (!viewUpdate.docChanged) return;
this.trackCompletion(viewUpdate);
this.$emit('valueChanged', this.content);
}),
];
// empty on first load, default param value
if (this.jsCode === '') {
this.$emit('valueChanged', this.placeholder);
}
const state = EditorState.create({
doc: this.jsCode === '' ? this.placeholder : this.jsCode,
extensions: [
...baseExtensions,
...stateBasedExtensions,
CODE_NODE_EDITOR_THEME,
javascript(),
this.autocompletionExtension(),
],
});
this.editor = new EditorView({
parent: this.$refs.codeNodeEditor as HTMLDivElement,
state,
});
},
});
</script>

<style lang="scss" scoped></style>
40 changes: 40 additions & 0 deletions packages/editor-ui/src/components/CodeNodeEditor/baseExtensions.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
import {
dropCursor,
EditorView,
highlightActiveLine,
highlightActiveLineGutter,
highlightSpecialChars,
keymap,
lineNumbers,
} from '@codemirror/view';
import { bracketMatching, foldGutter, indentOnInput } from '@codemirror/language';
import { acceptCompletion, closeBrackets } from '@codemirror/autocomplete';
import { history, indentWithTab, insertNewlineAndIndent, toggleComment } from '@codemirror/commands';
import { lintGutter } from '@codemirror/lint';
import type { Extension } from '@codemirror/state';

import { customInputHandler } from './inputHandler';

const [_, bracketState] = closeBrackets() as readonly Extension[];

export const baseExtensions = [
lineNumbers(),
highlightActiveLineGutter(),
highlightSpecialChars(),
history(),
foldGutter(),
lintGutter(),
[customInputHandler, bracketState],
dropCursor(),
indentOnInput(),
bracketMatching(),
highlightActiveLine(),
keymap.of([
{ key: 'Enter', run: insertNewlineAndIndent },
{ key: 'Tab', run: acceptCompletion },
{ key: 'Enter', run: acceptCompletion },
{ key: 'Mod-/', run: toggleComment },
indentWithTab,
]),
EditorView.lineWrapping,
];
Loading

0 comments on commit 1db4fa2

Please sign in to comment.