Improve autocomplete and a few other changes

This commit is contained in:
Jan Oberhauser 2021-12-23 11:41:46 +01:00
parent 27f696ad27
commit fa760ee26b
9 changed files with 246 additions and 41 deletions

57
package-lock.json generated
View file

@ -33010,6 +33010,31 @@
"moment": ">= 2.9.0" "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": { "mongodb": {
"version": "3.7.3", "version": "3.7.3",
"resolved": "https://registry.npmjs.org/mongodb/-/mongodb-3.7.3.tgz", "resolved": "https://registry.npmjs.org/mongodb/-/mongodb-3.7.3.tgz",
@ -33242,6 +33267,38 @@
"thenify-all": "^1.0.0" "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": { "named-placeholders": {
"version": "1.1.2", "version": "1.1.2",
"resolved": "https://registry.npmjs.org/named-placeholders/-/named-placeholders-1.1.2.tgz", "resolved": "https://registry.npmjs.org/named-placeholders/-/named-placeholders-1.1.2.tgz",

View file

@ -17,14 +17,29 @@
import * as monaco from 'monaco-editor/esm/vs/editor/editor.api'; import * as monaco from 'monaco-editor/esm/vs/editor/editor.api';
import { genericHelpers } from '@/components/mixins/genericHelpers'; import { genericHelpers } from '@/components/mixins/genericHelpers';
import { workflowHelpers } from '@/components/mixins/workflowHelpers';
import mixins from 'vue-typed-mixins'; import mixins from 'vue-typed-mixins';
import { IExecutionResponse } from '@/Interface'; import { IExecutionResponse, INodeUi } from '@/Interface';
import { INodeExecutionData } from 'n8n-workflow'; 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', name: 'CodeEdit',
props: ['dialogVisible', 'parameter', 'value'], props: ['codeAutocomplete', 'dialogVisible', 'parameter', 'type', 'value'],
data() { data() {
return { return {
monacoInstance: null as monaco.editor.IStandaloneCodeEditor | null, monacoInstance: null as monaco.editor.IStandaloneCodeEditor | null,
@ -35,7 +50,9 @@ export default mixins(genericHelpers).extend({
setTimeout(this.loadEditor); setTimeout(this.loadEditor);
}, },
destroyed() { destroyed() {
this.monacoLibrary!.dispose(); if (this.monacoLibrary) {
this.monacoLibrary.dispose();
}
}, },
methods: { methods: {
closeDialog() { closeDialog() {
@ -45,19 +62,50 @@ export default mixins(genericHelpers).extend({
return false; 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() { loadEditor() {
if (!this.$refs.code) return; if (!this.$refs.code) return;
this.monacoInstance = monaco.editor.create(this.$refs.code as HTMLElement, { this.monacoInstance = monaco.editor.create(this.$refs.code as HTMLElement, {
value: this.value, value: this.value,
language: 'javascript', language: this.type === 'code' ? 'javascript' : 'json',
tabSize: 2, tabSize: 2,
wordBasedSuggestions: false,
readOnly: this.isReadOnly, readOnly: this.isReadOnly,
minimap: { minimap: {
enabled: false, enabled: false,
}, },
}); });
this.monacoInstance.onDidChangeModelContent(() => {
const model = this.monacoInstance!.getModel();
if (model) {
this.$emit('valueChanged', model.getValue());
}
});
monaco.editor.defineTheme('n8nCustomTheme', { monaco.editor.defineTheme('n8nCustomTheme', {
base: 'vs', base: 'vs',
inherit: true, inherit: true,
@ -68,45 +116,112 @@ export default mixins(genericHelpers).extend({
}); });
monaco.editor.setTheme('n8nCustomTheme'); monaco.editor.setTheme('n8nCustomTheme');
this.monacoInstance.onDidChangeModelContent(() => { if (this.type === 'code') {
const model = this.monacoInstance!.getModel(); // 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.loadAutocompleteData();
this.$emit('valueChanged', model.getValue()); } else if (this.type === 'json') {
} monaco.languages.json.jsonDefaults.setDiagnosticsOptions({
}); validate: true,
});
this.loadAutocompleteData(); }
}, },
loadAutocompleteData(): void { 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 executionData = this.$store.getters.getWorkflowExecution as IExecutionResponse | null;
const lastNodeExecuted = executedWorkflow.data.resultData.lastNodeExecuted;
if (lastNodeExecuted) { let runExecutionData: IRunExecutionData;
const data = executedWorkflow.data.resultData.runData[lastNodeExecuted]; if (executionData === null) {
runExecutionData = {
// @ts-ignore resultData: {
autocompleteData = data[0].data!.main[0]; 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'),
);
}, },
}, },
}); });

View file

@ -13,7 +13,7 @@
/> />
<div v-else-if="['json', 'string'].includes(parameter.type) || remoteParameterOptionsLoadingIssues !== null"> <div v-else-if="['json', 'string'].includes(parameter.type) || remoteParameterOptionsLoadingIssues !== null">
<code-edit v-if="codeEditDialogVisible" :value="value" :parameter="parameter" @closeDialog="closeCodeEditDialog" @valueChanged="expressionUpdated"></code-edit> <code-edit v-if="codeEditDialogVisible" :value="value" :parameter="parameter" :type="editorType" :codeAutocomplete="codeAutocomplete" @closeDialog="closeCodeEditDialog" @valueChanged="expressionUpdated"></code-edit>
<text-edit :dialogVisible="textEditDialogVisible" :value="value" :parameter="parameter" @closeDialog="closeTextEditDialog" @valueChanged="expressionUpdated"></text-edit> <text-edit :dialogVisible="textEditDialogVisible" :value="value" :parameter="parameter" @closeDialog="closeTextEditDialog" @valueChanged="expressionUpdated"></text-edit>
<div v-if="isEditor === true" class="code-edit clickable" @click="displayEditDialog()"> <div v-if="isEditor === true" class="code-edit clickable" @click="displayEditDialog()">
@ -301,6 +301,9 @@ export default mixins(
}, },
}, },
computed: { computed: {
codeAutocomplete (): string | undefined {
return this.getArgument('codeAutocomplete') as string | undefined;
},
showExpressionAsTextInput(): boolean { showExpressionAsTextInput(): boolean {
const types = ['number', 'boolean', 'dateTime', 'options', 'multiOptions']; const types = ['number', 'boolean', 'dateTime', 'options', 'multiOptions'];
@ -500,7 +503,7 @@ export default mixins(
return this.parameter.default === this.value; return this.parameter.default === this.value;
}, },
isEditor (): boolean { isEditor (): boolean {
return this.getArgument('editor') === 'code'; return ['code', 'json'].includes(this.editorType);
}, },
isValueExpression () { isValueExpression () {
if (this.parameter.noDataExpression === true) { if (this.parameter.noDataExpression === true) {
@ -511,6 +514,9 @@ export default mixins(
} }
return false; return false;
}, },
editorType (): string {
return this.getArgument('editor') as string;
},
parameterOptions (): INodePropertyOptions[] { parameterOptions (): INodePropertyOptions[] {
if (this.remoteMethod === undefined) { if (this.remoteMethod === undefined) {
// Options are already given // Options are already given

View file

@ -25,7 +25,7 @@ module.exports = {
disableHostCheck: true, disableHostCheck: true,
}, },
plugins: [ plugins: [
new MonacoWebpackPlugin({ languages: ['javascript', 'html', 'typescript'] }), new MonacoWebpackPlugin({ languages: ['javascript', 'json', 'typescript'] }),
], ],
}, },
css: { css: {

View file

@ -107,7 +107,7 @@ export class ExecuteWorkflow implements INodeType {
type: 'string', type: 'string',
typeOptions: { typeOptions: {
alwaysOpenEditWindow: true, alwaysOpenEditWindow: true,
editor: 'code', editor: 'json',
rows: 10, rows: 10,
}, },
displayOptions: { displayOptions: {

View file

@ -28,6 +28,7 @@ export class Function implements INodeType {
name: 'functionCode', name: 'functionCode',
typeOptions: { typeOptions: {
alwaysOpenEditWindow: true, alwaysOpenEditWindow: true,
codeAutocomplete: 'function',
editor: 'code', editor: 'code',
rows: 10, rows: 10,
}, },

View file

@ -30,6 +30,7 @@ export class FunctionItem implements INodeType {
name: 'functionCode', name: 'functionCode',
typeOptions: { typeOptions: {
alwaysOpenEditWindow: true, alwaysOpenEditWindow: true,
codeAutocomplete: 'functionItem',
editor: 'code', editor: 'code',
rows: 10, rows: 10,
}, },

View file

@ -631,10 +631,13 @@ export type NodePropertyTypes =
| 'options' | 'options'
| 'string'; | 'string';
export type EditorTypes = 'code'; export type CodeAutocompleteTypes = 'function' | 'functionItem';
export type EditorTypes = 'code' | 'json';
export interface INodePropertyTypeOptions { export interface INodePropertyTypeOptions {
alwaysOpenEditWindow?: boolean; // Supported by: string alwaysOpenEditWindow?: boolean; // Supported by: string
codeAutocomplete?: CodeAutocompleteTypes; // Supported by: string
editor?: EditorTypes; // Supported by: string editor?: EditorTypes; // Supported by: string
loadOptionsDependsOn?: string[]; // Supported by: options loadOptionsDependsOn?: string[]; // Supported by: options
loadOptionsMethod?: string; // Supported by: options loadOptionsMethod?: string; // Supported by: options
@ -850,6 +853,7 @@ export interface IWebhookDescription {
} }
export interface IWorkflowDataProxyData { export interface IWorkflowDataProxyData {
[key: string]: any;
$binary: any; $binary: any;
$data: any; $data: any;
$env: any; $env: any;

View file

@ -92,6 +92,12 @@ export class WorkflowDataProxy {
return Reflect.ownKeys(target); return Reflect.ownKeys(target);
}, },
getOwnPropertyDescriptor(k) {
return {
enumerable: true,
configurable: true,
};
},
get(target, name, receiver) { get(target, name, receiver) {
// eslint-disable-next-line no-param-reassign // eslint-disable-next-line no-param-reassign
name = name.toString(); name = name.toString();
@ -142,6 +148,12 @@ export class WorkflowDataProxy {
ownKeys(target) { ownKeys(target) {
return Reflect.ownKeys(target); return Reflect.ownKeys(target);
}, },
getOwnPropertyDescriptor(k) {
return {
enumerable: true,
configurable: true,
};
},
get(target, name, receiver) { get(target, name, receiver) {
name = name.toString(); name = name.toString();
@ -385,6 +397,15 @@ export class WorkflowDataProxy {
return new Proxy( return new Proxy(
{}, {},
{ {
ownKeys(target) {
return allowedValues;
},
getOwnPropertyDescriptor(k) {
return {
enumerable: true,
configurable: true,
};
},
get(target, name, receiver) { get(target, name, receiver) {
if (!allowedValues.includes(name.toString())) { if (!allowedValues.includes(name.toString())) {
throw new Error(`The key "${name.toString()}" is not supported!`); throw new Error(`The key "${name.toString()}" is not supported!`);