mirror of
https://github.com/n8n-io/n8n.git
synced 2024-11-09 22:24:05 -08:00
refactor(editor): Replace monaco-editor/prismjs with CodeMirror (#5983)
Co-authored-by: Jan Oberhauser <jan.oberhauser@gmail.com> Co-authored-by: Milorad FIlipović <milorad@n8n.io> Co-authored-by: Alex Grozav <alex@grozav.com>
This commit is contained in:
parent
88724bb056
commit
ca4e0df90b
|
@ -18,7 +18,7 @@ export class NDV extends BasePage {
|
|||
outputDisplayMode: () => this.getters.outputPanel().findChildByTestId('ndv-run-data-display-mode').first(),
|
||||
pinDataButton: () => cy.getByTestId('ndv-pin-data'),
|
||||
editPinnedDataButton: () => cy.getByTestId('ndv-edit-pinned-data'),
|
||||
pinnedDataEditor: () => this.getters.outputPanel().find('.monaco-editor[role=code]'),
|
||||
pinnedDataEditor: () => this.getters.outputPanel().find('.cm-editor .cm-scroller'),
|
||||
runDataPaneHeader: () => cy.getByTestId('run-data-pane-header'),
|
||||
savePinnedDataButton: () => this.getters.runDataPaneHeader().find('button').filter(':visible').contains('Save'),
|
||||
outputTableRows: () => this.getters.outputDataContainer().find('table tr'),
|
||||
|
@ -77,8 +77,7 @@ export class NDV extends BasePage {
|
|||
this.getters.editPinnedDataButton().click();
|
||||
|
||||
this.getters.pinnedDataEditor().click();
|
||||
this.getters.pinnedDataEditor().type(`{selectall}{backspace}`);
|
||||
this.getters.pinnedDataEditor().type(JSON.stringify(data).replace(new RegExp('{', 'g'), '{{}'));
|
||||
this.getters.pinnedDataEditor().type(`{selectall}{backspace}${JSON.stringify(data).replace(new RegExp('{', 'g'), '{{}')}`);
|
||||
|
||||
this.actions.savePinnedData();
|
||||
},
|
||||
|
|
|
@ -276,6 +276,7 @@
|
|||
--color-json-highlight: #dcdfea;
|
||||
|
||||
--color-code-background: #222020;
|
||||
--color-code-background-readonly: #323230;
|
||||
--color-code-foreground: #f8f8f2;
|
||||
--color-code-caret: #f8f8f0;
|
||||
--color-code-selection: #312b25;
|
||||
|
|
|
@ -377,6 +377,7 @@
|
|||
);
|
||||
|
||||
--color-code-background: #ffffff;
|
||||
--color-code-background-readonly: #f5f2f0;
|
||||
--color-code-foreground: #4d4d4c;
|
||||
--color-code-caret: #aeafad;
|
||||
--color-code-selection: #d6d6d6;
|
||||
|
|
|
@ -29,6 +29,7 @@
|
|||
"@codemirror/autocomplete": "^6.4.0",
|
||||
"@codemirror/commands": "^6.1.0",
|
||||
"@codemirror/lang-javascript": "^6.1.2",
|
||||
"@codemirror/lang-json": "^6.0.1",
|
||||
"@codemirror/language": "^6.2.1",
|
||||
"@codemirror/lint": "^6.0.0",
|
||||
"@codemirror/state": "^6.1.4",
|
||||
|
@ -58,13 +59,11 @@
|
|||
"jsonpath": "^1.1.1",
|
||||
"lodash-es": "^4.17.21",
|
||||
"luxon": "^3.3.0",
|
||||
"monaco-editor": "^0.33.0",
|
||||
"n8n-design-system": "workspace:*",
|
||||
"n8n-workflow": "workspace:*",
|
||||
"normalize-wheel": "^1.0.1",
|
||||
"pinia": "^2.0.22",
|
||||
"prettier": "^2.8.3",
|
||||
"prismjs": "^1.17.1",
|
||||
"stream-browserify": "^3.0.0",
|
||||
"timeago.js": "^4.0.2",
|
||||
"uuid": "^8.3.2",
|
||||
|
@ -75,7 +74,6 @@
|
|||
"vue-i18n": "^8.26.7",
|
||||
"vue-infinite-loading": "^2.4.5",
|
||||
"vue-json-pretty": "1.9.3",
|
||||
"vue-prism-editor": "^0.3.0",
|
||||
"vue-router": "^3.6.5",
|
||||
"vue-template-compiler": "^2.7.14",
|
||||
"vue-typed-mixins": "^0.2.0",
|
||||
|
@ -113,7 +111,6 @@
|
|||
"sass-loader": "^10.1.1",
|
||||
"string-template-parser": "^1.2.6",
|
||||
"vite": "4.0.4",
|
||||
"vite-plugin-monaco-editor": "^1.0.10",
|
||||
"vitest": "^0.28.5",
|
||||
"vue-tsc": "^1.0.24"
|
||||
}
|
||||
|
|
|
@ -1,281 +0,0 @@
|
|||
<template>
|
||||
<el-dialog
|
||||
visible
|
||||
append-to-body
|
||||
:close-on-click-modal="false"
|
||||
width="80%"
|
||||
:title="`${$locale.baseText('codeEdit.edit')} ${$locale
|
||||
.nodeText()
|
||||
.inputLabelDisplayName(parameter, path)}`"
|
||||
:before-close="closeDialog"
|
||||
>
|
||||
<div class="text-editor-wrapper ignore-key-press">
|
||||
<code-editor
|
||||
:value="value"
|
||||
:autocomplete="loadAutocompleteData"
|
||||
:readonly="readonly"
|
||||
@input="$emit('valueChanged', $event)"
|
||||
/>
|
||||
</div>
|
||||
</el-dialog>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { genericHelpers } from '@/mixins/genericHelpers';
|
||||
import { workflowHelpers } from '@/mixins/workflowHelpers';
|
||||
|
||||
import mixins from 'vue-typed-mixins';
|
||||
import type { INodeUi } from '@/Interface';
|
||||
import type {
|
||||
IBinaryKeyData,
|
||||
IDataObject,
|
||||
INodeExecutionData,
|
||||
IRunExecutionData,
|
||||
IWorkflowDataProxyAdditionalKeys,
|
||||
} from 'n8n-workflow';
|
||||
import { WorkflowDataProxy } from 'n8n-workflow';
|
||||
|
||||
import { PLACEHOLDER_FILLED_AT_EXECUTION_TIME } from '@/constants';
|
||||
import { CodeEditor } from './forms';
|
||||
import { mapStores } from 'pinia';
|
||||
import { useWorkflowsStore } from '@/stores/workflows';
|
||||
import { useRootStore } from '@/stores/n8nRootStore';
|
||||
import { useNDVStore } from '@/stores/ndv';
|
||||
|
||||
export default mixins(genericHelpers, workflowHelpers).extend({
|
||||
name: 'CodeEdit',
|
||||
components: {
|
||||
CodeEditor,
|
||||
},
|
||||
props: ['codeAutocomplete', 'parameter', 'path', 'type', 'value', 'readonly'],
|
||||
computed: {
|
||||
...mapStores(useNDVStore, useRootStore, useWorkflowsStore),
|
||||
},
|
||||
methods: {
|
||||
loadAutocompleteData(): string[] {
|
||||
if (['function', 'functionItem'].includes(this.codeAutocomplete)) {
|
||||
const itemIndex = 0;
|
||||
const inputName = 'main';
|
||||
const mode = 'manual';
|
||||
let runIndex = 0;
|
||||
|
||||
const executedWorkflow = this.workflowsStore.getWorkflowExecution;
|
||||
const workflow = this.getCurrentWorkflow();
|
||||
const activeNode: INodeUi | null = this.ndvStore.activeNode;
|
||||
const parentNode = workflow.getParentNodes(activeNode!.name, inputName, 1);
|
||||
const nodeConnection = workflow.getNodeConnectionIndexes(
|
||||
activeNode!.name,
|
||||
parentNode[0],
|
||||
) || {
|
||||
sourceIndex: 0,
|
||||
destinationIndex: 0,
|
||||
};
|
||||
|
||||
const executionData = this.workflowsStore.getWorkflowExecution;
|
||||
|
||||
let runExecutionData: IRunExecutionData;
|
||||
if (!executionData || !executionData.data) {
|
||||
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,
|
||||
activeNode!.name,
|
||||
inputName,
|
||||
runIndex,
|
||||
nodeConnection,
|
||||
);
|
||||
|
||||
const additionalProxyKeys: IWorkflowDataProxyAdditionalKeys = {
|
||||
$execution: {
|
||||
id: PLACEHOLDER_FILLED_AT_EXECUTION_TIME,
|
||||
mode: 'test',
|
||||
resumeUrl: PLACEHOLDER_FILLED_AT_EXECUTION_TIME,
|
||||
},
|
||||
|
||||
// deprecated
|
||||
$executionId: PLACEHOLDER_FILLED_AT_EXECUTION_TIME,
|
||||
$resumeWebhookUrl: PLACEHOLDER_FILLED_AT_EXECUTION_TIME,
|
||||
};
|
||||
|
||||
const dataProxy = new WorkflowDataProxy(
|
||||
workflow,
|
||||
runExecutionData,
|
||||
runIndex,
|
||||
itemIndex,
|
||||
activeNode!.name,
|
||||
connectionInputData || [],
|
||||
{},
|
||||
mode,
|
||||
this.rootStore.timezone,
|
||||
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): {};',
|
||||
'function $item(itemIndex: number, runIndex?: number): {};',
|
||||
'function $items(nodeName?: string, outputIndex?: number, runIndex?: number): {};',
|
||||
];
|
||||
|
||||
const baseKeys = [
|
||||
'$env',
|
||||
'$executionId',
|
||||
'$mode',
|
||||
'$parameter',
|
||||
'$resumeWebhookUrl',
|
||||
'$vars',
|
||||
'$workflow',
|
||||
'$now',
|
||||
'$today',
|
||||
'$thisRunIndex',
|
||||
'DateTime',
|
||||
'Duration',
|
||||
'Interval',
|
||||
];
|
||||
|
||||
const functionItemKeys = ['$json', '$binary', '$position', '$thisItem', '$thisItemIndex'];
|
||||
|
||||
const additionalKeys: string[] = [];
|
||||
if (this.codeAutocomplete === 'functionItem') {
|
||||
additionalKeys.push(...functionItemKeys);
|
||||
}
|
||||
|
||||
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)}`);
|
||||
autoCompleteItems.push('function $jmespath(jsonDoc: object, query: string): {};');
|
||||
|
||||
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} = {}');
|
||||
}
|
||||
}
|
||||
|
||||
return autoCompleteItems;
|
||||
}
|
||||
|
||||
return [];
|
||||
},
|
||||
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;
|
||||
},
|
||||
|
||||
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;
|
||||
},
|
||||
},
|
||||
});
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.text-editor {
|
||||
min-height: 30rem;
|
||||
}
|
||||
</style>
|
|
@ -1,11 +1,11 @@
|
|||
<template>
|
||||
<div
|
||||
:class="$style['code-node-editor-container']"
|
||||
:class="['code-node-editor', $style['code-node-editor-container']]"
|
||||
@mouseover="onMouseOver"
|
||||
@mouseout="onMouseOut"
|
||||
ref="codeNodeEditorContainer"
|
||||
>
|
||||
<div ref="codeNodeEditor" class="ph-no-capture"></div>
|
||||
<div ref="codeNodeEditor" class="code-node-editor-input ph-no-capture"></div>
|
||||
<n8n-button
|
||||
v-if="isCloud && (isEditorHovered || isEditorFocused)"
|
||||
size="small"
|
||||
|
@ -19,40 +19,60 @@
|
|||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import type { PropType } from 'vue';
|
||||
import { mapStores } from 'pinia';
|
||||
import mixins from 'vue-typed-mixins';
|
||||
|
||||
import type { Extension } from '@codemirror/state';
|
||||
import { Compartment, EditorState } from '@codemirror/state';
|
||||
import type { ViewUpdate } from '@codemirror/view';
|
||||
import { EditorView } from '@codemirror/view';
|
||||
import { javascript } from '@codemirror/lang-javascript';
|
||||
import { json } from '@codemirror/lang-json';
|
||||
|
||||
import { baseExtensions } from './baseExtensions';
|
||||
import { readOnlyEditorExtensions, writableEditorExtensions } from './baseExtensions';
|
||||
import { linterExtension } from './linter';
|
||||
import { completerExtension } from './completer';
|
||||
import { CODE_NODE_EDITOR_THEME } from './theme';
|
||||
import { codeNodeEditorTheme } from './theme';
|
||||
import { workflowHelpers } from '@/mixins/workflowHelpers'; // for json field completions
|
||||
import { ASK_AI_MODAL_KEY, CODE_NODE_TYPE } from '@/constants';
|
||||
import { codeNodeEditorEventBus } from '@/event-bus';
|
||||
import { ALL_ITEMS_PLACEHOLDER, EACH_ITEM_PLACEHOLDER } from './constants';
|
||||
import { mapStores } from 'pinia';
|
||||
import {
|
||||
ALL_ITEMS_PLACEHOLDER,
|
||||
CODE_LANGUAGES,
|
||||
CODE_MODES,
|
||||
EACH_ITEM_PLACEHOLDER,
|
||||
} from './constants';
|
||||
import { useRootStore } from '@/stores/n8nRootStore';
|
||||
import Modal from '../Modal.vue';
|
||||
import { useSettingsStore } from '@/stores/settings';
|
||||
import type { CodeLanguage, CodeMode } from './types';
|
||||
|
||||
const placeholders: Partial<Record<CodeLanguage, Record<CodeMode, string>>> = {
|
||||
javaScript: {
|
||||
runOnceForAllItems: ALL_ITEMS_PLACEHOLDER,
|
||||
runOnceForEachItem: EACH_ITEM_PLACEHOLDER,
|
||||
},
|
||||
};
|
||||
|
||||
export default mixins(linterExtension, completerExtension, workflowHelpers).extend({
|
||||
name: 'code-node-editor',
|
||||
components: { Modal },
|
||||
props: {
|
||||
mode: {
|
||||
type: String,
|
||||
validator: (value: string): boolean =>
|
||||
['runOnceForAllItems', 'runOnceForEachItem'].includes(value),
|
||||
type: String as PropType<CodeMode>,
|
||||
validator: (value: CodeMode): boolean => CODE_MODES.includes(value),
|
||||
},
|
||||
language: {
|
||||
type: String as PropType<CodeLanguage>,
|
||||
default: 'javaScript' as CodeLanguage,
|
||||
validator: (value: CodeLanguage): boolean => CODE_LANGUAGES.includes(value),
|
||||
},
|
||||
isReadOnly: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
jsCode: {
|
||||
value: {
|
||||
type: String,
|
||||
},
|
||||
},
|
||||
|
@ -65,9 +85,12 @@ export default mixins(linterExtension, completerExtension, workflowHelpers).exte
|
|||
};
|
||||
},
|
||||
watch: {
|
||||
mode() {
|
||||
mode(newMode, previousMode: CodeMode) {
|
||||
this.reloadLinter();
|
||||
this.refreshPlaceholder();
|
||||
|
||||
if (this.content.trim() === placeholders[this.language]?.[previousMode]) {
|
||||
this.refreshPlaceholder();
|
||||
}
|
||||
},
|
||||
},
|
||||
computed: {
|
||||
|
@ -81,16 +104,7 @@ export default mixins(linterExtension, completerExtension, workflowHelpers).exte
|
|||
return this.editor.state.doc.toString();
|
||||
},
|
||||
placeholder(): string {
|
||||
return {
|
||||
runOnceForAllItems: ALL_ITEMS_PLACEHOLDER,
|
||||
runOnceForEachItem: EACH_ITEM_PLACEHOLDER,
|
||||
}[this.mode];
|
||||
},
|
||||
previousPlaceholder(): string {
|
||||
return {
|
||||
runOnceForAllItems: EACH_ITEM_PLACEHOLDER,
|
||||
runOnceForEachItem: ALL_ITEMS_PLACEHOLDER,
|
||||
}[this.mode];
|
||||
return placeholders[this.language]?.[this.mode] ?? '';
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
|
@ -114,25 +128,26 @@ export default mixins(linterExtension, completerExtension, workflowHelpers).exte
|
|||
reloadLinter() {
|
||||
if (!this.editor) return;
|
||||
|
||||
this.editor.dispatch({
|
||||
effects: this.linterCompartment.reconfigure(this.linterExtension()),
|
||||
});
|
||||
const linter = this.createLinter(this.language);
|
||||
if (linter) {
|
||||
this.editor.dispatch({
|
||||
effects: this.linterCompartment.reconfigure(linter),
|
||||
});
|
||||
}
|
||||
},
|
||||
refreshPlaceholder() {
|
||||
if (!this.editor) return;
|
||||
|
||||
if (!this.content.trim() || this.content.trim() === this.previousPlaceholder) {
|
||||
this.editor.dispatch({
|
||||
changes: { from: 0, to: this.content.length, insert: this.placeholder },
|
||||
});
|
||||
}
|
||||
this.editor.dispatch({
|
||||
changes: { from: 0, to: this.content.length, insert: this.placeholder },
|
||||
});
|
||||
},
|
||||
highlightLine(line: number | 'final') {
|
||||
if (!this.editor) return;
|
||||
|
||||
if (line === 'final') {
|
||||
this.editor.dispatch({
|
||||
selection: { anchor: this.content.trim().length },
|
||||
selection: { anchor: this.content.length },
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
@ -175,45 +190,62 @@ export default mixins(linterExtension, completerExtension, workflowHelpers).exte
|
|||
},
|
||||
},
|
||||
destroyed() {
|
||||
codeNodeEditorEventBus.off('error-line-number', this.highlightLine);
|
||||
if (!this.isReadOnly) codeNodeEditorEventBus.off('error-line-number', this.highlightLine);
|
||||
},
|
||||
mounted() {
|
||||
codeNodeEditorEventBus.on('error-line-number', this.highlightLine);
|
||||
|
||||
const stateBasedExtensions = [
|
||||
this.linterCompartment.of(this.linterExtension()),
|
||||
EditorState.readOnly.of(this.isReadOnly),
|
||||
EditorView.domEventHandlers({
|
||||
focus: () => {
|
||||
this.isEditorFocused = true;
|
||||
},
|
||||
blur: () => {
|
||||
this.isEditorFocused = false;
|
||||
},
|
||||
}),
|
||||
EditorView.updateListener.of((viewUpdate: ViewUpdate) => {
|
||||
if (!viewUpdate.docChanged) return;
|
||||
|
||||
this.trackCompletion(viewUpdate);
|
||||
|
||||
this.$emit('valueChanged', this.content);
|
||||
}),
|
||||
];
|
||||
if (!this.isReadOnly) codeNodeEditorEventBus.on('error-line-number', this.highlightLine);
|
||||
|
||||
// empty on first load, default param value
|
||||
if (this.jsCode === '') {
|
||||
if (!this.value) {
|
||||
this.$emit('valueChanged', this.placeholder);
|
||||
}
|
||||
|
||||
const { isReadOnly, language } = this;
|
||||
const extensions: Extension[] = [
|
||||
...readOnlyEditorExtensions,
|
||||
EditorState.readOnly.of(isReadOnly),
|
||||
EditorView.editable.of(!isReadOnly),
|
||||
codeNodeEditorTheme({ isReadOnly }),
|
||||
];
|
||||
|
||||
if (!isReadOnly) {
|
||||
const linter = this.createLinter(language);
|
||||
if (linter) {
|
||||
extensions.push(this.linterCompartment.of(linter));
|
||||
}
|
||||
|
||||
extensions.push(
|
||||
...writableEditorExtensions,
|
||||
EditorView.domEventHandlers({
|
||||
focus: () => {
|
||||
this.isEditorFocused = true;
|
||||
},
|
||||
blur: () => {
|
||||
this.isEditorFocused = false;
|
||||
},
|
||||
}),
|
||||
EditorView.updateListener.of((viewUpdate) => {
|
||||
if (!viewUpdate.docChanged) return;
|
||||
|
||||
this.trackCompletion(viewUpdate);
|
||||
|
||||
this.$emit('valueChanged', this.editor?.state.doc.toString());
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
switch (language) {
|
||||
case 'json':
|
||||
extensions.push(json());
|
||||
break;
|
||||
case 'javaScript':
|
||||
extensions.push(javascript(), this.autocompletionExtension());
|
||||
break;
|
||||
}
|
||||
|
||||
const state = EditorState.create({
|
||||
doc: this.jsCode === '' ? this.placeholder : this.jsCode,
|
||||
extensions: [
|
||||
...baseExtensions,
|
||||
...stateBasedExtensions,
|
||||
CODE_NODE_EDITOR_THEME,
|
||||
javascript(),
|
||||
this.autocompletionExtension(),
|
||||
],
|
||||
doc: this.value || this.placeholder,
|
||||
extensions,
|
||||
});
|
||||
|
||||
this.editor = new EditorView({
|
||||
|
@ -227,6 +259,10 @@ export default mixins(linterExtension, completerExtension, workflowHelpers).exte
|
|||
<style lang="scss" module>
|
||||
.code-node-editor-container {
|
||||
position: relative;
|
||||
|
||||
& > div {
|
||||
height: 100%;
|
||||
}
|
||||
}
|
||||
|
||||
.ask-ai-button {
|
||||
|
|
|
@ -18,21 +18,26 @@ import {
|
|||
deleteCharBackward,
|
||||
} from '@codemirror/commands';
|
||||
import { lintGutter } from '@codemirror/lint';
|
||||
import type { Extension } from '@codemirror/state';
|
||||
|
||||
import { codeInputHandler } from '@/plugins/codemirror/inputHandlers/code.inputHandler';
|
||||
|
||||
export const baseExtensions = [
|
||||
export const readOnlyEditorExtensions: readonly Extension[] = [
|
||||
lineNumbers(),
|
||||
highlightActiveLineGutter(),
|
||||
EditorView.lineWrapping,
|
||||
highlightSpecialChars(),
|
||||
];
|
||||
|
||||
export const writableEditorExtensions: readonly Extension[] = [
|
||||
history(),
|
||||
foldGutter(),
|
||||
lintGutter(),
|
||||
foldGutter(),
|
||||
codeInputHandler(),
|
||||
dropCursor(),
|
||||
indentOnInput(),
|
||||
bracketMatching(),
|
||||
highlightActiveLine(),
|
||||
highlightActiveLineGutter(),
|
||||
keymap.of([
|
||||
{ key: 'Enter', run: insertNewlineAndIndent },
|
||||
{ key: 'Tab', run: acceptCompletion },
|
||||
|
@ -42,5 +47,4 @@ export const baseExtensions = [
|
|||
{ key: 'Backspace', run: deleteCharBackward, shift: deleteCharBackward },
|
||||
indentWithTab,
|
||||
]),
|
||||
EditorView.lineWrapping,
|
||||
];
|
||||
|
|
|
@ -51,3 +51,6 @@ $input.item.json.myNewField = 1;
|
|||
|
||||
return $input.item;
|
||||
`.trim();
|
||||
|
||||
export const CODE_LANGUAGES = ['javaScript', 'json'] as const;
|
||||
export const CODE_MODES = ['runOnceForAllItems', 'runOnceForEachItem'] as const;
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
import Vue from 'vue';
|
||||
import type { Diagnostic } from '@codemirror/lint';
|
||||
import { linter as createLinter } from '@codemirror/lint';
|
||||
import { jsonParseLinter } from '@codemirror/lang-json';
|
||||
import * as esprima from 'esprima-next';
|
||||
|
||||
import {
|
||||
|
@ -12,12 +13,18 @@ import { walk } from './utils';
|
|||
|
||||
import type { EditorView } from '@codemirror/view';
|
||||
import type { Node } from 'estree';
|
||||
import type { CodeNodeEditorMixin, RangeNode } from './types';
|
||||
import type { CodeLanguage, CodeNodeEditorMixin, RangeNode } from './types';
|
||||
|
||||
export const linterExtension = (Vue as CodeNodeEditorMixin).extend({
|
||||
methods: {
|
||||
linterExtension() {
|
||||
return createLinter(this.lintSource, { delay: DEFAULT_LINTER_DELAY_IN_MS });
|
||||
createLinter(language: CodeLanguage) {
|
||||
switch (language) {
|
||||
case 'javaScript':
|
||||
return createLinter(this.lintSource, { delay: DEFAULT_LINTER_DELAY_IN_MS });
|
||||
case 'json':
|
||||
return createLinter(jsonParseLinter());
|
||||
}
|
||||
return undefined;
|
||||
},
|
||||
|
||||
lintSource(editorView: EditorView): Diagnostic[] {
|
||||
|
|
|
@ -29,7 +29,11 @@ const BASE_STYLING = {
|
|||
|
||||
const cssStyleDeclaration = getComputedStyle(document.documentElement);
|
||||
|
||||
export const CODE_NODE_EDITOR_THEME = [
|
||||
interface ThemeSettings {
|
||||
isReadOnly?: boolean;
|
||||
}
|
||||
|
||||
export const codeNodeEditorTheme = ({ isReadOnly }: ThemeSettings) => [
|
||||
EditorView.theme({
|
||||
'&': {
|
||||
'font-size': BASE_STYLING.fontSize,
|
||||
|
@ -37,6 +41,7 @@ export const CODE_NODE_EDITOR_THEME = [
|
|||
borderRadius: cssStyleDeclaration.getPropertyValue('--border-radius-base'),
|
||||
backgroundColor: 'var(--color-code-background)',
|
||||
color: 'var(--color-code-foreground)',
|
||||
height: '100%',
|
||||
},
|
||||
'.cm-content': {
|
||||
fontFamily: BASE_STYLING.fontFamily,
|
||||
|
@ -48,6 +53,9 @@ export const CODE_NODE_EDITOR_THEME = [
|
|||
'&.cm-focused .cm-selectionBackgroundm .cm-selectionBackground, .cm-content ::selection': {
|
||||
backgroundColor: 'var(--color-code-selection)',
|
||||
},
|
||||
'&.cm-editor': {
|
||||
...(isReadOnly ? { backgroundColor: 'var(--color-code-background-readonly)' } : {}),
|
||||
},
|
||||
'&.cm-editor.cm-focused': {
|
||||
outline: 'none',
|
||||
borderColor: 'var(--color-secondary)',
|
||||
|
@ -59,7 +67,9 @@ export const CODE_NODE_EDITOR_THEME = [
|
|||
backgroundColor: 'var(--color-code-lineHighlight)',
|
||||
},
|
||||
'.cm-gutters': {
|
||||
backgroundColor: 'var(--color-code-gutterBackground)',
|
||||
backgroundColor: isReadOnly
|
||||
? 'var(--color-code-background-readonly)'
|
||||
: 'var(--color-code-gutterBackground)',
|
||||
color: 'var(--color-code-gutterForeground)',
|
||||
borderRadius: 'var(--border-radius-base)',
|
||||
},
|
||||
|
@ -69,7 +79,8 @@ export const CODE_NODE_EDITOR_THEME = [
|
|||
},
|
||||
'.cm-scroller': {
|
||||
overflow: 'auto',
|
||||
maxHeight: BASE_STYLING.maxHeight,
|
||||
maxHeight: '100%',
|
||||
...(isReadOnly ? {} : { minHeight: '10em' }),
|
||||
},
|
||||
'.cm-diagnosticAction': {
|
||||
backgroundColor: BASE_STYLING.diagnosticButton.backgroundColor,
|
||||
|
|
|
@ -2,6 +2,7 @@ import type { EditorView } from '@codemirror/view';
|
|||
import type { I18nClass } from '@/plugins/i18n';
|
||||
import type { Workflow } from 'n8n-workflow';
|
||||
import type { Node } from 'estree';
|
||||
import type { CODE_LANGUAGES, CODE_MODES } from './constants';
|
||||
|
||||
export type CodeNodeEditorMixin = Vue.VueConstructor<
|
||||
Vue & {
|
||||
|
@ -13,3 +14,6 @@ export type CodeNodeEditorMixin = Vue.VueConstructor<
|
|||
>;
|
||||
|
||||
export type RangeNode = Node & { range: [number, number] };
|
||||
|
||||
export type CodeLanguage = (typeof CODE_LANGUAGES)[number];
|
||||
export type CodeMode = (typeof CODE_MODES)[number];
|
||||
|
|
|
@ -117,7 +117,7 @@
|
|||
}}</n8n-text>
|
||||
</template>
|
||||
|
||||
<template #recovered-artifical-output-data>
|
||||
<template #recovered-artificial-output-data>
|
||||
<div :class="$style.recoveredOutputData">
|
||||
<n8n-text tag="div" :bold="true" color="text-dark" size="large">{{
|
||||
$locale.baseText('executionDetails.executionFailed.recoveredNodeTitle')
|
||||
|
|
|
@ -436,6 +436,10 @@ export default mixins(debounceHelper).extend({
|
|||
visibility: hidden;
|
||||
}
|
||||
|
||||
.double-width {
|
||||
left: 90%;
|
||||
}
|
||||
|
||||
.dragButtonContainer {
|
||||
position: absolute;
|
||||
top: -12px;
|
||||
|
|
|
@ -41,6 +41,7 @@
|
|||
:hideInputAndOutput="activeNodeType === null"
|
||||
:position="isTriggerNode && !showTriggerPanel ? 0 : undefined"
|
||||
:isDraggable="!isTriggerNode"
|
||||
:hasDoubleWidth="activeNodeType?.parameterPane === 'wide'"
|
||||
:nodeType="activeNodeType"
|
||||
@close="close"
|
||||
@init="onPanelsInit"
|
||||
|
|
|
@ -962,7 +962,7 @@ export default mixins(externalHooks, nodeHelpers).extend({
|
|||
.node-parameters-wrapper {
|
||||
height: 100%;
|
||||
overflow-y: auto;
|
||||
padding: 0 20px 200px 20px;
|
||||
padding: 0 20px;
|
||||
}
|
||||
|
||||
&.dragging {
|
||||
|
|
|
@ -80,7 +80,7 @@
|
|||
</n8n-text>
|
||||
</template>
|
||||
|
||||
<template #recovered-artifical-output-data>
|
||||
<template #recovered-artificial-output-data>
|
||||
<div :class="$style.recoveredOutputData">
|
||||
<n8n-text tag="div" :bold="true" color="text-dark" size="large">{{
|
||||
$locale.baseText('executionDetails.executionFailed.recoveredNodeTitle')
|
||||
|
|
|
@ -51,17 +51,28 @@
|
|||
remoteParameterOptionsLoadingIssues !== null
|
||||
"
|
||||
>
|
||||
<code-edit
|
||||
<el-dialog
|
||||
v-if="codeEditDialogVisible"
|
||||
:value="value"
|
||||
:parameter="parameter"
|
||||
:type="editorType"
|
||||
:codeAutocomplete="codeAutocomplete"
|
||||
:path="path"
|
||||
:readonly="isReadOnly"
|
||||
@closeDialog="closeCodeEditDialog"
|
||||
@valueChanged="expressionUpdated"
|
||||
></code-edit>
|
||||
visible
|
||||
append-to-body
|
||||
:close-on-click-modal="false"
|
||||
width="80%"
|
||||
:title="`${$locale.baseText('codeEdit.edit')} ${$locale
|
||||
.nodeText()
|
||||
.inputLabelDisplayName(parameter, path)}`"
|
||||
:before-close="closeCodeEditDialog"
|
||||
>
|
||||
<div class="ignore-key-press">
|
||||
<code-node-editor
|
||||
:value="value"
|
||||
:defaultValue="parameter.default"
|
||||
:language="editorLanguage"
|
||||
:isReadOnly="isReadOnly"
|
||||
@valueChanged="expressionUpdated"
|
||||
/>
|
||||
</div>
|
||||
</el-dialog>
|
||||
|
||||
<text-edit
|
||||
:dialogVisible="textEditDialogVisible"
|
||||
:value="value"
|
||||
|
@ -73,15 +84,17 @@
|
|||
></text-edit>
|
||||
|
||||
<code-node-editor
|
||||
v-if="getArgument('editor') === 'codeNodeEditor' && isCodeNode(node)"
|
||||
v-if="editorType === 'codeNodeEditor' && isCodeNode(node)"
|
||||
:mode="node.parameters.mode"
|
||||
:jsCode="node.parameters.jsCode"
|
||||
:value="node.parameters.jsCode"
|
||||
:defaultValue="parameter.default"
|
||||
:language="editorLanguage"
|
||||
:isReadOnly="isReadOnly"
|
||||
@valueChanged="valueChangedDebounced"
|
||||
/>
|
||||
|
||||
<html-editor
|
||||
v-else-if="getArgument('editor') === 'htmlEditor'"
|
||||
v-else-if="editorType === 'htmlEditor'"
|
||||
:html="node.parameters.html"
|
||||
:isReadOnly="isReadOnly"
|
||||
:rows="getArgument('rows')"
|
||||
|
@ -91,17 +104,16 @@
|
|||
/>
|
||||
|
||||
<div
|
||||
v-else-if="isEditor === true"
|
||||
class="code-edit clickable ph-no-capture"
|
||||
v-else-if="editorType"
|
||||
class="readonly-code clickable ph-no-capture"
|
||||
@click="displayEditDialog()"
|
||||
>
|
||||
<prism-editor
|
||||
<code-node-editor
|
||||
v-if="!codeEditDialogVisible"
|
||||
:lineNumbers="true"
|
||||
:readonly="true"
|
||||
:code="displayValue"
|
||||
language="js"
|
||||
></prism-editor>
|
||||
:value="value"
|
||||
:language="editorLanguage"
|
||||
:isReadOnly="true"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<n8n-input
|
||||
|
@ -338,10 +350,11 @@ import type {
|
|||
INodeProperties,
|
||||
INodePropertyCollection,
|
||||
NodeParameterValueType,
|
||||
EditorType,
|
||||
CodeNodeEditorLanguage,
|
||||
} from 'n8n-workflow';
|
||||
import { NodeHelpers } from 'n8n-workflow';
|
||||
|
||||
import CodeEdit from '@/components/CodeEdit.vue';
|
||||
import CredentialsSelect from '@/components/CredentialsSelect.vue';
|
||||
import ImportParameter from '@/components/ImportParameter.vue';
|
||||
import ExpressionEdit from '@/components/ExpressionEdit.vue';
|
||||
|
@ -351,8 +364,6 @@ import ParameterOptions from '@/components/ParameterOptions.vue';
|
|||
import ParameterIssues from '@/components/ParameterIssues.vue';
|
||||
import ResourceLocator from '@/components/ResourceLocator/ResourceLocator.vue';
|
||||
import ExpressionParameterInput from '@/components/ExpressionParameterInput.vue';
|
||||
// @ts-ignore
|
||||
import PrismEditor from 'vue-prism-editor';
|
||||
import TextEdit from '@/components/TextEdit.vue';
|
||||
import CodeNodeEditor from '@/components/CodeNodeEditor/CodeNodeEditor.vue';
|
||||
import HtmlEditor from '@/components/HtmlEditor/HtmlEditor.vue';
|
||||
|
@ -385,14 +396,12 @@ export default mixins(
|
|||
).extend({
|
||||
name: 'parameter-input',
|
||||
components: {
|
||||
CodeEdit,
|
||||
CodeNodeEditor,
|
||||
HtmlEditor,
|
||||
ExpressionEdit,
|
||||
ExpressionParameterInput,
|
||||
NodeCredentials,
|
||||
CredentialsSelect,
|
||||
PrismEditor,
|
||||
ScopesNotice,
|
||||
ParameterOptions,
|
||||
ParameterIssues,
|
||||
|
@ -554,8 +563,8 @@ export default mixins(
|
|||
return null;
|
||||
}
|
||||
},
|
||||
node(): INodeUi | null {
|
||||
return this.ndvStore.activeNode;
|
||||
node(): INodeUi {
|
||||
return this.ndvStore.activeNode!;
|
||||
},
|
||||
displayTitle(): string {
|
||||
const interpolation = { interpolate: { shortPath: this.shortPath } };
|
||||
|
@ -636,7 +645,7 @@ export default mixins(
|
|||
return 'textarea';
|
||||
}
|
||||
|
||||
if (this.parameter.typeOptions && this.parameter.typeOptions.editor === 'code') {
|
||||
if (this.editorType === 'code') {
|
||||
return 'textarea';
|
||||
}
|
||||
|
||||
|
@ -719,11 +728,12 @@ export default mixins(
|
|||
|
||||
return [];
|
||||
},
|
||||
isEditor(): boolean {
|
||||
return ['code', 'json'].includes(this.editorType);
|
||||
editorType(): EditorType {
|
||||
return this.getArgument('editor') as EditorType;
|
||||
},
|
||||
editorType(): string {
|
||||
return this.getArgument('editor') as string;
|
||||
editorLanguage(): CodeNodeEditorLanguage {
|
||||
if (this.editorType === 'json' || this.parameter.type === 'json') return 'json';
|
||||
return 'javaScript';
|
||||
},
|
||||
parameterOptions():
|
||||
| Array<INodePropertyOptions | INodeProperties | INodePropertyCollection>
|
||||
|
@ -907,22 +917,14 @@ export default mixins(
|
|||
this.textEditDialogVisible = false;
|
||||
},
|
||||
displayEditDialog() {
|
||||
if (this.isEditor) {
|
||||
if (this.editorType) {
|
||||
this.codeEditDialogVisible = true;
|
||||
} else {
|
||||
this.textEditDialogVisible = true;
|
||||
}
|
||||
},
|
||||
getArgument(argumentName: string): string | number | boolean | undefined {
|
||||
if (this.parameter.typeOptions === undefined) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
if (this.parameter.typeOptions[argumentName] === undefined) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
return this.parameter.typeOptions[argumentName];
|
||||
return this.parameter.typeOptions?.[argumentName];
|
||||
},
|
||||
expressionUpdated(value: string) {
|
||||
const val: NodeParameterValueType = this.isResourceLocatorParameter
|
||||
|
@ -1167,32 +1169,6 @@ export default mixins(
|
|||
},
|
||||
{ deep: true, immediate: true },
|
||||
);
|
||||
|
||||
// Reload function on change element from
|
||||
// displayOptions.typeOptions.reloadOnChange parameters
|
||||
if (this.parameter.typeOptions && this.parameter.typeOptions.reloadOnChange) {
|
||||
// Get all parameter in reloadOnChange property
|
||||
// This reload when parameters in reloadOnChange is updated
|
||||
const parametersOnChange: string[] = this.parameter.typeOptions.reloadOnChange;
|
||||
for (let i = 0; i < parametersOnChange.length; i++) {
|
||||
const parameter = parametersOnChange[i] as string;
|
||||
if (parameter in this.node.parameters) {
|
||||
this.$watch(
|
||||
() => {
|
||||
if (this.node && this.node.parameters && this.node.parameters[parameter]) {
|
||||
return this.node.parameters![parameter];
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
},
|
||||
() => {
|
||||
this.loadRemoteParameterOptions();
|
||||
},
|
||||
{ deep: true, immediate: true },
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
this.$externalHooks().run('parameterInput.mount', {
|
||||
|
@ -1204,7 +1180,7 @@ export default mixins(
|
|||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
.code-edit {
|
||||
.readonly-code {
|
||||
font-size: var(--font-size-xs);
|
||||
}
|
||||
|
||||
|
|
|
@ -70,11 +70,7 @@ export default defineComponent({
|
|||
return false;
|
||||
}
|
||||
|
||||
if (
|
||||
this.parameter.typeOptions &&
|
||||
this.parameter.typeOptions.editor &&
|
||||
this.parameter.typeOptions.editor === 'codeNodeEditor'
|
||||
) {
|
||||
if (this.parameter.typeOptions?.editor === 'codeNodeEditor') {
|
||||
return false;
|
||||
}
|
||||
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
<template>
|
||||
<div :class="$style.container">
|
||||
<div :class="['run-data', $style.container]">
|
||||
<n8n-callout
|
||||
v-if="canPinData && hasPinData && !editMode.enabled && !isProductionExecutionPreview"
|
||||
theme="secondary"
|
||||
|
@ -190,11 +190,10 @@
|
|||
|
||||
<div v-else-if="editMode.enabled" :class="$style.editMode">
|
||||
<div :class="[$style.editModeBody, 'ignore-key-press']">
|
||||
<code-editor
|
||||
<code-node-editor
|
||||
:value="editMode.value"
|
||||
:options="{ scrollBeyondLastLine: false }"
|
||||
type="json"
|
||||
@input="ndvStore.setOutputPanelEditModeValue($event)"
|
||||
language="json"
|
||||
@valueChanged="ndvStore.setOutputPanelEditModeValue($event)"
|
||||
/>
|
||||
</div>
|
||||
<div :class="$style.editModeFooter">
|
||||
|
@ -228,7 +227,7 @@
|
|||
</div>
|
||||
|
||||
<div v-else-if="hasNodeRun && isArtificialRecoveredEventItem" :class="$style.center">
|
||||
<slot name="recovered-artifical-output-data"></slot>
|
||||
<slot name="recovered-artificial-output-data"></slot>
|
||||
</div>
|
||||
|
||||
<div v-else-if="hasNodeRun && hasRunError" :class="$style.stretchVertically">
|
||||
|
@ -501,7 +500,7 @@ import { externalHooks } from '@/mixins/externalHooks';
|
|||
import { genericHelpers } from '@/mixins/genericHelpers';
|
||||
import { nodeHelpers } from '@/mixins/nodeHelpers';
|
||||
import { pinData } from '@/mixins/pinData';
|
||||
import { CodeEditor } from '@/components/forms';
|
||||
import CodeNodeEditor from '@/components/CodeNodeEditor/CodeNodeEditor.vue';
|
||||
import { dataPinningEventBus } from '@/event-bus';
|
||||
import { clearJsonKey, executionDataToJson, stringSizeInBytes } from '@/utils';
|
||||
import { isEmpty } from '@/utils';
|
||||
|
@ -525,7 +524,7 @@ export default mixins(externalHooks, genericHelpers, nodeHelpers, pinData).exten
|
|||
BinaryDataDisplay,
|
||||
NodeErrorView,
|
||||
WarningTooltip,
|
||||
CodeEditor,
|
||||
CodeNodeEditor,
|
||||
RunDataTable,
|
||||
RunDataJson,
|
||||
RunDataSchema,
|
||||
|
@ -1571,28 +1570,30 @@ export default mixins(externalHooks, genericHelpers, nodeHelpers, pinData).exten
|
|||
}
|
||||
|
||||
.editMode {
|
||||
height: calc(100% - var(--spacing-s));
|
||||
height: 100%;
|
||||
max-height: calc(100% - var(--spacing-3xl));
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: flex-end;
|
||||
align-items: flex-end;
|
||||
justify-content: stretch;
|
||||
padding-left: var(--spacing-s);
|
||||
padding-right: var(--spacing-s);
|
||||
}
|
||||
|
||||
.editModeBody {
|
||||
flex: 1 1 auto;
|
||||
max-height: 100%;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
overflow: hidden;
|
||||
overflow: auto;
|
||||
}
|
||||
|
||||
.editModeFooter {
|
||||
flex: 0 1 auto;
|
||||
display: flex;
|
||||
width: 100%;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding-top: var(--spacing-s);
|
||||
padding-bottom: var(--spacing-s);
|
||||
}
|
||||
|
||||
.editModeFooterInfotip {
|
||||
|
@ -1617,3 +1618,11 @@ export default mixins(externalHooks, genericHelpers, nodeHelpers, pinData).exten
|
|||
border-bottom-left-radius: 0;
|
||||
}
|
||||
</style>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.run-data {
|
||||
.code-node-editor {
|
||||
height: 100%;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
|
|
@ -1,137 +0,0 @@
|
|||
<template>
|
||||
<div ref="code" class="text-editor ph-no-capture" @keydown.stop />
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { defineComponent } from 'vue';
|
||||
import * as monaco from 'monaco-editor';
|
||||
|
||||
export default defineComponent({
|
||||
props: {
|
||||
type: {
|
||||
type: String,
|
||||
default: 'code',
|
||||
},
|
||||
readonly: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
value: {
|
||||
type: String,
|
||||
default: '',
|
||||
},
|
||||
autocomplete: {
|
||||
type: Function,
|
||||
},
|
||||
options: {
|
||||
type: Object,
|
||||
default: () => ({}),
|
||||
},
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
monacoInstance: null as monaco.editor.IStandaloneCodeEditor | null,
|
||||
monacoLibrary: null as monaco.IDisposable | null,
|
||||
};
|
||||
},
|
||||
methods: {
|
||||
loadEditor() {
|
||||
if (!this.$refs.code) return;
|
||||
|
||||
this.monacoInstance = monaco.editor.create(this.$refs.code as HTMLElement, {
|
||||
automaticLayout: true,
|
||||
value: this.value,
|
||||
language: this.type === 'code' ? 'javascript' : 'json',
|
||||
tabSize: 2,
|
||||
wordBasedSuggestions: false,
|
||||
readOnly: this.readonly,
|
||||
padding: {
|
||||
top: 16,
|
||||
},
|
||||
minimap: {
|
||||
enabled: false,
|
||||
},
|
||||
...this.options,
|
||||
});
|
||||
|
||||
this.monacoInstance.onDidChangeModelContent(() => {
|
||||
const model = this.monacoInstance!.getModel();
|
||||
if (model) {
|
||||
this.$emit('input', model.getValue());
|
||||
}
|
||||
});
|
||||
|
||||
const darkModeBetaEnabled =
|
||||
document.body.classList.contains('theme-dark-beta') &&
|
||||
window.matchMedia('(prefers-color-scheme: dark)').matches;
|
||||
|
||||
monaco.editor.defineTheme('n8nCustomTheme', {
|
||||
base: darkModeBetaEnabled ? 'vs-dark' : 'vs',
|
||||
inherit: true,
|
||||
rules: [],
|
||||
colors: {},
|
||||
});
|
||||
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,
|
||||
});
|
||||
|
||||
if (this.autocomplete) {
|
||||
this.monacoLibrary = monaco.languages.typescript.javascriptDefaults.addExtraLib(
|
||||
this.autocomplete().join('\n'),
|
||||
);
|
||||
}
|
||||
} else if (this.type === 'json') {
|
||||
monaco.languages.json.jsonDefaults.setDiagnosticsOptions({
|
||||
validate: true,
|
||||
});
|
||||
}
|
||||
},
|
||||
handleResize() {
|
||||
if (this.monacoInstance) {
|
||||
// Workaround to force Monaco to recompute its boundaries
|
||||
this.monacoInstance.layout({} as unknown as undefined);
|
||||
}
|
||||
},
|
||||
},
|
||||
mounted() {
|
||||
setTimeout(this.loadEditor);
|
||||
window.addEventListener('resize', this.handleResize);
|
||||
},
|
||||
destroyed() {
|
||||
if (this.monacoLibrary) {
|
||||
this.monacoLibrary.dispose();
|
||||
}
|
||||
window.removeEventListener('resize', this.handleResize);
|
||||
},
|
||||
});
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.text-editor {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
flex: 1 1 auto;
|
||||
border: 1px solid var(--color-foreground-base);
|
||||
border-radius: var(--border-radius-base);
|
||||
}
|
||||
|
||||
::v-deep {
|
||||
.monaco-editor {
|
||||
.slider {
|
||||
border-radius: var(--border-radius-base);
|
||||
}
|
||||
|
||||
&,
|
||||
&-background,
|
||||
.inputarea.ime-input,
|
||||
.margin {
|
||||
border-radius: var(--border-radius-base);
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
|
@ -1 +0,0 @@
|
|||
export { default as CodeEditor } from './CodeEditor.vue';
|
|
@ -3,9 +3,6 @@
|
|||
import Vue from 'vue';
|
||||
|
||||
import './plugins';
|
||||
import 'prismjs';
|
||||
import 'prismjs/themes/prism.css';
|
||||
import 'vue-prism-editor/dist/VuePrismEditor.css';
|
||||
import 'vue-json-pretty/lib/styles.css';
|
||||
import '@jsplumb/browser-ui/css/jsplumbtoolkit.css';
|
||||
import 'n8n-design-system/css/index.scss';
|
||||
|
|
|
@ -1,6 +1,5 @@
|
|||
import vue from '@vitejs/plugin-vue2';
|
||||
import legacy from '@vitejs/plugin-legacy';
|
||||
import monacoEditorPlugin from 'vite-plugin-monaco-editor';
|
||||
import path, { resolve } from 'path';
|
||||
import { defineConfig, mergeConfig } from 'vite';
|
||||
import { defineConfig as defineVitestConfig } from 'vitest/config';
|
||||
|
@ -73,24 +72,11 @@ const alias = [
|
|||
},
|
||||
];
|
||||
|
||||
// https://github.com/vitest-dev/vitest/discussions/1806
|
||||
if (NODE_ENV === 'test') {
|
||||
alias.push({
|
||||
find: /^monaco-editor$/,
|
||||
replacement: __dirname + '/node_modules/monaco-editor/esm/vs/editor/editor.api',
|
||||
});
|
||||
}
|
||||
|
||||
const plugins = [
|
||||
vue(),
|
||||
legacy({
|
||||
targets: ['>1%', 'last 3 versions', 'not dead'],
|
||||
}),
|
||||
monacoEditorPlugin({
|
||||
publicPath: 'assets/monaco-editor',
|
||||
customDistPath: (root: string, buildOutDir: string, base: string) =>
|
||||
`${root}/${buildOutDir}/assets/monaco-editor`,
|
||||
}),
|
||||
];
|
||||
|
||||
const { SENTRY_AUTH_TOKEN: authToken, RELEASE: release } = process.env;
|
||||
|
|
|
@ -1019,7 +1019,8 @@ export type NodePropertyTypes =
|
|||
|
||||
export type CodeAutocompleteTypes = 'function' | 'functionItem';
|
||||
|
||||
export type EditorTypes = 'code' | 'codeNodeEditor' | 'htmlEditor' | 'json';
|
||||
export type EditorType = 'code' | 'codeNodeEditor' | 'htmlEditor' | 'json';
|
||||
export type CodeNodeEditorLanguage = 'javaScript' | 'json'; //| 'python' | 'sql';
|
||||
|
||||
export interface ILoadOptions {
|
||||
routing?: {
|
||||
|
@ -1032,7 +1033,8 @@ export interface ILoadOptions {
|
|||
export interface INodePropertyTypeOptions {
|
||||
alwaysOpenEditWindow?: boolean; // Supported by: json
|
||||
codeAutocomplete?: CodeAutocompleteTypes; // Supported by: string
|
||||
editor?: EditorTypes; // Supported by: string
|
||||
editor?: EditorType; // Supported by: string
|
||||
editorLanguage?: CodeNodeEditorLanguage; // Supported by: string in combination with editor: codeNodeEditor
|
||||
loadOptionsDependsOn?: string[]; // Supported by: options
|
||||
loadOptionsMethod?: string; // Supported by: options
|
||||
loadOptions?: ILoadOptions; // Supported by: options
|
||||
|
|
100
pnpm-lock.yaml
100
pnpm-lock.yaml
|
@ -841,6 +841,9 @@ importers:
|
|||
'@codemirror/lang-javascript':
|
||||
specifier: ^6.1.2
|
||||
version: 6.1.2
|
||||
'@codemirror/lang-json':
|
||||
specifier: ^6.0.1
|
||||
version: 6.0.1
|
||||
'@codemirror/language':
|
||||
specifier: ^6.2.1
|
||||
version: 6.2.1
|
||||
|
@ -928,9 +931,6 @@ importers:
|
|||
luxon:
|
||||
specifier: ^3.3.0
|
||||
version: 3.3.0
|
||||
monaco-editor:
|
||||
specifier: ^0.33.0
|
||||
version: 0.33.0
|
||||
n8n-design-system:
|
||||
specifier: workspace:*
|
||||
version: link:../design-system
|
||||
|
@ -946,9 +946,6 @@ importers:
|
|||
prettier:
|
||||
specifier: ^2.8.3
|
||||
version: 2.8.3
|
||||
prismjs:
|
||||
specifier: ^1.17.1
|
||||
version: 1.17.1
|
||||
stream-browserify:
|
||||
specifier: ^3.0.0
|
||||
version: 3.0.0
|
||||
|
@ -979,9 +976,6 @@ importers:
|
|||
vue-json-pretty:
|
||||
specifier: 1.9.3
|
||||
version: 1.9.3
|
||||
vue-prism-editor:
|
||||
specifier: ^0.3.0
|
||||
version: 0.3.0
|
||||
vue-router:
|
||||
specifier: ^3.6.5
|
||||
version: 3.6.5(vue@2.7.14)
|
||||
|
@ -1088,9 +1082,6 @@ importers:
|
|||
vite:
|
||||
specifier: 4.0.4
|
||||
version: 4.0.4(@types/node@16.18.12)(sass@1.55.0)(terser@5.16.1)
|
||||
vite-plugin-monaco-editor:
|
||||
specifier: ^1.0.10
|
||||
version: 1.0.10(monaco-editor@0.33.0)
|
||||
vitest:
|
||||
specifier: ^0.28.5
|
||||
version: 0.28.5(sass@1.55.0)(terser@5.16.1)
|
||||
|
@ -3369,6 +3360,13 @@ packages:
|
|||
'@lezer/javascript': 1.0.2
|
||||
dev: false
|
||||
|
||||
/@codemirror/lang-json@6.0.1:
|
||||
resolution: {integrity: sha512-+T1flHdgpqDDlJZ2Lkil/rLiRy684WMLc74xUnjJH48GQdfJo/pudlTRreZmKwzP8/tGdKf83wlbAdOCzlJOGQ==}
|
||||
dependencies:
|
||||
'@codemirror/language': 6.2.1
|
||||
'@lezer/json': 1.0.0
|
||||
dev: false
|
||||
|
||||
/@codemirror/language@6.2.1:
|
||||
resolution: {integrity: sha512-MC3svxuvIj0MRpFlGHxLS6vPyIdbTr2KKPEW46kCoCXw2ktb4NTkpkPBI/lSP/FoNXLCBJ0mrnUi1OoZxtpW1Q==}
|
||||
dependencies:
|
||||
|
@ -4232,6 +4230,13 @@ packages:
|
|||
'@lezer/lr': 1.2.3
|
||||
dev: false
|
||||
|
||||
/@lezer/json@1.0.0:
|
||||
resolution: {integrity: sha512-zbAuUY09RBzCoCA3lJ1+ypKw5WSNvLqGMtasdW6HvVOqZoCpPr8eWrsGnOVWGKGn8Rh21FnrKRVlJXrGAVUqRw==}
|
||||
dependencies:
|
||||
'@lezer/highlight': 1.1.1
|
||||
'@lezer/lr': 1.2.3
|
||||
dev: false
|
||||
|
||||
/@lezer/lr@1.2.3:
|
||||
resolution: {integrity: sha512-qpB7rBzH8f6Mzjv2AVZRahcm+2Cf7nbIH++uXbvVOL1yIRvVWQ3HAM/saeBLCyz/togB7LGo76qdJYL1uKQlqA==}
|
||||
dependencies:
|
||||
|
@ -9373,15 +9378,6 @@ packages:
|
|||
safe-buffer: 5.2.1
|
||||
dev: false
|
||||
|
||||
/clipboard@2.0.11:
|
||||
resolution: {integrity: sha512-C+0bbOqkezLIsmWSvlsXS0Q0bmkugu7jcfMIACB+RDEntIzQIkdr148we28AfSloQLRdZlYL/QYyrq05j/3Faw==}
|
||||
dependencies:
|
||||
good-listener: 1.2.2
|
||||
select: 1.1.2
|
||||
tiny-emitter: 2.1.0
|
||||
dev: false
|
||||
optional: true
|
||||
|
||||
/cliui@3.2.0:
|
||||
resolution: {integrity: sha512-0yayqDxWQbqk3ojkYqUKqaAQ6AfNKeKWRNA8kR0WXzAsdHpP4BIaOmMAG87JGuO6qcobyW4GjxHd9PmhEd+T9w==}
|
||||
dependencies:
|
||||
|
@ -9615,18 +9611,10 @@ packages:
|
|||
resolution: {integrity: sha512-Rd3se6QB+sO1TwqZjscQrurpEPIfO0/yYnSin6Q/rD3mOutHvUrCAhJub3r90uNb+SESBuE0QYoB90YdfatsRg==}
|
||||
dev: true
|
||||
|
||||
/component-props@1.1.1:
|
||||
resolution: {integrity: sha512-69pIRJs9fCCHRqCz3390YF2LV1Lu6iEMZ5zuVqqUn+G20V9BNXlMs0cWawWeW9g4Ynmg29JmkG6R7/lUJoGd1Q==}
|
||||
dev: false
|
||||
|
||||
/component-type@1.2.1:
|
||||
resolution: {integrity: sha512-Kgy+2+Uwr75vAi6ChWXgHuLvd+QLD7ssgpaRq2zCvt80ptvAfMc/hijcJxXkBa2wMlEZcJvC2H8Ubo+A9ATHIg==}
|
||||
dev: false
|
||||
|
||||
/component-xor@0.0.4:
|
||||
resolution: {integrity: sha512-ZIt6sla8gfo+AFVRZoZOertcnD5LJaY2T9CKE2j13NJxQt/mUafD69Bl7/Y4AnpI2LGjiXH7cOfJDx/n2G9edA==}
|
||||
dev: false
|
||||
|
||||
/compressible@2.0.18:
|
||||
resolution: {integrity: sha512-AF3r7P5dWxL8MxyITRMlORQNaOA2IkAFaTr4k7BUumjPtRpGDTZpl0Pb1XCO6JeDCBdp126Cgs9sMxqSjgYyRg==}
|
||||
engines: {node: '>= 0.6'}
|
||||
|
@ -10540,11 +10528,6 @@ packages:
|
|||
resolution: {integrity: sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==}
|
||||
engines: {node: '>=0.4.0'}
|
||||
|
||||
/delegate@3.2.0:
|
||||
resolution: {integrity: sha512-IofjkYBZaZivn0V8nnsMJGBr4jVLxHDheKSW88PyxS5QC4Vo9ZbZVvhzlSxY87fVq3STR6r+4cGepyHkcWOQSw==}
|
||||
dev: false
|
||||
optional: true
|
||||
|
||||
/delegates@1.0.0:
|
||||
resolution: {integrity: sha512-bd2L678uiWATM6m5Z1VzNCErI3jiGzt6HGY8OVICs40JQq/HALfbyNJmp0UDakEY4pMMaN0Ly5om/B1VI/+xfQ==}
|
||||
|
||||
|
@ -10691,13 +10674,6 @@ packages:
|
|||
resolution: {integrity: sha512-jNCX+uNJ3v38BKvPbpki6j5ItVlnSqVV6vDWGS6rExzCMjsc39frLjm1n91o6YaKK6AZl0wLloItW6C6mr61BQ==}
|
||||
dev: true
|
||||
|
||||
/dom-iterator@1.0.0:
|
||||
resolution: {integrity: sha512-7dsMOQI07EMU98gQM8NSB3GsAiIeBYIPKpnxR3c9xOvdvBjChAcOM0iJ222I3p5xyiZO9e5oggkNaCusuTdYig==}
|
||||
dependencies:
|
||||
component-props: 1.1.1
|
||||
component-xor: 0.0.4
|
||||
dev: false
|
||||
|
||||
/dom-serializer@0.2.2:
|
||||
resolution: {integrity: sha512-2/xPb3ORsQ42nHYiSunXkDjPLBaEj/xTwUO4B7XCZQTRk7EBtTOPaygh10YAAh2OI1Qrp6NWfpAhzswj0ydt9g==}
|
||||
dependencies:
|
||||
|
@ -12824,13 +12800,6 @@ packages:
|
|||
- supports-color
|
||||
dev: false
|
||||
|
||||
/good-listener@1.2.2:
|
||||
resolution: {integrity: sha512-goW1b+d9q/HIwbVYZzZ6SsTr4IgE+WA44A0GmPIQstuOrgsFcT7VEJ48nmr9GaRtNu0XTKacFLGnBPAM6Afouw==}
|
||||
dependencies:
|
||||
delegate: 3.2.0
|
||||
dev: false
|
||||
optional: true
|
||||
|
||||
/google-timezones-json@1.1.0:
|
||||
resolution: {integrity: sha512-6BmBx9gJVALV2jsfMks8PwmkWT5ip3+bmMyTgXu4PY+G8nKjHi61yrL7rSXpMYRsIzUXhVKpj+MnjhnwG9nung==}
|
||||
dev: false
|
||||
|
@ -16126,9 +16095,6 @@ packages:
|
|||
/moment@2.29.4:
|
||||
resolution: {integrity: sha512-5LC9SOxjSc2HF6vO2CyuTDNivEdoz2IvyJJGj6X8DJ0eFyfszE0QiEd+iXmBvUP3WHxSjFH/vIsA0EN00cgr8w==}
|
||||
|
||||
/monaco-editor@0.33.0:
|
||||
resolution: {integrity: sha512-VcRWPSLIUEgQJQIE0pVT8FcGBIgFoxz7jtqctE+IiCxWugD0DwgyQBcZBhdSrdMC84eumoqMZsGl2GTreOzwqw==}
|
||||
|
||||
/mongodb-connection-string-url@2.5.4:
|
||||
resolution: {integrity: sha512-SeAxuWs0ez3iI3vvmLk/j2y+zHwigTDKQhtdxTgt5ZCOQQS5+HW4g45/Xw5vzzbn7oQXCNQ24Z40AkJsizEy7w==}
|
||||
dependencies:
|
||||
|
@ -17812,12 +17778,6 @@ packages:
|
|||
hasBin: true
|
||||
dev: false
|
||||
|
||||
/prismjs@1.17.1:
|
||||
resolution: {integrity: sha512-PrEDJAFdUGbOP6xK/UsfkC5ghJsPJviKgnQOoxaDbBjwc8op68Quupwt1DeAFoG8GImPhiKXAvvsH7wDSLsu1Q==}
|
||||
optionalDependencies:
|
||||
clipboard: 2.0.11
|
||||
dev: false
|
||||
|
||||
/process-nextick-args@2.0.1:
|
||||
resolution: {integrity: sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==}
|
||||
|
||||
|
@ -19075,11 +19035,6 @@ packages:
|
|||
parseley: 0.7.0
|
||||
dev: false
|
||||
|
||||
/select@1.1.2:
|
||||
resolution: {integrity: sha512-OwpTSOfy6xSs1+pwcNrv0RBMOzI39Lp3qQKUTPVVPRjCdNa5JH/oPRiqsesIskK8TVgmRiHwO4KXlV2Li9dANA==}
|
||||
dev: false
|
||||
optional: true
|
||||
|
||||
/semver-greatest-satisfied-range@1.1.0:
|
||||
resolution: {integrity: sha512-Ny/iyOzSSa8M5ML46IAx3iXc6tfOsYU2R4AXi2UpHk60Zrgyq6eqPj/xiOfS0rRl/iiQ/rdJkVjw/5cdUyCntQ==}
|
||||
engines: {node: '>= 0.10'}
|
||||
|
@ -20370,11 +20325,6 @@ packages:
|
|||
resolution: {integrity: sha512-a7wPxPdVlQL7lqvitHGGRsofhdwtkoSXPGATFuSOA2i1ZNQEPLrGnj68vOp2sOJTCFAQVXPeNMX/GctBaO9L2w==}
|
||||
dev: false
|
||||
|
||||
/tiny-emitter@2.1.0:
|
||||
resolution: {integrity: sha512-NB6Dk1A9xgQPMoGqC5CVXn123gWyte215ONT5Pp5a0yt4nlEoO1ZWeCwpncaekPHXO60i47ihFnZPiRPjRMq4Q==}
|
||||
dev: false
|
||||
optional: true
|
||||
|
||||
/tiny-glob@0.2.9:
|
||||
resolution: {integrity: sha512-g/55ssRPUjShh+xkfx9UPDXqhckHEsHr4Vd9zX55oSdGZc/MD0m3sferOkwWtp98bv+kcVfEHtRJgBVJzelrzg==}
|
||||
dependencies:
|
||||
|
@ -21458,14 +21408,6 @@ packages:
|
|||
- terser
|
||||
dev: true
|
||||
|
||||
/vite-plugin-monaco-editor@1.0.10(monaco-editor@0.33.0):
|
||||
resolution: {integrity: sha512-7yTAFIE0SefjCmfnjrvXOl53wkxeSASc/ZIcB5tZeEK3vAmHhveV8y3f90Vp8b+PYdbUipjqf91mbFbSENkpcw==}
|
||||
peerDependencies:
|
||||
monaco-editor: 0.29.x
|
||||
dependencies:
|
||||
monaco-editor: 0.33.0
|
||||
dev: true
|
||||
|
||||
/vite@4.0.4(@types/node@16.18.12)(sass@1.55.0)(terser@5.16.1):
|
||||
resolution: {integrity: sha512-xevPU7M8FU0i/80DMR+YhgrzR5KS2ORy1B4xcX/cXLsvnUWvfHuqMmVU6N0YiJ4JWGRJJsLCgjEzKjG9/GKoSw==}
|
||||
engines: {node: ^14.18.0 || >=16.0.0}
|
||||
|
@ -21840,14 +21782,6 @@ packages:
|
|||
- whiskers
|
||||
dev: true
|
||||
|
||||
/vue-prism-editor@0.3.0:
|
||||
resolution: {integrity: sha512-yNSuwql/xHMJrWghn/OhZ5WPBKdhx7FkvFjgq2uDm99jHSJhuGwhcgPyuoGzpm6w8DRDzi85lgerKCu8DTDWWg==}
|
||||
dependencies:
|
||||
dom-iterator: 1.0.0
|
||||
escape-html: 1.0.3
|
||||
unescape: 1.0.1
|
||||
dev: false
|
||||
|
||||
/vue-property-decorator@9.1.2(vue-class-component@7.2.6)(vue@2.7.14):
|
||||
resolution: {integrity: sha512-xYA8MkZynPBGd/w5QFJ2d/NM0z/YeegMqYTphy7NJQXbZcuU6FC6AOdUAcy4SXP+YnkerC6AfH+ldg7PDk9ESQ==}
|
||||
peerDependencies:
|
||||
|
|
Loading…
Reference in a new issue