feat(editor): Add fullscreen view to code editor (#8084)

## Summary

<img width="1240" alt="image"
src="https://github.com/n8n-io/n8n/assets/8850410/2819f4ce-c343-431a-8a88-a1bc9c4b572a">
<img width="2649" alt="image"
src="https://github.com/n8n-io/n8n/assets/8850410/36862aaf-cc4c-4668-bdc8-cf5a6f00babe">

1. Add code node and open it
3. Click the fullscreen button in the bottom right
4. A fullscreen dialog should appear and allow editing the code
5. Changes made in the fullscreen dialog should be applied to the
original code editor when closed

It should work the same way for HTML/SQL/JSON editors

⚠️ Modal layout was updated so that modals/dialogs are centered, try to
test some modals

## Related tickets and issues
https://linear.app/n8n/issue/NODE-1009/add-fullscreen-view-to-code-node



## Review / Merge checklist
- [ ] PR title and summary are descriptive. **Remember, the title
automatically goes into the changelog. Use `(no-changelog)` otherwise.**
([conventions](https://github.com/n8n-io/n8n/blob/master/.github/pull_request_title_conventions.md))
- [ ] [Docs updated](https://github.com/n8n-io/n8n-docs) or follow-up
ticket created.
- [ ] Tests included.
> A bug is not considered fixed, unless a test is added to prevent it
from happening again.
   > A feature is not complete without tests.

---------

Co-authored-by: Giulio Andreini <andreini@netseven.it>
This commit is contained in:
Elias Meire
2024-01-04 17:23:24 +01:00
committed by GitHub
parent 8f22a265d6
commit 071e6d6b6e
28 changed files with 617 additions and 376 deletions

View File

@@ -17,7 +17,11 @@
name="code"
data-test-id="code-node-tab-code"
>
<div ref="codeNodeEditor" class="code-node-editor-input ph-no-capture code-editor-tabs" />
<div
ref="codeNodeEditor"
:class="['ph-no-capture', 'code-editor-tabs', $style.editorInput]"
/>
<slot name="suffix" />
</el-tab-pane>
<el-tab-pane
:label="$locale.baseText('codeNodeEditor.tabs.askAi')"
@@ -35,7 +39,10 @@
</el-tab-pane>
</el-tabs>
<!-- If AskAi not enabled, there's no point in rendering tabs -->
<div v-else ref="codeNodeEditor" class="code-node-editor-input ph-no-capture" />
<div v-else :class="$style.fillHeight">
<div ref="codeNodeEditor" :class="['ph-no-capture', $style.fillHeight]" />
<slot name="suffix" />
</div>
</div>
</template>
@@ -82,6 +89,10 @@ export default defineComponent({
type: Boolean,
default: false,
},
fillParent: {
type: Boolean,
default: false,
},
mode: {
type: String as PropType<CodeExecutionMode>,
validator: (value: CodeExecutionMode): boolean => CODE_EXECUTION_MODES.includes(value),
@@ -193,7 +204,12 @@ export default defineComponent({
...readOnlyEditorExtensions,
EditorState.readOnly.of(isReadOnly),
EditorView.editable.of(!isReadOnly),
codeNodeEditorTheme({ isReadOnly, customMinHeight: this.rows }),
codeNodeEditorTheme({
isReadOnly,
maxHeight: this.fillParent ? '100%' : '40vh',
minHeight: '20vh',
rows: this.rows,
}),
];
if (!isReadOnly) {
@@ -384,15 +400,9 @@ export default defineComponent({
<style lang="scss" module>
.code-node-editor-container {
position: relative;
& > div {
height: 100%;
}
}
.ask-ai-button {
position: absolute;
top: var(--spacing-2xs);
right: var(--spacing-2xs);
.fillHeight {
height: 100%;
}
</style>

View File

@@ -6,12 +6,14 @@ import {
highlightSpecialChars,
keymap,
lineNumbers,
type KeyBinding,
} from '@codemirror/view';
import { bracketMatching, foldGutter, indentOnInput } from '@codemirror/language';
import { acceptCompletion } from '@codemirror/autocomplete';
import { acceptCompletion, selectedCompletion } from '@codemirror/autocomplete';
import {
history,
indentWithTab,
indentLess,
indentMore,
insertNewlineAndIndent,
toggleComment,
redo,
@@ -19,7 +21,7 @@ import {
undo,
} from '@codemirror/commands';
import { lintGutter } from '@codemirror/lint';
import type { Extension } from '@codemirror/state';
import { type Extension, Prec } from '@codemirror/state';
import { codeInputHandler } from '@/plugins/codemirror/inputHandlers/code.inputHandler';
@@ -29,6 +31,42 @@ export const readOnlyEditorExtensions: readonly Extension[] = [
highlightSpecialChars(),
];
export const tabKeyMap: KeyBinding[] = [
{
any(editor, event) {
if (event.key === 'Tab' || (event.key === 'Escape' && selectedCompletion(editor.state))) {
event.stopPropagation();
}
return false;
},
},
{
key: 'Tab',
run: (editor) => {
if (selectedCompletion(editor.state)) {
return acceptCompletion(editor);
}
return indentMore(editor);
},
},
{ key: 'Shift-Tab', run: indentLess },
];
export const enterKeyMap: KeyBinding[] = [
{
key: 'Enter',
run: (editor) => {
if (selectedCompletion(editor.state)) {
return acceptCompletion(editor);
}
return insertNewlineAndIndent(editor);
},
},
];
export const writableEditorExtensions: readonly Extension[] = [
history(),
lintGutter(),
@@ -39,14 +77,14 @@ export const writableEditorExtensions: readonly Extension[] = [
bracketMatching(),
highlightActiveLine(),
highlightActiveLineGutter(),
keymap.of([
{ key: 'Enter', run: insertNewlineAndIndent },
{ key: 'Tab', run: acceptCompletion },
{ key: 'Enter', run: acceptCompletion },
{ key: 'Mod-/', run: toggleComment },
{ key: 'Mod-z', run: undo },
{ key: 'Mod-Shift-z', run: redo },
{ key: 'Backspace', run: deleteCharBackward, shift: deleteCharBackward },
indentWithTab,
]),
Prec.highest(
keymap.of([
...tabKeyMap,
...enterKeyMap,
{ key: 'Mod-/', run: toggleComment },
{ key: 'Mod-z', run: undo },
{ key: 'Mod-Shift-z', run: redo },
{ key: 'Backspace', run: deleteCharBackward, shift: deleteCharBackward },
]),
),
];

View File

@@ -29,14 +29,18 @@ const BASE_STYLING = {
interface ThemeSettings {
isReadOnly?: boolean;
customMaxHeight?: string;
customMinHeight?: number;
maxHeight?: string;
minHeight?: string;
rows?: number;
highlightColors?: 'default' | 'html';
}
export const codeNodeEditorTheme = ({
isReadOnly,
customMaxHeight,
customMinHeight,
minHeight,
maxHeight,
rows,
highlightColors,
}: ThemeSettings) => [
EditorView.theme({
'&': {
@@ -85,11 +89,13 @@ export const codeNodeEditorTheme = ({
},
'.cm-scroller': {
overflow: 'auto',
maxHeight: customMaxHeight ?? '100%',
maxHeight: maxHeight ?? '100%',
...(isReadOnly
? {}
: { minHeight: customMinHeight ? `${Number(customMinHeight) * 1.3}em` : '10em' }),
: { minHeight: rows && rows !== -1 ? `${Number(rows + 1) * 1.3}em` : 'auto' }),
},
'.cm-gutter,.cm-content': {
minHeight: rows && rows !== -1 ? 'auto' : minHeight ?? 'calc(35vh - var(--spacing-2xl))',
},
'.cm-diagnosticAction': {
backgroundColor: BASE_STYLING.diagnosticButton.backgroundColor,
@@ -106,47 +112,97 @@ export const codeNodeEditorTheme = ({
color: 'var(--color-text-base)',
},
}),
syntaxHighlighting(
HighlightStyle.define([
{
tag: tags.comment,
color: 'var(--color-code-tags-comment)',
},
{
tag: [tags.string, tags.special(tags.brace)],
color: 'var(--color-code-tags-string)',
},
{
tag: [tags.number, tags.self, tags.bool, tags.null],
color: 'var(--color-code-tags-primitive)',
},
{
tag: tags.keyword,
color: 'var(--color-code-tags-keyword)',
},
{
tag: tags.operator,
color: 'var(--color-code-tags-operator)',
},
{
tag: [
tags.variableName,
tags.propertyName,
tags.attributeName,
tags.regexp,
tags.className,
tags.typeName,
],
color: 'var(--color-code-tags-variable)',
},
{
tag: [
tags.definition(tags.typeName),
tags.definition(tags.propertyName),
tags.function(tags.variableName),
],
color: 'var(--color-code-tags-definition)',
},
]),
),
highlightColors === 'html'
? syntaxHighlighting(
HighlightStyle.define([
{ tag: tags.keyword, color: '#c678dd' },
{
tag: [tags.name, tags.deleted, tags.character, tags.propertyName, tags.macroName],
color: '#e06c75',
},
{ tag: [tags.function(tags.variableName), tags.labelName], color: '#61afef' },
{
tag: [tags.color, tags.constant(tags.name), tags.standard(tags.name)],
color: '#d19a66',
},
{ tag: [tags.definition(tags.name), tags.separator], color: '#abb2bf' },
{
tag: [
tags.typeName,
tags.className,
tags.number,
tags.changed,
tags.annotation,
tags.modifier,
tags.self,
tags.namespace,
],
color: '#e06c75',
},
{
tag: [
tags.operator,
tags.operatorKeyword,
tags.url,
tags.escape,
tags.regexp,
tags.link,
tags.special(tags.string),
],
color: '#56b6c2',
},
{ tag: [tags.meta, tags.comment], color: '#7d8799' },
{ tag: tags.strong, fontWeight: 'bold' },
{ tag: tags.emphasis, fontStyle: 'italic' },
{ tag: tags.strikethrough, textDecoration: 'line-through' },
{ tag: tags.link, color: '#7d8799', textDecoration: 'underline' },
{ tag: tags.heading, fontWeight: 'bold', color: '#e06c75' },
{ tag: [tags.atom, tags.bool, tags.special(tags.variableName)], color: '#d19a66' },
{ tag: [tags.processingInstruction, tags.string, tags.inserted], color: '#98c379' },
{ tag: tags.invalid, color: 'red', 'font-weight': 'bold' },
]),
)
: syntaxHighlighting(
HighlightStyle.define([
{
tag: tags.comment,
color: 'var(--color-code-tags-comment)',
},
{
tag: [tags.string, tags.special(tags.brace)],
color: 'var(--color-code-tags-string)',
},
{
tag: [tags.number, tags.self, tags.bool, tags.null],
color: 'var(--color-code-tags-primitive)',
},
{
tag: tags.keyword,
color: 'var(--color-code-tags-keyword)',
},
{
tag: tags.operator,
color: 'var(--color-code-tags-operator)',
},
{
tag: [
tags.variableName,
tags.propertyName,
tags.attributeName,
tags.regexp,
tags.className,
tags.typeName,
],
color: 'var(--color-code-tags-variable)',
},
{
tag: [
tags.definition(tags.typeName),
tags.definition(tags.propertyName),
tags.function(tags.variableName),
],
color: 'var(--color-code-tags-definition)',
},
]),
),
];

View File

@@ -1,44 +1,48 @@
<template>
<div ref="htmlEditor"></div>
<div :class="$style.editor">
<div ref="htmlEditor"></div>
<slot name="suffix" />
</div>
</template>
<script lang="ts">
import { defineComponent } from 'vue';
import { format } from 'prettier';
import htmlParser from 'prettier/plugins/html';
import cssParser from 'prettier/plugins/postcss';
import jsParser from 'prettier/plugins/babel';
import * as estree from 'prettier/plugins/estree';
import { htmlLanguage, autoCloseTags, html } from 'codemirror-lang-html-n8n';
import { autocompletion } from '@codemirror/autocomplete';
import { indentWithTab, insertNewlineAndIndent, history, redo, undo } from '@codemirror/commands';
import { history, redo, undo } from '@codemirror/commands';
import {
LanguageSupport,
bracketMatching,
ensureSyntaxTree,
foldGutter,
indentOnInput,
LanguageSupport,
} from '@codemirror/language';
import type { Extension } from '@codemirror/state';
import { EditorState } from '@codemirror/state';
import { EditorState, Prec } from '@codemirror/state';
import type { ViewUpdate } from '@codemirror/view';
import {
dropCursor,
EditorView,
dropCursor,
highlightActiveLine,
highlightActiveLineGutter,
keymap,
lineNumbers,
} from '@codemirror/view';
import { autoCloseTags, html, htmlLanguage } from 'codemirror-lang-html-n8n';
import { format } from 'prettier';
import jsParser from 'prettier/plugins/babel';
import * as estree from 'prettier/plugins/estree';
import htmlParser from 'prettier/plugins/html';
import cssParser from 'prettier/plugins/postcss';
import { defineComponent } from 'vue';
import { htmlEditorEventBus } from '@/event-bus';
import { expressionManager } from '@/mixins/expressionManager';
import { n8nCompletionSources } from '@/plugins/codemirror/completions/addCompletions';
import { expressionInputHandler } from '@/plugins/codemirror/inputHandlers/expression.inputHandler';
import { highlighter } from '@/plugins/codemirror/resolvableHighlighter';
import { htmlEditorEventBus } from '@/event-bus';
import { expressionManager } from '@/mixins/expressionManager';
import { theme } from './theme';
import { nonTakenRanges } from './utils';
import { enterKeyMap, tabKeyMap } from '../CodeNodeEditor/baseExtensions';
import { codeNodeEditorTheme } from '../CodeNodeEditor/theme';
import type { Range, Section } from './types';
import { nonTakenRanges } from './utils';
export default defineComponent({
name: 'HtmlEditor',
@@ -52,6 +56,10 @@ export default defineComponent({
type: Boolean,
default: false,
},
fillParent: {
type: Boolean,
default: false,
},
rows: {
type: Number,
default: 4,
@@ -90,15 +98,21 @@ export default defineComponent({
this.disableExpressionCompletions ? html() : htmlWithCompletions(),
autoCloseTags,
expressionInputHandler(),
keymap.of([
indentWithTab,
{ key: 'Enter', run: insertNewlineAndIndent },
{ key: 'Mod-z', run: undo },
{ key: 'Mod-Shift-z', run: redo },
]),
Prec.highest(
keymap.of([
...tabKeyMap,
...enterKeyMap,
{ key: 'Mod-z', run: undo },
{ key: 'Mod-Shift-z', run: redo },
]),
),
indentOnInput(),
theme({
codeNodeEditorTheme({
isReadOnly: this.isReadOnly,
maxHeight: this.fillParent ? '100%' : '40vh',
minHeight: '20vh',
rows: this.rows,
highlightColors: 'html',
}),
lineNumbers(),
highlightActiveLineGutter(),
@@ -288,4 +302,12 @@ export default defineComponent({
});
</script>
<style lang="scss" module></style>
<style lang="scss" module>
.editor {
height: 100%;
& > div {
height: 100%;
}
}
</style>

View File

@@ -1,97 +0,0 @@
import { HighlightStyle, syntaxHighlighting } from '@codemirror/language';
import { EditorView } from '@codemirror/view';
import { tags } from '@lezer/highlight';
export const theme = ({ isReadOnly }: { isReadOnly: boolean }) => [
EditorView.theme({
'&': {
'font-size': '0.8em',
border: 'var(--border-base)',
borderRadius: 'var(--border-radius-base)',
backgroundColor: 'var(--color-code-background)',
color: 'var(--color-code-foreground)',
},
'.cm-content': {
fontFamily: "Menlo, Consolas, 'DejaVu Sans Mono', monospace !important",
caretColor: 'var(--color-code-caret)',
},
'.cm-cursor, .cm-dropCursor': {
borderLeftColor: 'var(--color-code-caret)',
},
'&.cm-editor': {
...(isReadOnly ? { backgroundColor: 'var(--color-code-background-readonly)' } : {}),
borderColor: 'var(--border-color-base)',
},
'&.cm-editor.cm-focused': {
outline: '0',
},
'&.cm-focused .cm-selectionBackgroundm .cm-selectionBackground, .cm-content ::selection': {
backgroundColor: 'var(--color-code-selection)',
},
'.cm-activeLine': {
backgroundColor: 'var(--color-code-lineHighlight)',
},
'.cm-activeLineGutter': {
backgroundColor: 'var(--color-code-lineHighlight)',
},
'.cm-gutters': {
backgroundColor: isReadOnly
? 'var(--color-code-background-readonly)'
: 'var(--color-code-gutterBackground)',
color: 'var(--color-code-gutterForeground)',
borderTopLeftRadius: 'var(--border-radius-base)',
borderBottomLeftRadius: 'var(--border-radius-base)',
borderRightColor: 'var(--border-color-base)',
},
'.cm-scroller': {
overflow: 'auto',
maxHeight: '350px',
},
}),
syntaxHighlighting(
HighlightStyle.define([
{ tag: tags.keyword, color: '#c678dd' },
{
tag: [tags.name, tags.deleted, tags.character, tags.propertyName, tags.macroName],
color: '#e06c75',
},
{ tag: [tags.function(tags.variableName), tags.labelName], color: '#61afef' },
{ tag: [tags.color, tags.constant(tags.name), tags.standard(tags.name)], color: '#d19a66' },
{ tag: [tags.definition(tags.name), tags.separator], color: '#abb2bf' },
{
tag: [
tags.typeName,
tags.className,
tags.number,
tags.changed,
tags.annotation,
tags.modifier,
tags.self,
tags.namespace,
],
color: '#e06c75',
},
{
tag: [
tags.operator,
tags.operatorKeyword,
tags.url,
tags.escape,
tags.regexp,
tags.link,
tags.special(tags.string),
],
color: '#56b6c2',
},
{ tag: [tags.meta, tags.comment], color: '#7d8799' },
{ tag: tags.strong, fontWeight: 'bold' },
{ tag: tags.emphasis, fontStyle: 'italic' },
{ tag: tags.strikethrough, textDecoration: 'line-through' },
{ tag: tags.link, color: '#7d8799', textDecoration: 'underline' },
{ tag: tags.heading, fontWeight: 'bold', color: '#e06c75' },
{ tag: [tags.atom, tags.bool, tags.special(tags.variableName)], color: '#d19a66' },
{ tag: [tags.processingInstruction, tags.string, tags.inserted], color: '#98c379' },
{ tag: tags.invalid, color: 'red', 'font-weight': 'bold' },
]),
),
];

View File

@@ -1,26 +1,30 @@
<template>
<div ref="jsEditor" class="ph-no-capture js-editor"></div>
<div :class="$style.editor">
<div ref="jsEditor" class="ph-no-capture js-editor"></div>
<slot name="suffix" />
</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 { autocompletion } from '@codemirror/autocomplete';
import { history, redo, toggleComment, undo } from '@codemirror/commands';
import { javascript } from '@codemirror/lang-javascript';
import { foldGutter, indentOnInput } from '@codemirror/language';
import { lintGutter } from '@codemirror/lint';
import type { Extension } from '@codemirror/state';
import { EditorState } from '@codemirror/state';
import { EditorState, Prec } from '@codemirror/state';
import type { ViewUpdate } from '@codemirror/view';
import {
dropCursor,
EditorView,
dropCursor,
highlightActiveLine,
highlightActiveLineGutter,
keymap,
lineNumbers,
} from '@codemirror/view';
import { defineComponent } from 'vue';
import { enterKeyMap, tabKeyMap } from '../CodeNodeEditor/baseExtensions';
import { codeNodeEditorTheme } from '../CodeNodeEditor/theme';
export default defineComponent({
@@ -34,6 +38,10 @@ export default defineComponent({
type: Boolean,
default: false,
},
fillParent: {
type: Boolean,
default: false,
},
rows: {
type: Number,
default: 4,
@@ -57,18 +65,25 @@ export default defineComponent({
EditorView.lineWrapping,
EditorState.readOnly.of(isReadOnly),
EditorView.editable.of(!isReadOnly),
codeNodeEditorTheme({ isReadOnly, customMinHeight: this.rows }),
codeNodeEditorTheme({
isReadOnly,
maxHeight: this.fillParent ? '100%' : '40vh',
minHeight: '20vh',
rows: 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,
]),
Prec.highest(
keymap.of([
...tabKeyMap,
...enterKeyMap,
{ key: 'Mod-z', run: undo },
{ key: 'Mod-Shift-z', run: redo },
{ key: 'Mod-/', run: toggleComment },
]),
),
lintGutter(),
autocompletion(),
indentOnInput(),
@@ -93,3 +108,13 @@ export default defineComponent({
},
});
</script>
<style lang="scss" module>
.editor {
height: 100%;
& > div {
height: 100%;
}
}
</style>

View File

@@ -1,26 +1,30 @@
<template>
<div ref="jsonEditor" class="ph-no-capture json-editor"></div>
<div :class="$style.editor">
<div ref="jsonEditor" class="ph-no-capture json-editor"></div>
<slot name="suffix" />
</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 { history, redo, undo } from '@codemirror/commands';
import { json, jsonParseLinter } from '@codemirror/lang-json';
import { lintGutter, linter as createLinter } from '@codemirror/lint';
import { bracketMatching, foldGutter, indentOnInput } from '@codemirror/language';
import { linter as createLinter, lintGutter } from '@codemirror/lint';
import type { Extension } from '@codemirror/state';
import { EditorState } from '@codemirror/state';
import { EditorState, Prec } from '@codemirror/state';
import type { ViewUpdate } from '@codemirror/view';
import {
dropCursor,
EditorView,
dropCursor,
highlightActiveLine,
highlightActiveLineGutter,
keymap,
lineNumbers,
} from '@codemirror/view';
import { defineComponent } from 'vue';
import { enterKeyMap, tabKeyMap } from '../CodeNodeEditor/baseExtensions';
import { codeNodeEditorTheme } from '../CodeNodeEditor/theme';
export default defineComponent({
@@ -34,6 +38,10 @@ export default defineComponent({
type: Boolean,
default: false,
},
fillParent: {
type: Boolean,
default: false,
},
rows: {
type: Number,
default: 4,
@@ -57,16 +65,24 @@ export default defineComponent({
EditorView.lineWrapping,
EditorState.readOnly.of(isReadOnly),
EditorView.editable.of(!isReadOnly),
codeNodeEditorTheme({ isReadOnly, customMinHeight: this.rows }),
codeNodeEditorTheme({
isReadOnly,
maxHeight: this.fillParent ? '100%' : '40vh',
minHeight: '20vh',
rows: this.rows,
}),
];
if (!isReadOnly) {
extensions.push(
history(),
keymap.of([
indentWithTab,
{ key: 'Mod-z', run: undo },
{ key: 'Mod-Shift-z', run: redo },
]),
Prec.highest(
keymap.of([
...tabKeyMap,
...enterKeyMap,
{ key: 'Mod-z', run: undo },
{ key: 'Mod-Shift-z', run: redo },
]),
),
createLinter(jsonParseLinter()),
lintGutter(),
autocompletion(),
@@ -93,3 +109,13 @@ export default defineComponent({
},
});
</script>
<style lang="scss" module>
.editor {
height: 100%;
& > div {
height: 100%;
}
}
</style>

View File

@@ -1166,7 +1166,7 @@ export default defineComponent({
.node-parameters-wrapper {
overflow-y: auto;
padding: 0 var(--spacing-m) 200px var(--spacing-m);
padding: 0 var(--spacing-m) var(--spacing-l) var(--spacing-m);
flex-grow: 1;
}

View File

@@ -15,7 +15,7 @@
:is-read-only="isReadOnly"
:redact-values="shouldRedactValue"
@closeDialog="closeExpressionEditDialog"
@update:modelValue="expressionUpdated"
@update:model-value="expressionUpdated"
></ExpressionEdit>
<div class="parameter-input ignore-key-press" :style="parameterInputWrapperStyle">
<ResourceLocator
@@ -34,7 +34,7 @@
:node="node"
:path="path"
:event-bus="eventBus"
@update:modelValue="valueChanged"
@update:model-value="valueChanged"
@modalOpenerClick="openExpressionEditorModal"
@focus="setFocus"
@blur="onBlur"
@@ -50,7 +50,7 @@
:path="path"
:additional-expression-data="additionalExpressionData"
:class="{ 'ph-no-capture': shouldRedactValue }"
@update:modelValue="expressionUpdated"
@update:model-value="expressionUpdated"
@modalOpenerClick="openExpressionEditorModal"
@focus="setFocus"
@blur="onBlur"
@@ -62,23 +62,60 @@
"
>
<el-dialog
v-if="codeEditDialogVisible"
:model-value="true"
:model-value="codeEditDialogVisible"
append-to-body
:close-on-click-modal="false"
width="80%"
:title="`${i18n.baseText('codeEdit.edit')} ${$locale
.nodeText()
.inputLabelDisplayName(parameter, path)}`"
:before-close="closeCodeEditDialog"
data-test-id="code-editor-fullscreen"
>
<div class="ignore-key-press">
<div :key="codeEditDialogVisible" class="ignore-key-press code-edit-dialog">
<CodeNodeEditor
v-if="editorType === 'codeNodeEditor'"
:model-value="modelValue"
:default-value="parameter.default"
:language="editorLanguage"
:is-read-only="isReadOnly"
@update:modelValue="expressionUpdated"
fill-parent
@update:model-value="valueChangedDebounced"
/>
<HtmlEditor
v-else-if="editorType === 'htmlEditor'"
:model-value="modelValue"
:is-read-only="isReadOnly"
:rows="getArgument('rows')"
:disable-expression-coloring="!isHtmlNode(node)"
:disable-expression-completions="!isHtmlNode(node)"
fill-parent
@update:model-value="valueChangedDebounced"
/>
<SqlEditor
v-else-if="editorType === 'sqlEditor'"
:model-value="modelValue"
:dialect="getArgument('sqlDialect')"
:is-read-only="isReadOnly"
:rows="getArgument('rows')"
fill-parent
@update:model-value="valueChangedDebounced"
/>
<JsEditor
v-else-if="editorType === 'jsEditor'"
:model-value="modelValue"
:is-read-only="isReadOnly"
:rows="getArgument('rows')"
fill-parent
@update:model-value="valueChangedDebounced"
/>
<JsonEditor
v-else-if="parameter.type === 'json'"
:model-value="modelValue"
:is-read-only="isReadOnly"
:rows="getArgument('rows')"
fill-parent
@update:model-value="valueChangedDebounced"
/>
</div>
</el-dialog>
@@ -90,11 +127,12 @@
:path="path"
:is-read-only="isReadOnly"
@closeDialog="closeTextEditDialog"
@update:modelValue="expressionUpdated"
@update:model-value="expressionUpdated"
></TextEdit>
<CodeNodeEditor
v-if="editorType === 'codeNodeEditor' && isCodeNode(node)"
:key="codeEditDialogVisible"
:mode="node.parameters.mode"
:model-value="modelValue"
:default-value="parameter.default"
@@ -102,43 +140,102 @@
:is-read-only="isReadOnly"
:rows="getArgument('rows')"
:ai-button-enabled="settingsStore.isCloudDeployment"
@update:modelValue="valueChangedDebounced"
/>
@update:model-value="valueChangedDebounced"
>
<template #suffix>
<n8n-icon
data-test-id="code-editor-fullscreen-button"
icon="external-link-alt"
size="xsmall"
class="textarea-modal-opener"
:title="$locale.baseText('parameterInput.openEditWindow')"
@click="displayEditDialog()"
/>
</template>
</CodeNodeEditor>
<HtmlEditor
v-else-if="editorType === 'htmlEditor'"
:key="codeEditDialogVisible"
:model-value="modelValue"
:is-read-only="isReadOnly"
:rows="getArgument('rows')"
:disable-expression-coloring="!isHtmlNode(node)"
:disable-expression-completions="!isHtmlNode(node)"
@update:modelValue="valueChangedDebounced"
/>
@update:model-value="valueChangedDebounced"
>
<template #suffix>
<n8n-icon
data-test-id="code-editor-fullscreen-button"
icon="external-link-alt"
size="xsmall"
class="textarea-modal-opener"
:title="$locale.baseText('parameterInput.openEditWindow')"
@click="displayEditDialog()"
/>
</template>
</HtmlEditor>
<SqlEditor
v-else-if="editorType === 'sqlEditor'"
:key="codeEditDialogVisible"
:model-value="modelValue"
:dialect="getArgument('sqlDialect')"
:is-read-only="isReadOnly"
:rows="getArgument('rows')"
@update:modelValue="valueChangedDebounced"
/>
@update:model-value="valueChangedDebounced"
>
<template #suffix>
<n8n-icon
data-test-id="code-editor-fullscreen-button"
icon="external-link-alt"
size="xsmall"
class="textarea-modal-opener"
:title="$locale.baseText('parameterInput.openEditWindow')"
@click="displayEditDialog()"
/>
</template>
</SqlEditor>
<JsEditor
v-else-if="editorType === 'jsEditor'"
:key="codeEditDialogVisible"
:model-value="modelValue"
:is-read-only="isReadOnly"
:rows="getArgument('rows')"
@update:modelValue="valueChangedDebounced"
/>
@update:model-value="valueChangedDebounced"
>
<template #suffix>
<n8n-icon
data-test-id="code-editor-fullscreen-button"
icon="external-link-alt"
size="xsmall"
class="textarea-modal-opener"
:title="$locale.baseText('parameterInput.openEditWindow')"
@click="displayEditDialog()"
/>
</template>
</JsEditor>
<JsonEditor
v-else-if="parameter.type === 'json'"
:key="codeEditDialogVisible"
:model-value="modelValue"
:is-read-only="isReadOnly"
:rows="getArgument('rows')"
@update:modelValue="valueChangedDebounced"
/>
@update:model-value="valueChangedDebounced"
>
<template #suffix>
<n8n-icon
data-test-id="code-editor-fullscreen-button"
icon="external-link-alt"
size="xsmall"
class="textarea-modal-opener"
:title="$locale.baseText('parameterInput.openEditWindow')"
@click="displayEditDialog()"
/>
</template>
</JsonEditor>
<div v-else-if="editorType" class="readonly-code clickable" @click="displayEditDialog()">
<CodeNodeEditor
@@ -161,7 +258,7 @@
:disabled="isReadOnly"
:title="displayTitle"
:placeholder="getPlaceholder()"
@update:modelValue="valueChanged($event) && onUpdateTextInput($event)"
@update:model-value="valueChanged($event) && onUpdateTextInput($event)"
@keydown.stop
@focus="setFocus"
@blur="onBlur"
@@ -194,7 +291,7 @@
:show-alpha="getArgument('showAlpha')"
@focus="setFocus"
@blur="onBlur"
@update:modelValue="valueChanged"
@update:model-value="valueChanged"
/>
<n8n-input
v-model="tempValue"
@@ -202,7 +299,7 @@
type="text"
:disabled="isReadOnly"
:title="displayTitle"
@update:modelValue="valueChanged"
@update:model-value="valueChanged"
@keydown.stop
@focus="setFocus"
@blur="onBlur"
@@ -226,7 +323,7 @@
"
:picker-options="dateTimePickerOptions"
:class="{ 'ph-no-capture': shouldRedactValue }"
@update:modelValue="valueChanged"
@update:model-value="valueChanged"
@focus="setFocus"
@blur="onBlur"
@keydown.stop
@@ -245,7 +342,7 @@
:class="{ 'ph-no-capture': shouldRedactValue }"
:title="displayTitle"
:placeholder="parameter.placeholder"
@update:modelValue="onUpdateTextInput"
@update:model-value="onUpdateTextInput"
@focus="setFocus"
@blur="onBlur"
@keydown.stop
@@ -262,7 +359,7 @@
:is-read-only="isReadOnly"
:display-title="displayTitle"
@credentialSelected="credentialSelected"
@update:modelValue="valueChanged"
@update:model-value="valueChanged"
@setFocus="setFocus"
@onBlur="onBlur"
>
@@ -283,7 +380,7 @@
:loading="remoteParameterOptionsLoading"
:disabled="isReadOnly || remoteParameterOptionsLoading"
:title="displayTitle"
@update:modelValue="valueChanged"
@update:model-value="valueChanged"
@keydown.stop
@focus="setFocus"
@blur="onBlur"
@@ -321,7 +418,7 @@
:disabled="isReadOnly || remoteParameterOptionsLoading"
:title="displayTitle"
:placeholder="i18n.baseText('parameterInput.select')"
@update:modelValue="valueChanged"
@update:model-value="valueChanged"
@keydown.stop
@focus="setFocus"
@blur="onBlur"
@@ -358,7 +455,7 @@
active-color="#13ce66"
:model-value="displayValue"
:disabled="isReadOnly"
@update:modelValue="valueChanged"
@update:model-value="valueChanged"
/>
</div>
@@ -1056,7 +1153,7 @@ export default defineComponent({
return;
}
if (this.editorType) {
if (this.editorType || this.parameter.type === 'json') {
this.codeEditDialogVisible = true;
} else {
this.textEditDialogVisible = true;
@@ -1418,4 +1515,12 @@ export default defineComponent({
.invalid {
border-color: var(--color-danger);
}
.code-edit-dialog {
height: 70vh;
.code-node-editor {
height: 100%;
}
}
</style>

View File

@@ -1,7 +1,9 @@
<template>
<div v-on-click-outside="onBlur" :class="$style.sqlEditor">
<div ref="sqlEditor" data-test-id="sql-editor-container"></div>
<slot name="suffix" />
<InlineExpressionEditorOutput
v-if="!fillParent"
:segments="segments"
:is-read-only="isReadOnly"
:visible="isFocused"
@@ -11,41 +13,42 @@
</template>
<script lang="ts">
import { defineComponent } from 'vue';
import { acceptCompletion, autocompletion, ifNotIn } from '@codemirror/autocomplete';
import { indentWithTab, history, redo, toggleComment, undo } from '@codemirror/commands';
import { bracketMatching, foldGutter, indentOnInput, LanguageSupport } from '@codemirror/language';
import InlineExpressionEditorOutput from '@/components/InlineExpressionEditor/InlineExpressionEditorOutput.vue';
import { EXPRESSIONS_DOCS_URL } from '@/constants';
import { codeNodeEditorEventBus } from '@/event-bus';
import { expressionManager } from '@/mixins/expressionManager';
import { n8nCompletionSources } from '@/plugins/codemirror/completions/addCompletions';
import { expressionInputHandler } from '@/plugins/codemirror/inputHandlers/expression.inputHandler';
import { highlighter } from '@/plugins/codemirror/resolvableHighlighter';
import { autocompletion, ifNotIn } from '@codemirror/autocomplete';
import { history, redo, toggleComment, undo } from '@codemirror/commands';
import { LanguageSupport, bracketMatching, foldGutter, indentOnInput } from '@codemirror/language';
import { type Extension, type Line, Prec } from '@codemirror/state';
import { EditorState } from '@codemirror/state';
import type { Line, Extension } from '@codemirror/state';
import type { ViewUpdate } from '@codemirror/view';
import {
dropCursor,
EditorView,
dropCursor,
highlightActiveLine,
highlightActiveLineGutter,
keymap,
lineNumbers,
} from '@codemirror/view';
import type { ViewUpdate } from '@codemirror/view';
import type { SQLDialect as SQLDialectType } from '@n8n/codemirror-lang-sql';
import {
MSSQL,
MySQL,
PostgreSQL,
StandardSQL,
MariaSQL,
SQLite,
Cassandra,
MSSQL,
MariaSQL,
MySQL,
PLSQL,
PostgreSQL,
SQLite,
StandardSQL,
keywordCompletionSource,
} from '@n8n/codemirror-lang-sql';
import type { SQLDialect as SQLDialectType } from '@n8n/codemirror-lang-sql';
import { defineComponent } from 'vue';
import { enterKeyMap, tabKeyMap } from '../CodeNodeEditor/baseExtensions';
import { codeNodeEditorTheme } from '../CodeNodeEditor/theme';
import { n8nCompletionSources } from '@/plugins/codemirror/completions/addCompletions';
import { expressionInputHandler } from '@/plugins/codemirror/inputHandlers/expression.inputHandler';
import { highlighter } from '@/plugins/codemirror/resolvableHighlighter';
import { expressionManager } from '@/mixins/expressionManager';
import InlineExpressionEditorOutput from '@/components/InlineExpressionEditor/InlineExpressionEditorOutput.vue';
import { EXPRESSIONS_DOCS_URL } from '@/constants';
import { codeNodeEditorEventBus } from '@/event-bus';
const SQL_DIALECTS = {
StandardSQL,
@@ -88,6 +91,10 @@ export default defineComponent({
type: Boolean,
default: false,
},
fillParent: {
type: Boolean,
default: false,
},
rows: {
type: Number,
default: 4,
@@ -129,8 +136,9 @@ export default defineComponent({
expressionInputHandler(),
codeNodeEditorTheme({
isReadOnly: this.isReadOnly,
customMaxHeight: '350px',
customMinHeight: this.rows,
maxHeight: this.fillParent ? '100%' : '40vh',
minHeight: '10vh',
rows: this.rows,
}),
lineNumbers(),
EditorView.lineWrapping,
@@ -146,13 +154,15 @@ export default defineComponent({
if (!this.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,
]),
Prec.highest(
keymap.of([
...tabKeyMap,
...enterKeyMap,
{ key: 'Mod-z', run: undo },
{ key: 'Mod-Shift-z', run: redo },
{ key: 'Mod-/', run: toggleComment },
]),
),
autocompletion(),
indentOnInput(),
highlightActiveLine(),
@@ -233,5 +243,10 @@ export default defineComponent({
<style module lang="scss">
.sqlEditor {
position: relative;
height: 100%;
& > div {
height: 100%;
}
}
</style>