diff --git a/cypress/e2e/5-ndv.cy.ts b/cypress/e2e/5-ndv.cy.ts index 8bad424554..e0f2072af1 100644 --- a/cypress/e2e/5-ndv.cy.ts +++ b/cypress/e2e/5-ndv.cy.ts @@ -1,6 +1,10 @@ import { setCredentialValues } from '../composables/modals/credential-modal'; -import { clickCreateNewCredential } from '../composables/ndv'; -import { MANUAL_TRIGGER_NODE_DISPLAY_NAME, NOTION_NODE_NAME } from '../constants'; +import { clickCreateNewCredential, setParameterSelectByContent } from '../composables/ndv'; +import { + EDIT_FIELDS_SET_NODE_NAME, + MANUAL_TRIGGER_NODE_DISPLAY_NAME, + NOTION_NODE_NAME, +} from '../constants'; import { NDV, WorkflowPage } from '../pages'; import { NodeCreator } from '../pages/features/node-creator'; @@ -359,15 +363,71 @@ describe('NDV', () => { 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 }); ndv.actions.openCodeEditorFullscreen(); ndv.getters.codeEditorFullscreen().type('{selectall}').type('{backspace}').type('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.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('
Hello World'); + ndv.getters.codeEditorFullscreen().should('contain.text', '
Hello World
'); + cy.wait(200); + + ndv.getters.codeEditorDialog().find('.el-dialog__close').click(); + ndv.getters + .parameterInput('html') + .get('.cm-content') + .should('contain.text', '
Hello World
'); + 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', () => { diff --git a/packages/editor-ui/src/components/HtmlEditor/HtmlEditor.vue b/packages/editor-ui/src/components/HtmlEditor/HtmlEditor.vue index 91b9b0c35a..c7202ef498 100644 --- a/packages/editor-ui/src/components/HtmlEditor/HtmlEditor.vue +++ b/packages/editor-ui/src/components/HtmlEditor/HtmlEditor.vue @@ -83,6 +83,7 @@ const { editor: editorRef, segments, readEditorValue, + isDirty, } = useExpressionEditor({ editorRef: htmlEditor, editorValue, @@ -230,6 +231,7 @@ onMounted(() => { }); onBeforeUnmount(() => { + if (isDirty.value) emit('update:model-value', readEditorValue()); htmlEditorEventBus.off('format-html', formatHtml); }); @@ -246,7 +248,10 @@ async function onDrop(value: string, event: MouseEvent) { @@ -264,6 +269,10 @@ async function onDrop(value: string, event: MouseEvent) { } } +.fillHeight { + height: 100%; +} + .droppable { :global(.cm-editor) { border-color: var(--color-ndv-droppable-parameter); diff --git a/packages/editor-ui/src/components/JsonEditor/JsonEditor.vue b/packages/editor-ui/src/components/JsonEditor/JsonEditor.vue index 998430838f..4b2c92d87a 100644 --- a/packages/editor-ui/src/components/JsonEditor/JsonEditor.vue +++ b/packages/editor-ui/src/components/JsonEditor/JsonEditor.vue @@ -17,7 +17,7 @@ import { import { editorKeymap } from '@/plugins/codemirror/keymap'; 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 { mappingDropCursor } from '@/plugins/codemirror/dragAndDrop'; @@ -36,6 +36,7 @@ const emit = defineEmits<{ const jsonEditorRef = ref(); const editor = ref(null); const editorState = ref(null); +const isDirty = ref(false); const extensions = computed(() => { const extensionsToApply: Extension[] = [ @@ -65,6 +66,7 @@ const extensions = computed(() => { bracketMatching(), mappingDropCursor(), EditorView.updateListener.of((viewUpdate: ViewUpdate) => { + isDirty.value = true; if (!viewUpdate.docChanged || !editor.value) return; emit('update:modelValue', editor.value?.state.doc.toString()); }), @@ -77,6 +79,12 @@ onMounted(() => { createEditor(); }); +onBeforeUnmount(() => { + if (!editor.value) return; + if (isDirty.value) emit('update:modelValue', editor.value.state.doc.toString()); + editor.value.destroy(); +}); + watch( () => props.modelValue, (newValue: string) => { diff --git a/packages/editor-ui/src/components/ParameterInput.vue b/packages/editor-ui/src/components/ParameterInput.vue index 359a0a6549..7d0aa44441 100644 --- a/packages/editor-ui/src/components/ParameterInput.vue +++ b/packages/editor-ui/src/components/ParameterInput.vue @@ -1108,7 +1108,7 @@ onUpdated(async () => { >
{ @update:model-value="valueChangedDebounced" /> { @update:model-value="valueChangedDebounced" /> { @update:model-value="valueChangedDebounced" /> { /> { { }); onBeforeUnmount(() => { + if (isDirty.value) emit('update:model-value', readEditorValue()); codeNodeEditorEventBus.off('highlightLine', highlightLine); }); @@ -226,6 +228,10 @@ async function onDrop(value: string, event: MouseEvent) { .sqlEditor { position: relative; height: 100%; + + & > div { + height: 100%; + } } .codemirror { diff --git a/packages/editor-ui/src/composables/useExpressionEditor.ts b/packages/editor-ui/src/composables/useExpressionEditor.ts index c196988764..ed845e3501 100644 --- a/packages/editor-ui/src/composables/useExpressionEditor.ts +++ b/packages/editor-ui/src/composables/useExpressionEditor.ts @@ -70,6 +70,7 @@ export const useExpressionEditor = ({ const telemetryExtensions = ref(new Compartment()); const autocompleteStatus = ref<'pending' | 'active' | null>(null); const dragging = ref(false); + const isDirty = ref(false); const updateSegments = (): void => { const state = editor.value?.state; @@ -156,6 +157,7 @@ export const useExpressionEditor = ({ const debouncedUpdateSegments = debounce(updateSegments, 200); function onEditorUpdate(viewUpdate: ViewUpdate) { + isDirty.value = true; autocompleteStatus.value = completionStatus(viewUpdate.view.state); updateSelection(viewUpdate); @@ -463,5 +465,6 @@ export const useExpressionEditor = ({ select, selectAll, focus, + isDirty, }; };