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,
};
};