diff --git a/package-lock.json b/package-lock.json index ab8cb4aab3..8cb074c171 100644 --- a/package-lock.json +++ b/package-lock.json @@ -33010,6 +33010,31 @@ "moment": ">= 2.9.0" } }, + "monaco-editor": { + "version": "0.29.1", + "resolved": "https://registry.npmjs.org/monaco-editor/-/monaco-editor-0.29.1.tgz", + "integrity": "sha512-rguaEG/zrPQSaKzQB7IfX/PpNa0qxF1FY8ZXRkN4WIl8qZdTQRSRJCtRto7IMcSgrU6H53RXI+fTcywOBC4aVw==" + }, + "monaco-editor-webpack-plugin": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/monaco-editor-webpack-plugin/-/monaco-editor-webpack-plugin-5.0.0.tgz", + "integrity": "sha512-KrUUTmMO3lDCNK4honZ6rrrKjOI7FFLeyCktPetIo5HlRqr5dfE6ewaA9qNLH96XY7CekE3Z+v/+I6ufAs3ObA==", + "requires": { + "loader-utils": "^2.0.0" + }, + "dependencies": { + "loader-utils": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/loader-utils/-/loader-utils-2.0.2.tgz", + "integrity": "sha512-TM57VeHptv569d/GKh6TAYdzKblwDNiumOdkFnejjD0XwTH87K90w3O7AiJRqdQoXygvi1VQTJTLGhJl7WqA7A==", + "requires": { + "big.js": "^5.2.2", + "emojis-list": "^3.0.0", + "json5": "^2.1.2" + } + } + } + }, "mongodb": { "version": "3.7.3", "resolved": "https://registry.npmjs.org/mongodb/-/mongodb-3.7.3.tgz", diff --git a/packages/editor-ui/package.json b/packages/editor-ui/package.json index 6d0710be80..d0f57cf0dd 100644 --- a/packages/editor-ui/package.json +++ b/packages/editor-ui/package.json @@ -27,6 +27,7 @@ "dependencies": { "@fontsource/open-sans": "^4.5.0", "n8n-design-system": "~0.9.0", + "monaco-editor": "^0.29.1", "timeago.js": "^4.0.2", "v-click-outside": "^3.1.2", "vue-fragment": "^1.5.2", @@ -75,6 +76,7 @@ "lodash.get": "^4.4.2", "lodash.set": "^4.3.2", "n8n-workflow": "~0.80.0", + "monaco-editor-webpack-plugin": "^5.0.0", "normalize-wheel": "^1.0.1", "prismjs": "^1.17.1", "quill": "^2.0.0-dev.3", diff --git a/packages/editor-ui/src/components/CodeEdit.vue b/packages/editor-ui/src/components/CodeEdit.vue index 7d29bc0a03..ad2eb3417d 100644 --- a/packages/editor-ui/src/components/CodeEdit.vue +++ b/packages/editor-ui/src/components/CodeEdit.vue @@ -1,59 +1,247 @@ - diff --git a/packages/editor-ui/src/components/ParameterInput.vue b/packages/editor-ui/src/components/ParameterInput.vue index 4204326830..7f68f53eca 100644 --- a/packages/editor-ui/src/components/ParameterInput.vue +++ b/packages/editor-ui/src/components/ParameterInput.vue @@ -13,7 +13,7 @@ />
- +
@@ -300,6 +300,9 @@ export default mixins( }, }, computed: { + codeAutocomplete (): string | undefined { + return this.getArgument('codeAutocomplete') as string | undefined; + }, showExpressionAsTextInput(): boolean { const types = ['number', 'boolean', 'dateTime', 'options', 'multiOptions']; @@ -499,7 +502,7 @@ export default mixins( return this.parameter.default === this.value; }, isEditor (): boolean { - return this.getArgument('editor') === 'code'; + return ['code', 'json'].includes(this.editorType); }, isValueExpression () { if (this.parameter.noDataExpression === true) { @@ -510,6 +513,9 @@ export default mixins( } return false; }, + editorType (): string { + return this.getArgument('editor') as string; + }, parameterOptions (): INodePropertyOptions[] { if (this.remoteMethod === undefined) { // Options are already given diff --git a/packages/editor-ui/vue.config.js b/packages/editor-ui/vue.config.js index 6eb356306d..5f43c129b9 100644 --- a/packages/editor-ui/vue.config.js +++ b/packages/editor-ui/vue.config.js @@ -1,3 +1,5 @@ +const MonacoWebpackPlugin = require('monaco-editor-webpack-plugin'); + module.exports = { chainWebpack: config => { config.resolve.symlinks(false); @@ -22,6 +24,9 @@ module.exports = { devServer: { disableHostCheck: true, }, + plugins: [ + new MonacoWebpackPlugin({ languages: ['javascript', 'json', 'typescript'] }), + ], }, css: { loaderOptions: { @@ -32,5 +37,5 @@ module.exports = { }, }, }, - publicPath: process.env.VUE_APP_PUBLIC_PATH ? process.env.VUE_APP_PUBLIC_PATH : '/', + publicPath: process.env.VUE_APP_PUBLIC_PATH && process.env.VUE_APP_PUBLIC_PATH !== '/%BASE_PATH%/' ? process.env.VUE_APP_PUBLIC_PATH : '/', }; diff --git a/packages/nodes-base/nodes/ExecuteWorkflow/ExecuteWorkflow.node.ts b/packages/nodes-base/nodes/ExecuteWorkflow/ExecuteWorkflow.node.ts index 02271ed8a7..07e5d222fe 100644 --- a/packages/nodes-base/nodes/ExecuteWorkflow/ExecuteWorkflow.node.ts +++ b/packages/nodes-base/nodes/ExecuteWorkflow/ExecuteWorkflow.node.ts @@ -107,7 +107,7 @@ export class ExecuteWorkflow implements INodeType { type: 'string', typeOptions: { alwaysOpenEditWindow: true, - editor: 'code', + editor: 'json', rows: 10, }, displayOptions: { diff --git a/packages/nodes-base/nodes/Function/Function.node.ts b/packages/nodes-base/nodes/Function/Function.node.ts index f1a1b6aac1..649ec70ee9 100644 --- a/packages/nodes-base/nodes/Function/Function.node.ts +++ b/packages/nodes-base/nodes/Function/Function.node.ts @@ -28,6 +28,7 @@ export class Function implements INodeType { name: 'functionCode', typeOptions: { alwaysOpenEditWindow: true, + codeAutocomplete: 'function', editor: 'code', rows: 10, }, @@ -101,7 +102,7 @@ return items;`, try { // Execute the function code - items = (await vm.run(`module.exports = async function() {${functionCode}}()`, __dirname)); + items = (await vm.run(`module.exports = async function() {${functionCode}\n}()`, __dirname)); // Do very basic validation of the data if (items === undefined) { throw new NodeOperationError(this.getNode(), 'No data got returned. Always return an Array of items!'); @@ -126,6 +127,18 @@ return items;`, if (this.continueOnFail()) { items=[{json:{ error: error.message }}]; } else { + // Try to find the line number which contains the error and attach to error message + const stackLines = error.stack.split('\n'); + if (stackLines.length > 0) { + const lineParts = stackLines[1].split(':'); + if (lineParts.length > 2) { + const lineNumber = lineParts.splice(-2, 1); + if (!isNaN(lineNumber)) { + error.message = `${error.message} [Line ${lineNumber}]`; + } + } + } + return Promise.reject(error); } } diff --git a/packages/nodes-base/nodes/FunctionItem/FunctionItem.node.ts b/packages/nodes-base/nodes/FunctionItem/FunctionItem.node.ts index b536c8daf7..2ae9fba283 100644 --- a/packages/nodes-base/nodes/FunctionItem/FunctionItem.node.ts +++ b/packages/nodes-base/nodes/FunctionItem/FunctionItem.node.ts @@ -30,6 +30,7 @@ export class FunctionItem implements INodeType { name: 'functionCode', typeOptions: { alwaysOpenEditWindow: true, + codeAutocomplete: 'functionItem', editor: 'code', rows: 10, }, @@ -113,12 +114,27 @@ return item;`, let jsonData: IDataObject; try { // Execute the function code - jsonData = await vm.run(`module.exports = async function() {${functionCode}}()`, __dirname); + jsonData = await vm.run(`module.exports = async function() {${functionCode}\n}()`, __dirname); } catch (error) { if (this.continueOnFail()) { returnData.push({json:{ error: error.message }}); continue; } else { + // Try to find the line number which contains the error and attach to error message + const stackLines = error.stack.split('\n'); + if (stackLines.length > 0) { + const lineParts = stackLines[1].split(':'); + if (lineParts.length > 2) { + const lineNumber = lineParts.splice(-2, 1); + if (!isNaN(lineNumber)) { + error.message = `${error.message} [Line ${lineNumber} | Item Index: ${itemIndex}]`; + return Promise.reject(error); + } + } + } + + error.message = `${error.message} [Item Index: ${itemIndex}]`; + return Promise.reject(error); } } diff --git a/packages/workflow/src/Interfaces.ts b/packages/workflow/src/Interfaces.ts index cb798ed923..0b0ea184f6 100644 --- a/packages/workflow/src/Interfaces.ts +++ b/packages/workflow/src/Interfaces.ts @@ -631,10 +631,13 @@ export type NodePropertyTypes = | 'options' | 'string'; -export type EditorTypes = 'code'; +export type CodeAutocompleteTypes = 'function' | 'functionItem'; + +export type EditorTypes = 'code' | 'json'; export interface INodePropertyTypeOptions { alwaysOpenEditWindow?: boolean; // Supported by: string + codeAutocomplete?: CodeAutocompleteTypes; // Supported by: string editor?: EditorTypes; // Supported by: string loadOptionsDependsOn?: string[]; // Supported by: options loadOptionsMethod?: string; // Supported by: options @@ -849,6 +852,7 @@ export interface IWebhookDescription { } export interface IWorkflowDataProxyData { + [key: string]: any; $binary: any; $data: any; $env: any; diff --git a/packages/workflow/src/WorkflowDataProxy.ts b/packages/workflow/src/WorkflowDataProxy.ts index 735e04a178..e168c6b48c 100644 --- a/packages/workflow/src/WorkflowDataProxy.ts +++ b/packages/workflow/src/WorkflowDataProxy.ts @@ -92,6 +92,12 @@ export class WorkflowDataProxy { return Reflect.ownKeys(target); }, + getOwnPropertyDescriptor(k) { + return { + enumerable: true, + configurable: true, + }; + }, get(target, name, receiver) { // eslint-disable-next-line no-param-reassign name = name.toString(); @@ -142,6 +148,12 @@ export class WorkflowDataProxy { ownKeys(target) { return Reflect.ownKeys(target); }, + getOwnPropertyDescriptor(k) { + return { + enumerable: true, + configurable: true, + }; + }, get(target, name, receiver) { name = name.toString(); @@ -385,6 +397,15 @@ export class WorkflowDataProxy { return new Proxy( {}, { + ownKeys(target) { + return allowedValues; + }, + getOwnPropertyDescriptor(k) { + return { + enumerable: true, + configurable: true, + }; + }, get(target, name, receiver) { if (!allowedValues.includes(name.toString())) { throw new Error(`The key "${name.toString()}" is not supported!`);