mirror of
https://github.com/n8n-io/n8n.git
synced 2025-03-05 20:50:17 -08:00
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
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:
parent
bfe3c5611a
commit
aa1f3a7d98
|
@ -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', () => {
|
||||||
|
|
|
@ -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);
|
||||||
|
|
|
@ -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) => {
|
||||||
|
|
|
@ -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"
|
||||||
|
|
|
@ -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 {
|
||||||
|
|
|
@ -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,
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
Loading…
Reference in a new issue