mirror of
https://github.com/n8n-io/n8n.git
synced 2025-03-05 20:50:17 -08:00
🔀 Merge branch 'arpadgabor-feat/monaco'
This commit is contained in:
commit
aecb30dfa8
25
package-lock.json
generated
25
package-lock.json
generated
|
@ -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",
|
||||||
|
|
|
@ -27,6 +27,7 @@
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@fontsource/open-sans": "^4.5.0",
|
"@fontsource/open-sans": "^4.5.0",
|
||||||
"n8n-design-system": "~0.9.0",
|
"n8n-design-system": "~0.9.0",
|
||||||
|
"monaco-editor": "^0.29.1",
|
||||||
"timeago.js": "^4.0.2",
|
"timeago.js": "^4.0.2",
|
||||||
"v-click-outside": "^3.1.2",
|
"v-click-outside": "^3.1.2",
|
||||||
"vue-fragment": "^1.5.2",
|
"vue-fragment": "^1.5.2",
|
||||||
|
@ -75,6 +76,7 @@
|
||||||
"lodash.get": "^4.4.2",
|
"lodash.get": "^4.4.2",
|
||||||
"lodash.set": "^4.3.2",
|
"lodash.set": "^4.3.2",
|
||||||
"n8n-workflow": "~0.80.0",
|
"n8n-workflow": "~0.80.0",
|
||||||
|
"monaco-editor-webpack-plugin": "^5.0.0",
|
||||||
"normalize-wheel": "^1.0.1",
|
"normalize-wheel": "^1.0.1",
|
||||||
"prismjs": "^1.17.1",
|
"prismjs": "^1.17.1",
|
||||||
"quill": "^2.0.0-dev.3",
|
"quill": "^2.0.0-dev.3",
|
||||||
|
|
|
@ -1,59 +1,247 @@
|
||||||
<template>
|
<template>
|
||||||
<div v-if="dialogVisible">
|
<el-dialog
|
||||||
<el-dialog :visible="dialogVisible" append-to-body :close-on-click-modal="false" width="80%" :title="`${$locale.baseText('codeEdit.edit')} ${$locale.nodeText().topParameterDisplayName(parameter)}`" :before-close="closeDialog">
|
visible
|
||||||
<div class="ignore-key-press">
|
append-to-body
|
||||||
<n8n-input-label :label="$locale.nodeText().topParameterDisplayName(parameter)">
|
:close-on-click-modal="false"
|
||||||
<div :class="$style.editor" @keydown.stop>
|
width="80%"
|
||||||
<prism-editor :lineNumbers="true" :code="value" :readonly="isReadOnly" @change="valueChanged" language="js"></prism-editor>
|
:title="`${$locale.baseText('codeEdit.edit')} ${$locale.nodeText().topParameterDisplayName(parameter)}`"
|
||||||
</div>
|
:before-close="closeDialog"
|
||||||
</n8n-input-label>
|
>
|
||||||
</div>
|
<div class="text-editor-wrapper ignore-key-press">
|
||||||
</el-dialog>
|
<div ref="code" class="text-editor" @keydown.stop></div>
|
||||||
</div>
|
</div>
|
||||||
|
</el-dialog>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
// @ts-ignore
|
import * as monaco from 'monaco-editor/esm/vs/editor/editor.api';
|
||||||
import PrismEditor from 'vue-prism-editor';
|
|
||||||
|
|
||||||
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, INodeUi } from '@/Interface';
|
||||||
|
import {
|
||||||
|
IBinaryKeyData,
|
||||||
|
IDataObject,
|
||||||
|
INodeExecutionData,
|
||||||
|
IRunExecutionData,
|
||||||
|
IWorkflowDataProxyAdditionalKeys,
|
||||||
|
WorkflowDataProxy,
|
||||||
|
} from 'n8n-workflow';
|
||||||
|
|
||||||
|
import {
|
||||||
|
PLACEHOLDER_FILLED_AT_EXECUTION_TIME,
|
||||||
|
} from '@/constants';
|
||||||
|
|
||||||
export default mixins(
|
export default mixins(
|
||||||
genericHelpers,
|
genericHelpers,
|
||||||
)
|
workflowHelpers,
|
||||||
.extend({
|
).extend({
|
||||||
name: 'CodeEdit',
|
name: 'CodeEdit',
|
||||||
props: [
|
props: ['codeAutocomplete', 'parameter', 'type', 'value'],
|
||||||
'dialogVisible',
|
data() {
|
||||||
'parameter',
|
return {
|
||||||
'value',
|
monacoInstance: null as monaco.editor.IStandaloneCodeEditor | null,
|
||||||
],
|
monacoLibrary: null as monaco.IDisposable | null,
|
||||||
components: {
|
};
|
||||||
PrismEditor,
|
},
|
||||||
|
mounted() {
|
||||||
|
setTimeout(this.loadEditor);
|
||||||
|
},
|
||||||
|
destroyed() {
|
||||||
|
if (this.monacoLibrary) {
|
||||||
|
this.monacoLibrary.dispose();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
closeDialog() {
|
||||||
|
// Handle the close externally as the visible parameter is an external prop
|
||||||
|
// and is so not allowed to be changed here.
|
||||||
|
this.$emit('closeDialog');
|
||||||
|
return false;
|
||||||
},
|
},
|
||||||
data () {
|
|
||||||
return {
|
|
||||||
};
|
|
||||||
},
|
|
||||||
methods: {
|
|
||||||
valueChanged (value: string) {
|
|
||||||
this.$emit('valueChanged', value);
|
|
||||||
},
|
|
||||||
|
|
||||||
closeDialog () {
|
createSimpleRepresentation(inputData: object | null | undefined | boolean | string | number | boolean[] | string[] | number[] | object[]): object | null | undefined | boolean | string | number | boolean[] | string[] | number[] | object[] {
|
||||||
// Handle the close externally as the visible parameter is an external prop
|
if (inputData === null || inputData === undefined) {
|
||||||
// and is so not allowed to be changed here.
|
return inputData;
|
||||||
this.$emit('closeDialog');
|
} else if (typeof inputData === 'string') {
|
||||||
return false;
|
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: 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,
|
||||||
|
rules: [],
|
||||||
|
colors: {
|
||||||
|
'editor.background': '#f5f2f0',
|
||||||
|
},
|
||||||
|
});
|
||||||
|
monaco.editor.setTheme('n8nCustomTheme');
|
||||||
|
|
||||||
|
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,
|
||||||
|
});
|
||||||
|
|
||||||
|
this.loadAutocompleteData();
|
||||||
|
} else if (this.type === 'json') {
|
||||||
|
monaco.languages.json.jsonDefaults.setDiagnosticsOptions({
|
||||||
|
validate: true,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
loadAutocompleteData(): void {
|
||||||
|
if (['function', 'functionItem'].includes(this.codeAutocomplete)) {
|
||||||
|
const itemIndex = 0;
|
||||||
|
const inputName = 'main';
|
||||||
|
const mode = 'manual';
|
||||||
|
let runIndex = 0;
|
||||||
|
|
||||||
|
const executedWorkflow: IExecutionResponse | null = this.$store.getters.getWorkflowExecution;
|
||||||
|
const workflow = this.getWorkflow();
|
||||||
|
const activeNode: INodeUi | null = this.$store.getters.activeNode;
|
||||||
|
const parentNode = workflow.getParentNodes(activeNode!.name, inputName, 1);
|
||||||
|
const inputIndex = workflow.getNodeConnectionOutputIndex(activeNode!.name, parentNode[0]) || 0;
|
||||||
|
|
||||||
|
const autocompleteData: string[] = [];
|
||||||
|
|
||||||
|
const executionData = this.$store.getters.getWorkflowExecution as IExecutionResponse | null;
|
||||||
|
|
||||||
|
let runExecutionData: IRunExecutionData;
|
||||||
|
if (executionData === null) {
|
||||||
|
runExecutionData = {
|
||||||
|
resultData: {
|
||||||
|
runData: {},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
} else {
|
||||||
|
runExecutionData = executionData.data;
|
||||||
|
if (runExecutionData.resultData.runData[activeNode!.name]) {
|
||||||
|
runIndex = runExecutionData.resultData.runData[activeNode!.name].length - 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 && connectionInputData && connectionInputData.length) {
|
||||||
|
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') {
|
||||||
|
if (connectionInputData) {
|
||||||
|
autoCompleteItems.push(`const items = ${JSON.stringify(this.createSimpleRepresentation(connectionInputData))}`);
|
||||||
|
} else {
|
||||||
|
autoCompleteItems.push(`const items: {json: {[key: string]: any}}[] = []`);
|
||||||
|
}
|
||||||
|
} else if (this.codeAutocomplete === 'functionItem') {
|
||||||
|
if (connectionInputData) {
|
||||||
|
autoCompleteItems.push(`const item = $json`);
|
||||||
|
} else {
|
||||||
|
autoCompleteItems.push(`const item: {[key: string]: any} = {}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
this.monacoLibrary = monaco.languages.typescript.javascriptDefaults.addExtraLib(
|
||||||
|
autoCompleteItems.join('\n'),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style lang="scss" module>
|
<style scoped>
|
||||||
.editor {
|
|
||||||
font-size: var(--font-size-s);
|
.text-editor {
|
||||||
|
min-height: 30rem;
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|
|
@ -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 :dialogVisible="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()">
|
||||||
|
@ -300,6 +300,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'];
|
||||||
|
|
||||||
|
@ -499,7 +502,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) {
|
||||||
|
@ -510,6 +513,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
|
||||||
|
|
|
@ -1,3 +1,5 @@
|
||||||
|
const MonacoWebpackPlugin = require('monaco-editor-webpack-plugin');
|
||||||
|
|
||||||
module.exports = {
|
module.exports = {
|
||||||
chainWebpack: config => {
|
chainWebpack: config => {
|
||||||
config.resolve.symlinks(false);
|
config.resolve.symlinks(false);
|
||||||
|
@ -22,6 +24,9 @@ module.exports = {
|
||||||
devServer: {
|
devServer: {
|
||||||
disableHostCheck: true,
|
disableHostCheck: true,
|
||||||
},
|
},
|
||||||
|
plugins: [
|
||||||
|
new MonacoWebpackPlugin({ languages: ['javascript', 'json', 'typescript'] }),
|
||||||
|
],
|
||||||
},
|
},
|
||||||
css: {
|
css: {
|
||||||
loaderOptions: {
|
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 : '/',
|
||||||
};
|
};
|
||||||
|
|
|
@ -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: {
|
||||||
|
|
|
@ -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,
|
||||||
},
|
},
|
||||||
|
@ -101,7 +102,7 @@ return items;`,
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// Execute the function code
|
// 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
|
// Do very basic validation of the data
|
||||||
if (items === undefined) {
|
if (items === undefined) {
|
||||||
throw new NodeOperationError(this.getNode(), 'No data got returned. Always return an Array of items!');
|
throw new NodeOperationError(this.getNode(), 'No data got returned. Always return an Array of items!');
|
||||||
|
@ -126,6 +127,18 @@ return items;`,
|
||||||
if (this.continueOnFail()) {
|
if (this.continueOnFail()) {
|
||||||
items=[{json:{ error: error.message }}];
|
items=[{json:{ error: error.message }}];
|
||||||
} else {
|
} 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);
|
return Promise.reject(error);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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,
|
||||||
},
|
},
|
||||||
|
@ -113,12 +114,27 @@ return item;`,
|
||||||
let jsonData: IDataObject;
|
let jsonData: IDataObject;
|
||||||
try {
|
try {
|
||||||
// Execute the function code
|
// 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) {
|
} catch (error) {
|
||||||
if (this.continueOnFail()) {
|
if (this.continueOnFail()) {
|
||||||
returnData.push({json:{ error: error.message }});
|
returnData.push({json:{ error: error.message }});
|
||||||
continue;
|
continue;
|
||||||
} else {
|
} 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);
|
return Promise.reject(error);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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
|
||||||
|
@ -849,6 +852,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;
|
||||||
|
|
|
@ -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!`);
|
||||||
|
|
Loading…
Reference in a new issue