diff --git a/package-lock.json b/package-lock.json index 6d2bc54f2b..552414fd01 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", @@ -33242,6 +33267,38 @@ "thenify-all": "^1.0.0" } }, + "n8n-core": { + "version": "0.96.0", + "resolved": "https://registry.npmjs.org/n8n-core/-/n8n-core-0.96.0.tgz", + "integrity": "sha512-FwcLt9tYATP2FJkEkiGlbQCdzzdShTcXfpd6ba0RDeOAwvKK+IIoMvlnaU4ClhJX5og7wvLDouAOWRj9UjjdRQ==", + "requires": { + "axios": "^0.21.1", + "client-oauth2": "^4.2.5", + "cron": "~1.7.2", + "crypto-js": "~4.1.1", + "file-type": "^14.6.2", + "form-data": "^4.0.0", + "lodash.get": "^4.4.2", + "mime-types": "^2.1.27", + "n8n-workflow": "~0.79.0", + "oauth-1.0a": "^2.2.6", + "p-cancelable": "^2.0.0", + "qs": "^6.10.1", + "request": "^2.88.2", + "request-promise-native": "^1.0.7" + } + }, + "n8n-workflow": { + "version": "0.79.0", + "resolved": "https://registry.npmjs.org/n8n-workflow/-/n8n-workflow-0.79.0.tgz", + "integrity": "sha512-ylzM1l7M00gfAnCcQtdRn2DzYZ+7vyWu2gfVe5crpOJnuoLstYbUF+UgShXyrlugdMg6GV86w9mE0iRkgbMb/Q==", + "requires": { + "lodash.get": "^4.4.2", + "lodash.isequal": "^4.5.0", + "riot-tmpl": "^3.0.8", + "xml2js": "^0.4.23" + } + }, "named-placeholders": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/named-placeholders/-/named-placeholders-1.1.2.tgz", diff --git a/packages/editor-ui/src/components/CodeEdit.vue b/packages/editor-ui/src/components/CodeEdit.vue index e0535dab03..5bb6aff73f 100644 --- a/packages/editor-ui/src/components/CodeEdit.vue +++ b/packages/editor-ui/src/components/CodeEdit.vue @@ -17,14 +17,29 @@ import * as monaco from 'monaco-editor/esm/vs/editor/editor.api'; import { genericHelpers } from '@/components/mixins/genericHelpers'; +import { workflowHelpers } from '@/components/mixins/workflowHelpers'; import mixins from 'vue-typed-mixins'; -import { IExecutionResponse } from '@/Interface'; -import { INodeExecutionData } from 'n8n-workflow'; +import { IExecutionResponse, INodeUi } from '@/Interface'; +import { + IBinaryKeyData, + IDataObject, + INodeExecutionData, + IRunExecutionData, + IWorkflowDataProxyAdditionalKeys, + WorkflowDataProxy, +} from 'n8n-workflow'; -export default mixins(genericHelpers).extend({ +import { + PLACEHOLDER_FILLED_AT_EXECUTION_TIME, +} from '@/constants'; + +export default mixins( + genericHelpers, + workflowHelpers, +).extend({ name: 'CodeEdit', - props: ['dialogVisible', 'parameter', 'value'], + props: ['codeAutocomplete', 'dialogVisible', 'parameter', 'type', 'value'], data() { return { monacoInstance: null as monaco.editor.IStandaloneCodeEditor | null, @@ -35,7 +50,9 @@ export default mixins(genericHelpers).extend({ setTimeout(this.loadEditor); }, destroyed() { - this.monacoLibrary!.dispose(); + if (this.monacoLibrary) { + this.monacoLibrary.dispose(); + } }, methods: { closeDialog() { @@ -45,19 +62,50 @@ export default mixins(genericHelpers).extend({ return false; }, + createSimpleRepresentation(inputData: object | null | undefined | boolean | string | number | boolean[] | string[] | number[] | object[]): object | null | undefined | boolean | string | number | boolean[] | string[] | number[] | object[] { + if (inputData === null || inputData === undefined) { + return inputData; + } else if (typeof inputData === 'string') { + return ''; + } else if (typeof inputData === 'boolean') { + return true; + } else if (typeof inputData === 'number') { + return 1; + } else if (Array.isArray(inputData)) { + return inputData.map(value => this.createSimpleRepresentation(value)); + } else if (typeof inputData === 'object') { + const returnData: { [key: string]: object } = {}; + Object.keys(inputData).forEach(key => { + // @ts-ignore + returnData[key] = this.createSimpleRepresentation(inputData[key]); + }); + return returnData; + } + return inputData; + }, + loadEditor() { if (!this.$refs.code) return; this.monacoInstance = monaco.editor.create(this.$refs.code as HTMLElement, { value: this.value, - language: 'javascript', + language: this.type === 'code' ? 'javascript' : 'json', tabSize: 2, + wordBasedSuggestions: false, readOnly: this.isReadOnly, minimap: { enabled: false, }, }); + + this.monacoInstance.onDidChangeModelContent(() => { + const model = this.monacoInstance!.getModel(); + if (model) { + this.$emit('valueChanged', model.getValue()); + } + }); + monaco.editor.defineTheme('n8nCustomTheme', { base: 'vs', inherit: true, @@ -68,45 +116,112 @@ export default mixins(genericHelpers).extend({ }); monaco.editor.setTheme('n8nCustomTheme'); - this.monacoInstance.onDidChangeModelContent(() => { - const model = this.monacoInstance!.getModel(); + if (this.type === 'code') { + // As wordBasedSuggestions: false does not have any effect does it however seem + // to remove all all suggestions from the editor if I do this + monaco.languages.typescript.javascriptDefaults.setCompilerOptions({ + allowNonTsExtensions: true, + }); - if (model) { - this.$emit('valueChanged', model.getValue()); - } - }); - - this.loadAutocompleteData(); + this.loadAutocompleteData(); + } else if (this.type === 'json') { + monaco.languages.json.jsonDefaults.setDiagnosticsOptions({ + validate: true, + }); + } }, loadAutocompleteData(): void { - const executedWorkflow: IExecutionResponse | null = this.$store.getters.getWorkflowExecution; + if (['function', 'functionItem'].includes(this.codeAutocomplete)) { + const executedWorkflow: IExecutionResponse | null = this.$store.getters.getWorkflowExecution; + const workflow = this.getWorkflow(); + const activeNode: INodeUi | null = this.$store.getters.activeNode; + const inputIndex = 0; + const itemIndex = 0; + const inputName = 'main'; + const mode = 'manual'; + const runIndex = 0; - let autocompleteData: INodeExecutionData[] = []; + const autocompleteData: string[] = []; - if (executedWorkflow) { - const lastNodeExecuted = executedWorkflow.data.resultData.lastNodeExecuted; + const executionData = this.$store.getters.getWorkflowExecution as IExecutionResponse | null; - if (lastNodeExecuted) { - const data = executedWorkflow.data.resultData.runData[lastNodeExecuted]; - - // @ts-ignore - autocompleteData = data[0].data!.main[0]; + let runExecutionData: IRunExecutionData; + if (executionData === null) { + runExecutionData = { + resultData: { + runData: {}, + }, + }; + } else { + runExecutionData = executionData.data; } + const parentNode = workflow.getParentNodes(activeNode!.name, inputName, 1); + + const connectionInputData = this.connectionInputData(parentNode, inputName, runIndex, inputIndex); + + const additionalProxyKeys: IWorkflowDataProxyAdditionalKeys = { + $executionId: PLACEHOLDER_FILLED_AT_EXECUTION_TIME, + $resumeWebhookUrl: PLACEHOLDER_FILLED_AT_EXECUTION_TIME, + }; + + const dataProxy = new WorkflowDataProxy(workflow, runExecutionData, runIndex, itemIndex, activeNode!.name, connectionInputData || [], {}, mode, additionalProxyKeys); + const proxy = dataProxy.getDataProxy(); + + const autoCompleteItems = [ + `function $evaluateExpression(expression: string, itemIndex?: number): any {};`, + `function getNodeParameter(parameterName: string, itemIndex: number, fallbackValue?: any): any {};`, + `function getWorkflowStaticData(type: string): object {};`, + `function $item(itemIndex: number, runIndex?: number) {};`, + `function $items(nodeName?: string, outputIndex?: number, runIndex?: number) {};`, + ]; + + const baseKeys = ['$env', '$executionId', '$mode', '$parameter', '$position', '$resumeWebhookUrl', '$workflow']; + const additionalKeys = ['$json', '$binary']; + if (executedWorkflow) { + baseKeys.push(...additionalKeys); + } else { + additionalKeys.forEach(key => { + autoCompleteItems.push(`const ${key} = {}`); + }); + } + + for (const key of baseKeys) { + autoCompleteItems.push(`const ${key} = ${JSON.stringify(this.createSimpleRepresentation(proxy[key]))}`); + } + + // Add the nodes and their simplified data + const nodes: { + [key: string]: INodeExecutionData; + } = {}; + for (const [nodeName, node] of Object.entries(workflow.nodes)) { + // To not load to much data create a simple representation. + nodes[nodeName] = { + json: {} as IDataObject, + parameter: this.createSimpleRepresentation(proxy.$node[nodeName].parameter) as IDataObject, + }; + + try { + nodes[nodeName]!.json = this.createSimpleRepresentation(proxy.$node[nodeName].json) as IDataObject; + nodes[nodeName]!.context = this.createSimpleRepresentation(proxy.$node[nodeName].context) as IDataObject; + nodes[nodeName]!.runIndex = proxy.$node[nodeName].runIndex; + if (Object.keys(proxy.$node[nodeName].binary).length) { + nodes[nodeName]!.binary = this.createSimpleRepresentation(proxy.$node[nodeName].binary) as IBinaryKeyData; + } + } catch(error) {} + } + autoCompleteItems.push(`const $node = ${JSON.stringify(nodes)}`); + + if (this.codeAutocomplete === 'function') { + autoCompleteItems.push(`const items = ${JSON.stringify(this.createSimpleRepresentation(connectionInputData))}`); + } else if (this.codeAutocomplete === 'functionItem') { + autoCompleteItems.push(`const item = $json`); + } + + this.monacoLibrary = monaco.languages.typescript.javascriptDefaults.addExtraLib( + autoCompleteItems.join('\n'), + ); } - - const autoCompleteItems = [ - `/**\n\`\`\`\nconst items = ${JSON.stringify(autocompleteData, null, 2)}\n\`\`\`\n*/`, - `const items = ${JSON.stringify(autocompleteData)}`, - ]; - - if (autocompleteData.length) { - autoCompleteItems.push(`const $json = ${JSON.stringify(autocompleteData[0].json)}`); - } - - this.monacoLibrary = monaco.languages.typescript.javascriptDefaults.addExtraLib( - autoCompleteItems.join('\n'), - ); }, }, }); diff --git a/packages/editor-ui/src/components/ParameterInput.vue b/packages/editor-ui/src/components/ParameterInput.vue index 0a7f441740..cfecb4f250 100644 --- a/packages/editor-ui/src/components/ParameterInput.vue +++ b/packages/editor-ui/src/components/ParameterInput.vue @@ -13,7 +13,7 @@ />
- +
@@ -301,6 +301,9 @@ export default mixins( }, }, computed: { + codeAutocomplete (): string | undefined { + return this.getArgument('codeAutocomplete') as string | undefined; + }, showExpressionAsTextInput(): boolean { const types = ['number', 'boolean', 'dateTime', 'options', 'multiOptions']; @@ -500,7 +503,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) { @@ -511,6 +514,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 e1b23fb014..5f43c129b9 100644 --- a/packages/editor-ui/vue.config.js +++ b/packages/editor-ui/vue.config.js @@ -25,7 +25,7 @@ module.exports = { disableHostCheck: true, }, plugins: [ - new MonacoWebpackPlugin({ languages: ['javascript', 'html', 'typescript'] }), + new MonacoWebpackPlugin({ languages: ['javascript', 'json', 'typescript'] }), ], }, css: { 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..ac0c46f555 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, }, diff --git a/packages/nodes-base/nodes/FunctionItem/FunctionItem.node.ts b/packages/nodes-base/nodes/FunctionItem/FunctionItem.node.ts index b536c8daf7..5e586a0b13 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, }, diff --git a/packages/workflow/src/Interfaces.ts b/packages/workflow/src/Interfaces.ts index 3b3c018316..cc2b34ae6a 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 @@ -850,6 +853,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!`);