fix(editor): Make sure code editors work correctly in fullscreen (#12597)
Some checks are pending
Test Master / install-and-build (push) Waiting to run
Test Master / Unit tests (18.x) (push) Blocked by required conditions
Test Master / Unit tests (20.x) (push) Blocked by required conditions
Test Master / Unit tests (22.4) (push) Blocked by required conditions
Test Master / Lint (push) Blocked by required conditions
Test Master / Notify Slack on failure (push) Blocked by required conditions

This commit is contained in:
Elias Meire 2025-01-14 18:16:41 +01:00 committed by GitHub
parent bfe3c5611a
commit aa1f3a7d98
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
6 changed files with 98 additions and 12 deletions

View file

@ -1,6 +1,10 @@
import { setCredentialValues } from '../composables/modals/credential-modal'; import { setCredentialValues } from '../composables/modals/credential-modal';
import { clickCreateNewCredential } from '../composables/ndv'; import { clickCreateNewCredential, setParameterSelectByContent } from '../composables/ndv';
import { MANUAL_TRIGGER_NODE_DISPLAY_NAME, NOTION_NODE_NAME } from '../constants'; import {
EDIT_FIELDS_SET_NODE_NAME,
MANUAL_TRIGGER_NODE_DISPLAY_NAME,
NOTION_NODE_NAME,
} from '../constants';
import { NDV, WorkflowPage } from '../pages'; import { NDV, WorkflowPage } from '../pages';
import { NodeCreator } from '../pages/features/node-creator'; import { NodeCreator } from '../pages/features/node-creator';
@ -359,15 +363,71 @@ describe('NDV', () => {
ndv.getters.nodeExecuteButton().should('be.visible'); ndv.getters.nodeExecuteButton().should('be.visible');
}); });
it('should allow editing code in fullscreen in the Code node', () => { it('should allow editing code in fullscreen in the code editors', () => {
// Code (JavaScript)
workflowPage.actions.addInitialNodeToCanvas('Code', { keepNdvOpen: true }); workflowPage.actions.addInitialNodeToCanvas('Code', { keepNdvOpen: true });
ndv.actions.openCodeEditorFullscreen(); ndv.actions.openCodeEditorFullscreen();
ndv.getters.codeEditorFullscreen().type('{selectall}').type('{backspace}').type('foo()'); ndv.getters.codeEditorFullscreen().type('{selectall}').type('{backspace}').type('foo()');
ndv.getters.codeEditorFullscreen().should('contain.text', 'foo()'); ndv.getters.codeEditorFullscreen().should('contain.text', 'foo()');
cy.wait(200); cy.wait(200); // allow change to emit before closing modal
ndv.getters.codeEditorDialog().find('.el-dialog__close').click(); ndv.getters.codeEditorDialog().find('.el-dialog__close').click();
ndv.getters.parameterInput('jsCode').get('.cm-content').should('contain.text', 'foo()'); ndv.getters.parameterInput('jsCode').get('.cm-content').should('contain.text', 'foo()');
ndv.actions.close();
// SQL
workflowPage.actions.addNodeToCanvas('Postgres', true, true, 'Execute a SQL query');
ndv.actions.openCodeEditorFullscreen();
ndv.getters
.codeEditorFullscreen()
.type('{selectall}')
.type('{backspace}')
.type('SELECT * FROM workflows');
ndv.getters.codeEditorFullscreen().should('contain.text', 'SELECT * FROM workflows');
cy.wait(200);
ndv.getters.codeEditorDialog().find('.el-dialog__close').click();
ndv.getters
.parameterInput('query')
.get('.cm-content')
.should('contain.text', 'SELECT * FROM workflows');
ndv.actions.close();
// HTML
workflowPage.actions.addNodeToCanvas('HTML', true, true, 'Generate HTML template');
ndv.actions.openCodeEditorFullscreen();
ndv.getters
.codeEditorFullscreen()
.type('{selectall}')
.type('{backspace}')
.type('<div>Hello World');
ndv.getters.codeEditorFullscreen().should('contain.text', '<div>Hello World</div>');
cy.wait(200);
ndv.getters.codeEditorDialog().find('.el-dialog__close').click();
ndv.getters
.parameterInput('html')
.get('.cm-content')
.should('contain.text', '<div>Hello World</div>');
ndv.actions.close();
// JSON
workflowPage.actions.addNodeToCanvas(EDIT_FIELDS_SET_NODE_NAME, true, true);
setParameterSelectByContent('mode', 'JSON');
ndv.actions.openCodeEditorFullscreen();
ndv.getters
.codeEditorFullscreen()
.type('{selectall}')
.type('{backspace}')
.type('{ "key": "value" }', { parseSpecialCharSequences: false });
ndv.getters.codeEditorFullscreen().should('contain.text', '{ "key": "value" }');
cy.wait(200);
ndv.getters.codeEditorDialog().find('.el-dialog__close').click();
ndv.getters
.parameterInput('jsonOutput')
.get('.cm-content')
.should('contain.text', '{ "key": "value" }');
}); });
it('should not retrieve remote options when a parameter value changes', () => { it('should not retrieve remote options when a parameter value changes', () => {

View file

@ -83,6 +83,7 @@ const {
editor: editorRef, editor: editorRef,
segments, segments,
readEditorValue, readEditorValue,
isDirty,
} = useExpressionEditor({ } = useExpressionEditor({
editorRef: htmlEditor, editorRef: htmlEditor,
editorValue, editorValue,
@ -230,6 +231,7 @@ onMounted(() => {
}); });
onBeforeUnmount(() => { onBeforeUnmount(() => {
if (isDirty.value) emit('update:model-value', readEditorValue());
htmlEditorEventBus.off('format-html', formatHtml); htmlEditorEventBus.off('format-html', formatHtml);
}); });
@ -246,7 +248,10 @@ async function onDrop(value: string, event: MouseEvent) {
<template #default="{ activeDrop, droppable }"> <template #default="{ activeDrop, droppable }">
<div <div
ref="htmlEditor" ref="htmlEditor"
:class="{ [$style.activeDrop]: activeDrop, [$style.droppable]: droppable }" :class="[
$style.fillHeight,
{ [$style.activeDrop]: activeDrop, [$style.droppable]: droppable },
]"
data-test-id="html-editor-container" data-test-id="html-editor-container"
></div ></div
></template> ></template>
@ -264,6 +269,10 @@ async function onDrop(value: string, event: MouseEvent) {
} }
} }
.fillHeight {
height: 100%;
}
.droppable { .droppable {
:global(.cm-editor) { :global(.cm-editor) {
border-color: var(--color-ndv-droppable-parameter); border-color: var(--color-ndv-droppable-parameter);

View file

@ -17,7 +17,7 @@ import {
import { editorKeymap } from '@/plugins/codemirror/keymap'; import { editorKeymap } from '@/plugins/codemirror/keymap';
import { n8nAutocompletion } from '@/plugins/codemirror/n8nLang'; import { n8nAutocompletion } from '@/plugins/codemirror/n8nLang';
import { computed, onMounted, ref, watch } from 'vue'; import { computed, onBeforeUnmount, onMounted, ref, watch } from 'vue';
import { codeEditorTheme } from '../CodeNodeEditor/theme'; import { codeEditorTheme } from '../CodeNodeEditor/theme';
import { mappingDropCursor } from '@/plugins/codemirror/dragAndDrop'; import { mappingDropCursor } from '@/plugins/codemirror/dragAndDrop';
@ -36,6 +36,7 @@ const emit = defineEmits<{
const jsonEditorRef = ref<HTMLDivElement>(); const jsonEditorRef = ref<HTMLDivElement>();
const editor = ref<EditorView | null>(null); const editor = ref<EditorView | null>(null);
const editorState = ref<EditorState | null>(null); const editorState = ref<EditorState | null>(null);
const isDirty = ref(false);
const extensions = computed(() => { const extensions = computed(() => {
const extensionsToApply: Extension[] = [ const extensionsToApply: Extension[] = [
@ -65,6 +66,7 @@ const extensions = computed(() => {
bracketMatching(), bracketMatching(),
mappingDropCursor(), mappingDropCursor(),
EditorView.updateListener.of((viewUpdate: ViewUpdate) => { EditorView.updateListener.of((viewUpdate: ViewUpdate) => {
isDirty.value = true;
if (!viewUpdate.docChanged || !editor.value) return; if (!viewUpdate.docChanged || !editor.value) return;
emit('update:modelValue', editor.value?.state.doc.toString()); emit('update:modelValue', editor.value?.state.doc.toString());
}), }),
@ -77,6 +79,12 @@ onMounted(() => {
createEditor(); createEditor();
}); });
onBeforeUnmount(() => {
if (!editor.value) return;
if (isDirty.value) emit('update:modelValue', editor.value.state.doc.toString());
editor.value.destroy();
});
watch( watch(
() => props.modelValue, () => props.modelValue,
(newValue: string) => { (newValue: string) => {

View file

@ -1108,7 +1108,7 @@ onUpdated(async () => {
> >
<div class="ignore-key-press-canvas code-edit-dialog"> <div class="ignore-key-press-canvas code-edit-dialog">
<CodeNodeEditor <CodeNodeEditor
v-if="editorType === 'codeNodeEditor'" v-if="editorType === 'codeNodeEditor' && codeEditDialogVisible"
:id="parameterId" :id="parameterId"
:mode="codeEditorMode" :mode="codeEditorMode"
:model-value="modelValueString" :model-value="modelValueString"
@ -1119,7 +1119,7 @@ onUpdated(async () => {
@update:model-value="valueChangedDebounced" @update:model-value="valueChangedDebounced"
/> />
<HtmlEditor <HtmlEditor
v-else-if="editorType === 'htmlEditor' && !codeEditDialogVisible" v-else-if="editorType === 'htmlEditor' && codeEditDialogVisible"
:model-value="modelValueString" :model-value="modelValueString"
:is-read-only="isReadOnly" :is-read-only="isReadOnly"
:rows="editorRows" :rows="editorRows"
@ -1129,7 +1129,7 @@ onUpdated(async () => {
@update:model-value="valueChangedDebounced" @update:model-value="valueChangedDebounced"
/> />
<SqlEditor <SqlEditor
v-else-if="editorType === 'sqlEditor' && !codeEditDialogVisible" v-else-if="editorType === 'sqlEditor' && codeEditDialogVisible"
:model-value="modelValueString" :model-value="modelValueString"
:dialect="getArgument('sqlDialect')" :dialect="getArgument('sqlDialect')"
:is-read-only="isReadOnly" :is-read-only="isReadOnly"
@ -1138,7 +1138,7 @@ onUpdated(async () => {
@update:model-value="valueChangedDebounced" @update:model-value="valueChangedDebounced"
/> />
<JsEditor <JsEditor
v-else-if="editorType === 'jsEditor' && !codeEditDialogVisible" v-else-if="editorType === 'jsEditor' && codeEditDialogVisible"
:model-value="modelValueString" :model-value="modelValueString"
:is-read-only="isReadOnly" :is-read-only="isReadOnly"
:rows="editorRows" :rows="editorRows"
@ -1148,7 +1148,7 @@ onUpdated(async () => {
/> />
<JsonEditor <JsonEditor
v-else-if="parameter.type === 'json' && !codeEditDialogVisible" v-else-if="parameter.type === 'json' && codeEditDialogVisible"
:model-value="modelValueString" :model-value="modelValueString"
:is-read-only="isReadOnly" :is-read-only="isReadOnly"
:rows="editorRows" :rows="editorRows"
@ -1256,7 +1256,7 @@ onUpdated(async () => {
</JsEditor> </JsEditor>
<JsonEditor <JsonEditor
v-else-if="parameter.type === 'json'" v-else-if="parameter.type === 'json' && !codeEditDialogVisible"
:model-value="modelValueString" :model-value="modelValueString"
:is-read-only="isReadOnly" :is-read-only="isReadOnly"
:rows="editorRows" :rows="editorRows"

View file

@ -114,6 +114,7 @@ const {
segments: { all: segments }, segments: { all: segments },
readEditorValue, readEditorValue,
hasFocus: editorHasFocus, hasFocus: editorHasFocus,
isDirty,
} = useExpressionEditor({ } = useExpressionEditor({
editorRef: sqlEditor, editorRef: sqlEditor,
editorValue, editorValue,
@ -148,6 +149,7 @@ onMounted(() => {
}); });
onBeforeUnmount(() => { onBeforeUnmount(() => {
if (isDirty.value) emit('update:model-value', readEditorValue());
codeNodeEditorEventBus.off('highlightLine', highlightLine); codeNodeEditorEventBus.off('highlightLine', highlightLine);
}); });
@ -226,6 +228,10 @@ async function onDrop(value: string, event: MouseEvent) {
.sqlEditor { .sqlEditor {
position: relative; position: relative;
height: 100%; height: 100%;
& > div {
height: 100%;
}
} }
.codemirror { .codemirror {

View file

@ -70,6 +70,7 @@ export const useExpressionEditor = ({
const telemetryExtensions = ref<Compartment>(new Compartment()); const telemetryExtensions = ref<Compartment>(new Compartment());
const autocompleteStatus = ref<'pending' | 'active' | null>(null); const autocompleteStatus = ref<'pending' | 'active' | null>(null);
const dragging = ref(false); const dragging = ref(false);
const isDirty = ref(false);
const updateSegments = (): void => { const updateSegments = (): void => {
const state = editor.value?.state; const state = editor.value?.state;
@ -156,6 +157,7 @@ export const useExpressionEditor = ({
const debouncedUpdateSegments = debounce(updateSegments, 200); const debouncedUpdateSegments = debounce(updateSegments, 200);
function onEditorUpdate(viewUpdate: ViewUpdate) { function onEditorUpdate(viewUpdate: ViewUpdate) {
isDirty.value = true;
autocompleteStatus.value = completionStatus(viewUpdate.view.state); autocompleteStatus.value = completionStatus(viewUpdate.view.state);
updateSelection(viewUpdate); updateSelection(viewUpdate);
@ -463,5 +465,6 @@ export const useExpressionEditor = ({
select, select,
selectAll, selectAll,
focus, focus,
isDirty,
}; };
}; };