feat(editor): Create separate components for JS and JSON editors (no-changelog) (#8156)

## Summary
This is part-1 of refactoring our code editors to extract different type
of editors into their own components.
In part-2 we'll
1. delete a of unused or duplicate code
2. switch to a `useEditor` composable to bring more UX consistency
across all the code editors.

## Review / Merge checklist
- [x] PR title and summary are descriptive
- [x] Tests included
This commit is contained in:
कारतोफ्फेलस्क्रिप्ट™ 2023-12-29 10:49:27 +01:00 committed by GitHub
parent 1286d6583c
commit 216ec079c9
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
31 changed files with 290 additions and 91 deletions

View file

@ -133,8 +133,6 @@ export class OutputParserStructured implements INodeType {
}`,
typeOptions: {
rows: 10,
editor: 'json',
editorLanguage: 'json',
},
required: true,
},

View file

@ -117,9 +117,8 @@ export class RetrieverWorkflow implements INodeType {
{
displayName: 'Workflow JSON',
name: 'workflowJson',
type: 'string',
type: 'json',
typeOptions: {
editor: 'json',
rows: 10,
},
displayOptions: {
@ -257,11 +256,9 @@ export class RetrieverWorkflow implements INodeType {
{
displayName: 'Value',
name: 'objectValue',
type: 'string',
type: 'json',
default: '={}',
typeOptions: {
editor: 'json',
editorLanguage: 'json',
rows: 2,
},
displayOptions: {

View file

@ -119,9 +119,8 @@ export class ToolWorkflow implements INodeType {
{
displayName: 'Workflow JSON',
name: 'workflowJson',
type: 'string',
type: 'json',
typeOptions: {
editor: 'json',
rows: 10,
},
displayOptions: {
@ -266,11 +265,9 @@ export class ToolWorkflow implements INodeType {
{
displayName: 'Value',
name: 'objectValue',
type: 'string',
type: 'json',
default: '={}',
typeOptions: {
editor: 'json',
editorLanguage: 'json',
rows: 2,
},
displayOptions: {

View file

@ -123,11 +123,9 @@ describe('Validation', () => {
{
displayName: 'Value',
name: 'objectValue',
type: 'string',
type: 'json',
default: '={}',
typeOptions: {
editor: 'json',
editorLanguage: 'json',
rows: 2,
},
displayOptions: {

View file

@ -52,7 +52,6 @@ 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 { python } from '@codemirror/lang-python';
import type { CodeExecutionMode, CodeNodeEditorLanguage } from 'n8n-workflow';
import { CODE_EXECUTION_MODES, CODE_LANGUAGES } from 'n8n-workflow';
@ -97,10 +96,9 @@ export default defineComponent({
type: Boolean,
default: false,
},
rows: {
type: Number,
default: -1,
default: 4,
},
modelValue: {
type: String,
@ -177,8 +175,6 @@ export default defineComponent({
// eslint-disable-next-line vue/return-in-computed-property
languageExtensions(): [LanguageSupport, ...Extension[]] {
switch (this.language) {
case 'json':
return [json()];
case 'javaScript':
return [javascript(), this.autocompletionExtension('javaScript')];
case 'python':

View file

@ -1,7 +1,6 @@
import { defineComponent } from 'vue';
import type { Diagnostic } from '@codemirror/lint';
import { linter as createLinter } from '@codemirror/lint';
import { jsonParseLinter } from '@codemirror/lang-json';
import type { EditorView } from '@codemirror/view';
import * as esprima from 'esprima-next';
import type { Node } from 'estree';
@ -21,8 +20,6 @@ export const linterExtension = defineComponent({
switch (language) {
case 'javaScript':
return createLinter(this.lintSource, { delay: DEFAULT_LINTER_DELAY_IN_MS });
case 'json':
return createLinter(jsonParseLinter());
}
return undefined;
},

View file

@ -54,7 +54,7 @@ export default defineComponent({
},
rows: {
type: Number,
default: -1,
default: 4,
},
disableExpressionColoring: {
type: Boolean,
@ -67,7 +67,8 @@ export default defineComponent({
},
data() {
return {
editor: {} as EditorView,
editor: null as EditorView | null,
editorState: null as EditorState | null,
};
},
computed: {
@ -111,8 +112,6 @@ export default defineComponent({
EditorView.updateListener.of((viewUpdate: ViewUpdate) => {
if (!viewUpdate.docChanged) return;
this.editorState = this.editor.state;
this.getHighlighter()?.removeColor(this.editor, this.htmlSegments);
this.getHighlighter()?.addColor(this.editor, this.resolvableSegments);

View file

@ -0,0 +1,95 @@
<template>
<div ref="jsEditor" class="ph-no-capture js-editor"></div>
</template>
<script lang="ts">
import { defineComponent } from 'vue';
import { acceptCompletion, autocompletion } from '@codemirror/autocomplete';
import { indentWithTab, history, redo, toggleComment, undo } from '@codemirror/commands';
import { foldGutter, indentOnInput } from '@codemirror/language';
import { javascript } from '@codemirror/lang-javascript';
import { lintGutter } from '@codemirror/lint';
import type { Extension } from '@codemirror/state';
import { EditorState } from '@codemirror/state';
import type { ViewUpdate } from '@codemirror/view';
import {
dropCursor,
EditorView,
highlightActiveLine,
highlightActiveLineGutter,
keymap,
lineNumbers,
} from '@codemirror/view';
import { codeNodeEditorTheme } from '../CodeNodeEditor/theme';
export default defineComponent({
name: 'JsEditor',
props: {
modelValue: {
type: String,
required: true,
},
isReadOnly: {
type: Boolean,
default: false,
},
rows: {
type: Number,
default: 4,
},
},
data() {
return {
editor: null as EditorView | null,
editorState: null as EditorState | null,
};
},
computed: {
doc(): string {
return this.editor?.state.doc.toString() ?? '';
},
extensions(): Extension[] {
const { isReadOnly } = this;
const extensions: Extension[] = [
javascript(),
lineNumbers(),
EditorView.lineWrapping,
EditorState.readOnly.of(isReadOnly),
EditorView.editable.of(!isReadOnly),
codeNodeEditorTheme({ isReadOnly, customMinHeight: this.rows }),
];
if (!isReadOnly) {
extensions.push(
history(),
keymap.of([
{ key: 'Mod-z', run: undo },
{ key: 'Mod-Shift-z', run: redo },
{ key: 'Mod-/', run: toggleComment },
{ key: 'Tab', run: acceptCompletion },
indentWithTab,
]),
lintGutter(),
autocompletion(),
indentOnInput(),
highlightActiveLine(),
highlightActiveLineGutter(),
foldGutter(),
dropCursor(),
EditorView.updateListener.of((viewUpdate: ViewUpdate) => {
if (!viewUpdate.docChanged || !this.editor) return;
this.$emit('update:modelValue', this.editor?.state.doc.toString());
}),
);
}
return extensions;
},
},
mounted() {
const state = EditorState.create({ doc: this.modelValue, extensions: this.extensions });
const parent = this.$refs.jsEditor as HTMLDivElement;
this.editor = new EditorView({ parent, state });
this.editorState = this.editor.state;
},
});
</script>

View file

@ -0,0 +1,95 @@
<template>
<div ref="jsonEditor" class="ph-no-capture json-editor"></div>
</template>
<script lang="ts">
import { defineComponent } from 'vue';
import { autocompletion } from '@codemirror/autocomplete';
import { indentWithTab, history, redo, undo } from '@codemirror/commands';
import { bracketMatching, foldGutter, indentOnInput } from '@codemirror/language';
import { json, jsonParseLinter } from '@codemirror/lang-json';
import { lintGutter, linter as createLinter } from '@codemirror/lint';
import type { Extension } from '@codemirror/state';
import { EditorState } from '@codemirror/state';
import type { ViewUpdate } from '@codemirror/view';
import {
dropCursor,
EditorView,
highlightActiveLine,
highlightActiveLineGutter,
keymap,
lineNumbers,
} from '@codemirror/view';
import { codeNodeEditorTheme } from '../CodeNodeEditor/theme';
export default defineComponent({
name: 'JsonEditor',
props: {
modelValue: {
type: String,
required: true,
},
isReadOnly: {
type: Boolean,
default: false,
},
rows: {
type: Number,
default: 4,
},
},
data() {
return {
editor: null as EditorView | null,
editorState: null as EditorState | null,
};
},
computed: {
doc(): string {
return this.editor?.state.doc.toString();
},
extensions(): Extension[] {
const { isReadOnly } = this;
const extensions: Extension[] = [
json(),
lineNumbers(),
EditorView.lineWrapping,
EditorState.readOnly.of(isReadOnly),
EditorView.editable.of(!isReadOnly),
codeNodeEditorTheme({ isReadOnly, customMinHeight: this.rows }),
];
if (!isReadOnly) {
extensions.push(
history(),
keymap.of([
indentWithTab,
{ key: 'Mod-z', run: undo },
{ key: 'Mod-Shift-z', run: redo },
]),
createLinter(jsonParseLinter()),
lintGutter(),
autocompletion(),
indentOnInput(),
highlightActiveLine(),
highlightActiveLineGutter(),
foldGutter(),
dropCursor(),
bracketMatching(),
EditorView.updateListener.of((viewUpdate: ViewUpdate) => {
if (!viewUpdate.docChanged || !this.editor) return;
this.$emit('update:modelValue', this.editor?.state.doc.toString());
}),
);
}
return extensions;
},
},
mounted() {
const state = EditorState.create({ doc: this.modelValue, extensions: this.extensions });
const parent = this.$refs.jsonEditor as HTMLDivElement;
this.editor = new EditorView({ parent, state });
this.editorState = this.editor.state;
},
});
</script>

View file

@ -124,14 +124,18 @@
@update:modelValue="valueChangedDebounced"
/>
<CodeNodeEditor
v-else-if="editorType === 'json' && !isExecuteWorkflowNode(node)"
:mode="node.parameters.mode"
<JsEditor
v-else-if="editorType === 'jsEditor'"
:model-value="modelValue"
:is-read-only="isReadOnly"
:rows="getArgument('rows')"
@update:modelValue="valueChangedDebounced"
/>
<JsonEditor
v-else-if="parameter.type === 'json'"
:model-value="modelValue"
:default-value="parameter.default"
:language="editorLanguage"
:is-read-only="isReadOnly"
:ai-button-enabled="false"
:rows="getArgument('rows')"
@update:modelValue="valueChangedDebounced"
/>
@ -396,18 +400,15 @@ import ExpressionParameterInput from '@/components/ExpressionParameterInput.vue'
import TextEdit from '@/components/TextEdit.vue';
import CodeNodeEditor from '@/components/CodeNodeEditor/CodeNodeEditor.vue';
import HtmlEditor from '@/components/HtmlEditor/HtmlEditor.vue';
import JsEditor from '@/components/JsEditor/JsEditor.vue';
import JsonEditor from '@/components/JsonEditor/JsonEditor.vue';
import SqlEditor from '@/components/SqlEditor/SqlEditor.vue';
import { workflowHelpers } from '@/mixins/workflowHelpers';
import { hasExpressionMapping, isValueExpression } from '@/utils/nodeTypesUtils';
import { isResourceLocatorValue } from '@/utils/typeGuards';
import {
CUSTOM_API_CALL_KEY,
EXECUTE_WORKFLOW_NODE_TYPE,
HTML_NODE_TYPE,
NODES_USING_CODE_NODE_EDITOR,
} from '@/constants';
import { CUSTOM_API_CALL_KEY, HTML_NODE_TYPE, NODES_USING_CODE_NODE_EDITOR } from '@/constants';
import type { PropType } from 'vue';
import { debounceHelper } from '@/mixins/debounce';
@ -432,6 +433,8 @@ export default defineComponent({
components: {
CodeNodeEditor,
HtmlEditor,
JsEditor,
JsonEditor,
SqlEditor,
ExpressionEdit,
ExpressionParameterInput,
@ -1119,9 +1122,6 @@ export default defineComponent({
isHtmlNode(node: INodeUi): boolean {
return node.type === HTML_NODE_TYPE;
},
isExecuteWorkflowNode(node: INodeUi): boolean {
return node.type === EXECUTE_WORKFLOW_NODE_TYPE;
},
rgbaToHex(value: string): string | null {
// Convert rgba to hex from: https://stackoverflow.com/questions/5623838/rgb-to-hex-and-hex-to-rgb
const valueMatch = value.match(/^rgba\((\d+),\s*(\d+),\s*(\d+),\s*(\d+(\.\d+)?)\)$/);

View file

@ -236,9 +236,8 @@
<div v-else-if="editMode.enabled" :class="$style.editMode">
<div :class="[$style.editModeBody, 'ignore-key-press']">
<CodeNodeEditor
<JsonEditor
:model-value="editMode.value"
language="json"
@update:modelValue="ndvStore.setOutputPanelEditModeValue($event)"
/>
</div>
@ -604,11 +603,11 @@ import {
import BinaryDataDisplay from '@/components/BinaryDataDisplay.vue';
import NodeErrorView from '@/components/Error/NodeErrorView.vue';
import JsonEditor from '@/components/JsonEditor/JsonEditor.vue';
import { genericHelpers } from '@/mixins/genericHelpers';
import { pinData } from '@/mixins/pinData';
import type { PinDataSource } from '@/mixins/pinData';
import CodeNodeEditor from '@/components/CodeNodeEditor/CodeNodeEditor.vue';
import { dataPinningEventBus } from '@/event-bus';
import { clearJsonKey, isEmpty } from '@/utils/typesUtils';
import { executionDataToJson } from '@/utils/nodeTypesUtils';
@ -636,7 +635,7 @@ export default defineComponent({
components: {
BinaryDataDisplay,
NodeErrorView,
CodeNodeEditor,
JsonEditor,
RunDataTable,
RunDataJson,
RunDataSchema,

View file

@ -60,6 +60,7 @@ const SQL_DIALECTS = {
type SQLEditorData = {
editor: EditorView | null;
editorState: EditorState | null;
isFocused: boolean;
skipSegments: string[];
expressionsDocsUrl: string;
@ -89,12 +90,13 @@ export default defineComponent({
},
rows: {
type: Number,
default: -1,
default: 4,
},
},
data(): SQLEditorData {
return {
editor: null,
editorState: null,
expressionsDocsUrl: EXPRESSIONS_DOCS_URL,
isFocused: false,
skipSegments: ['Statement', 'CompositeIdentifier', 'Parens'],
@ -132,7 +134,6 @@ export default defineComponent({
}),
lineNumbers(),
EditorView.lineWrapping,
EditorState.readOnly.of(this.isReadOnly),
EditorView.domEventHandlers({
focus: () => {
this.isFocused = true;
@ -162,8 +163,6 @@ export default defineComponent({
EditorView.updateListener.of((viewUpdate: ViewUpdate) => {
if (!viewUpdate.docChanged || !this.editor) return;
this.editorState = this.editor.state;
highlighter.removeColor(this.editor as EditorView, this.plaintextSegments);
highlighter.addColor(this.editor as EditorView, this.resolvableSegments);

View file

@ -0,0 +1,19 @@
import { createTestingPinia } from '@pinia/testing';
import JsEditor from '@/components/JsEditor/JsEditor.vue';
import { renderComponent } from '@/__tests__/render';
describe('JsEditor', () => {
const renderEditor = (jsonString: string) =>
renderComponent(JsEditor, {
global: {
plugins: [createTestingPinia()],
},
props: { modelValue: jsonString },
});
it('renders simple js', async () => {
const modelValue = 'return [1, 2, 3]';
const result = renderEditor(modelValue);
expect(result.container.querySelector('.cm-content')?.textContent).toEqual(modelValue);
});
});

View file

@ -0,0 +1,30 @@
import { createTestingPinia } from '@pinia/testing';
import JsonEditor from '@/components/JsonEditor/JsonEditor.vue';
import { renderComponent } from '@/__tests__/render';
describe('JsonEditor', () => {
const renderEditor = (jsonString: string) =>
renderComponent(JsonEditor, {
global: {
plugins: [createTestingPinia()],
},
props: { modelValue: jsonString },
});
it('renders simple json', async () => {
const modelValue = '{ "testing": [true, 5] }';
const result = renderEditor(modelValue);
expect(result.container.querySelector('.cm-content')?.textContent).toEqual(modelValue);
});
it('renders multiline json', async () => {
const modelValue = '{\n\t"testing": [true, 5]\n}';
const result = renderEditor(modelValue);
const gutter = result.container.querySelector('.cm-gutters');
expect(gutter?.querySelectorAll('.cm-lineNumbers .cm-gutterElement').length).toEqual(4);
const content = result.container.querySelector('.cm-content');
const lines = [...content!.querySelectorAll('.cm-line').values()].map((l) => l.textContent);
expect(lines).toEqual(['{', '\t"testing": [true, 5]', '}']);
});
});

View file

@ -68,10 +68,6 @@ const JSON_PARAM: INodeProperties = {
displayName: 'JSON Payload',
name: 'payloadJson',
type: 'json',
typeOptions: {
alwaysOpenEditWindow: true,
editor: 'code',
},
default: '',
};

View file

@ -55,14 +55,14 @@ const versionDescription: INodeTypeDescription = {
displayName: 'Allowed Mentions',
name: 'allowedMentions',
type: 'json',
typeOptions: { alwaysOpenEditWindow: true, editor: 'code' },
typeOptions: { alwaysOpenEditWindow: true },
default: '',
},
{
displayName: 'Attachments',
name: 'attachments',
type: 'json',
typeOptions: { alwaysOpenEditWindow: true, editor: 'code' },
typeOptions: { alwaysOpenEditWindow: true },
default: '',
},
{
@ -75,14 +75,14 @@ const versionDescription: INodeTypeDescription = {
displayName: 'Components',
name: 'components',
type: 'json',
typeOptions: { alwaysOpenEditWindow: true, editor: 'code' },
typeOptions: { alwaysOpenEditWindow: true },
default: '',
},
{
displayName: 'Embeds',
name: 'embeds',
type: 'json',
typeOptions: { alwaysOpenEditWindow: true, editor: 'code' },
typeOptions: { alwaysOpenEditWindow: true },
default: '',
},
{
@ -95,7 +95,7 @@ const versionDescription: INodeTypeDescription = {
displayName: 'JSON Payload',
name: 'payloadJson',
type: 'json',
typeOptions: { alwaysOpenEditWindow: true, editor: 'code' },
typeOptions: { alwaysOpenEditWindow: true },
default: '',
},
{

View file

@ -416,11 +416,9 @@ export const embedsFixedCollection: INodeProperties = {
{
displayName: 'Value',
name: 'json',
type: 'string',
type: 'json',
default: '={}',
typeOptions: {
editor: 'json',
editorLanguage: 'json',
rows: 2,
},
displayOptions: {

View file

@ -111,9 +111,8 @@ export class ExecuteWorkflow implements INodeType {
{
displayName: 'Workflow JSON',
name: 'workflowJson',
type: 'string',
type: 'json',
typeOptions: {
editor: 'json',
rows: 10,
},
displayOptions: {

View file

@ -1,10 +1,6 @@
import { readFile as fsReadFile } from 'fs/promises';
import {
NodeOperationError,
type IExecuteFunctions,
type IExecuteWorkflowInfo,
jsonParse,
} from 'n8n-workflow';
import { NodeOperationError, jsonParse } from 'n8n-workflow';
import type { IWorkflowBase, IExecuteFunctions, IExecuteWorkflowInfo } from 'n8n-workflow';
export async function getWorkflowInfo(this: IExecuteFunctions, source: string, itemIndex = 0) {
const workflowInfo: IExecuteWorkflowInfo = {};
@ -33,8 +29,7 @@ export async function getWorkflowInfo(this: IExecuteFunctions, source: string, i
workflowInfo.code = jsonParse(workflowJson);
} else if (source === 'parameter') {
// Read workflow from parameter
const workflowJson = this.getNodeParameter('workflowJson', itemIndex) as string;
workflowInfo.code = jsonParse(workflowJson);
workflowInfo.code = this.getNodeParameter('workflowJson', itemIndex) as IWorkflowBase;
} else if (source === 'url') {
// Read workflow from url
const workflowUrl = this.getNodeParameter('workflowUrl', itemIndex) as string;

View file

@ -40,7 +40,7 @@ export class Function implements INodeType {
typeOptions: {
alwaysOpenEditWindow: true,
codeAutocomplete: 'function',
editor: 'code',
editor: 'jsEditor',
rows: 10,
},
type: 'string',

View file

@ -40,7 +40,7 @@ export class FunctionItem implements INodeType {
typeOptions: {
alwaysOpenEditWindow: true,
codeAutocomplete: 'functionItem',
editor: 'code',
editor: 'jsEditor',
rows: 10,
},
type: 'string',

View file

@ -596,7 +596,7 @@ export class ItemListsV1 implements INodeType {
type: 'string',
typeOptions: {
alwaysOpenEditWindow: true,
editor: 'code',
editor: 'jsEditor',
rows: 10,
},
default: `// The two items to compare are in the variables a and b

View file

@ -598,7 +598,7 @@ export class ItemListsV2 implements INodeType {
type: 'string',
typeOptions: {
alwaysOpenEditWindow: true,
editor: 'code',
editor: 'jsEditor',
rows: 10,
},
default: `// The two items to compare are in the variables a and b

View file

@ -96,7 +96,7 @@ const properties: INodeProperties[] = [
type: 'string',
typeOptions: {
alwaysOpenEditWindow: true,
editor: 'code',
editor: 'jsEditor',
rows: 10,
},
default: `// The two items to compare are in the variables a and b

View file

@ -115,8 +115,6 @@ export class RespondToWebhook implements INodeType {
},
default: '{\n "myField": "value"\n}',
typeOptions: {
editor: 'json',
editorLanguage: 'json',
rows: 4,
},
description: 'The HTTP response JSON data',

View file

@ -139,11 +139,9 @@ const properties: INodeProperties[] = [
{
displayName: 'Value',
name: 'objectValue',
type: 'string',
type: 'json',
default: '={}',
typeOptions: {
editor: 'json',
editorLanguage: 'json',
rows: 2,
},
displayOptions: {

View file

@ -15,10 +15,8 @@ const properties: INodeProperties[] = [
{
displayName: 'JSON Output',
name: 'jsonOutput',
type: 'string',
type: 'json',
typeOptions: {
editor: 'json',
editorLanguage: 'json',
rows: 5,
},
default: '{\n "my_field_1": "value",\n "my_field_2": 1\n}',

View file

@ -13,13 +13,12 @@ const properties: INodeProperties[] = [
{
displayName: 'Query',
name: 'queryJson',
type: 'string',
type: 'json',
required: true,
default: '=[\n {\n "_name": "listOrganisation"\n }\n]',
description: 'Search for objects with filtering and sorting capabilities',
hint: 'The query should be an array of operations with the required selection and optional filtering, sorting, and pagination. See <a href="https://docs.strangebee.com/thehive/api-docs/#operation/Query%20API" target="_blank">Query API</a> for more information.',
typeOptions: {
editor: 'json',
rows: 10,
},
},

View file

@ -106,7 +106,7 @@ export class Sort implements INodeType {
type: 'string',
typeOptions: {
alwaysOpenEditWindow: true,
editor: 'code',
editor: 'jsEditor',
rows: 10,
},
default: `// The two items to compare are in the variables a and b

View file

@ -3,7 +3,7 @@ export const WAIT_TIME_UNLIMITED = '3000-01-01T00:00:00.000Z';
export const LOG_LEVELS = ['silent', 'error', 'warn', 'info', 'debug', 'verbose'] as const;
export const CODE_LANGUAGES = ['javaScript', 'json', 'python'] as const;
export const CODE_LANGUAGES = ['javaScript', 'python'] as const;
export const CODE_EXECUTION_MODES = ['runOnceForAllItems', 'runOnceForEachItem'] as const;
/**

View file

@ -1078,7 +1078,7 @@ export type NodePropertyTypes =
export type CodeAutocompleteTypes = 'function' | 'functionItem';
export type EditorType = 'code' | 'codeNodeEditor' | 'htmlEditor' | 'sqlEditor' | 'json';
export type EditorType = 'codeNodeEditor' | 'jsEditor' | 'htmlEditor' | 'sqlEditor';
export type CodeNodeEditorLanguage = (typeof CODE_LANGUAGES)[number];
export type CodeExecutionMode = (typeof CODE_EXECUTION_MODES)[number];
export type SQLDialect =
@ -1105,7 +1105,6 @@ export interface INodePropertyTypeOptions {
alwaysOpenEditWindow?: boolean; // Supported by: json
codeAutocomplete?: CodeAutocompleteTypes; // Supported by: string
editor?: EditorType; // Supported by: string
editorLanguage?: CodeNodeEditorLanguage; // Supported by: string in combination with editor: codeNodeEditor
sqlDialect?: SQLDialect; // Supported by: sqlEditor
loadOptionsDependsOn?: string[]; // Supported by: options
loadOptionsMethod?: string; // Supported by: options