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:
@@ -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>
|
||||
|
||||
@@ -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 },
|
||||
]),
|
||||
),
|
||||
];
|
||||
|
||||
@@ -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)',
|
||||
},
|
||||
]),
|
||||
),
|
||||
];
|
||||
|
||||
Reference in New Issue
Block a user