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!`);