diff --git a/cypress/e2e/41-editors.cy.ts b/cypress/e2e/41-editors.cy.ts
index c99b982e4c..d84aa5843f 100644
--- a/cypress/e2e/41-editors.cy.ts
+++ b/cypress/e2e/41-editors.cy.ts
@@ -110,6 +110,40 @@ describe('Editors', () => {
ndv.actions.close();
workflowPage.getters.isWorkflowSaved().should('not.be.true');
});
+
+ it('should allow switching between SQL editors in connected nodes', () => {
+ workflowPage.actions.addInitialNodeToCanvas('Postgres', {
+ action: 'Execute a SQL query',
+ keepNdvOpen: true,
+ });
+ ndv.getters
+ .sqlEditorContainer()
+ .click()
+ .find('.cm-content')
+ .paste('SELECT * FROM `firstTable`');
+ ndv.actions.close();
+
+ workflowPage.actions.addNodeToCanvas('Postgres', true, true, 'Execute a SQL query');
+ ndv.getters
+ .sqlEditorContainer()
+ .click()
+ .find('.cm-content')
+ .paste('SELECT * FROM `secondTable`');
+ ndv.actions.close();
+
+ workflowPage.actions.openNode('Postgres');
+ ndv.actions.clickFloatingNode('Postgres1');
+ ndv.getters
+ .sqlEditorContainer()
+ .find('.cm-content')
+ .should('have.text', 'SELECT * FROM `secondTable`');
+
+ ndv.actions.clickFloatingNode('Postgres');
+ ndv.getters
+ .sqlEditorContainer()
+ .find('.cm-content')
+ .should('have.text', 'SELECT * FROM `firstTable`');
+ });
});
describe('HTML Editor', () => {
@@ -173,5 +207,38 @@ describe('Editors', () => {
ndv.actions.close();
workflowPage.getters.isWorkflowSaved().should('not.be.true');
});
+
+ it('should allow switching between HTML editors in connected nodes', () => {
+ workflowPage.actions.addInitialNodeToCanvas('HTML', {
+ action: 'Generate HTML template',
+ keepNdvOpen: true,
+ });
+ ndv.getters
+ .htmlEditorContainer()
+ .click()
+ .find('.cm-content')
+ .type('{selectall}')
+ .paste('
Hello World');
+ .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')
diff --git a/packages/frontend/editor-ui/src/components/CssEditor/CssEditor.vue b/packages/frontend/editor-ui/src/components/CssEditor/CssEditor.vue
index a6ef3ffbb3..d163ba2680 100644
--- a/packages/frontend/editor-ui/src/components/CssEditor/CssEditor.vue
+++ b/packages/frontend/editor-ui/src/components/CssEditor/CssEditor.vue
@@ -10,18 +10,18 @@ import {
keymap,
lineNumbers,
} from '@codemirror/view';
-import { computed, onMounted, ref, toRaw, watch } from 'vue';
+import { computed, ref, toRaw } from 'vue';
import { useExpressionEditor } from '@/composables/useExpressionEditor';
import { n8nCompletionSources } from '@/plugins/codemirror/completions/addCompletions';
-import { editorKeymap } from '@/plugins/codemirror/keymap';
-import { n8nAutocompletion } from '@/plugins/codemirror/n8nLang';
-import { codeEditorTheme } from '../CodeNodeEditor/theme';
import { dropInExpressionEditor, mappingDropCursor } from '@/plugins/codemirror/dragAndDrop';
import {
expressionCloseBrackets,
expressionCloseBracketsConfig,
} from '@/plugins/codemirror/expressionCloseBrackets';
+import { editorKeymap } from '@/plugins/codemirror/keymap';
+import { n8nAutocompletion } from '@/plugins/codemirror/n8nLang';
+import { codeEditorTheme } from '../CodeNodeEditor/theme';
type Props = {
modelValue: string;
@@ -69,23 +69,13 @@ const extensions = computed(() => [
mappingDropCursor(),
]);
-const {
- editor: editorRef,
- segments,
- readEditorValue,
- isDirty,
-} = useExpressionEditor({
+const { editor: editorRef, readEditorValue } = useExpressionEditor({
editorRef: cssEditor,
editorValue,
extensions,
-});
-
-watch(segments.display, () => {
- emit('update:model-value', readEditorValue());
-});
-
-onMounted(() => {
- if (isDirty.value) emit('update:model-value', readEditorValue());
+ onChange: () => {
+ emit('update:model-value', readEditorValue());
+ },
});
async function onDrop(value: string, event: MouseEvent) {
diff --git a/packages/frontend/editor-ui/src/components/HtmlEditor/HtmlEditor.vue b/packages/frontend/editor-ui/src/components/HtmlEditor/HtmlEditor.vue
index e868ff7ef2..9830e3b810 100644
--- a/packages/frontend/editor-ui/src/components/HtmlEditor/HtmlEditor.vue
+++ b/packages/frontend/editor-ui/src/components/HtmlEditor/HtmlEditor.vue
@@ -20,22 +20,22 @@ import jsParser from 'prettier/plugins/babel';
import * as estree from 'prettier/plugins/estree';
import htmlParser from 'prettier/plugins/html';
import cssParser from 'prettier/plugins/postcss';
-import { computed, onBeforeUnmount, onMounted, ref, toRaw, toValue, watch } from 'vue';
+import { computed, onBeforeUnmount, onMounted, ref, toRaw, toValue } from 'vue';
import { useExpressionEditor } from '@/composables/useExpressionEditor';
import { htmlEditorEventBus } from '@/event-bus';
import { n8nCompletionSources } from '@/plugins/codemirror/completions/addCompletions';
+import { dropInExpressionEditor, mappingDropCursor } from '@/plugins/codemirror/dragAndDrop';
+import {
+ expressionCloseBrackets,
+ expressionCloseBracketsConfig,
+} from '@/plugins/codemirror/expressionCloseBrackets';
import { editorKeymap } from '@/plugins/codemirror/keymap';
import { n8nAutocompletion } from '@/plugins/codemirror/n8nLang';
import { autoCloseTags, htmlLanguage } from 'codemirror-lang-html-n8n';
import { codeEditorTheme } from '../CodeNodeEditor/theme';
import type { Range, Section } from './types';
import { nonTakenRanges } from './utils';
-import { dropInExpressionEditor, mappingDropCursor } from '@/plugins/codemirror/dragAndDrop';
-import {
- expressionCloseBrackets,
- expressionCloseBracketsConfig,
-} from '@/plugins/codemirror/expressionCloseBrackets';
type Props = {
modelValue: string;
@@ -55,7 +55,6 @@ const emit = defineEmits<{
}>();
const htmlEditor = ref
();
-const editorValue = ref(props.modelValue);
const extensions = computed(() => [
bracketMatching(),
n8nAutocompletion(),
@@ -82,15 +81,13 @@ const extensions = computed(() => [
highlightActiveLine(),
mappingDropCursor(),
]);
-const {
- editor: editorRef,
- segments,
- readEditorValue,
- isDirty,
-} = useExpressionEditor({
+const { editor: editorRef, readEditorValue } = useExpressionEditor({
editorRef: htmlEditor,
- editorValue,
+ editorValue: () => props.modelValue,
extensions,
+ onChange: () => {
+ emit('update:model-value', readEditorValue());
+ },
});
const sections = computed(() => {
@@ -225,16 +222,11 @@ async function formatHtml() {
});
}
-watch(segments.display, () => {
- emit('update:model-value', readEditorValue());
-});
-
onMounted(() => {
htmlEditorEventBus.on('format-html', formatHtml);
});
onBeforeUnmount(() => {
- if (isDirty.value) emit('update:model-value', readEditorValue());
htmlEditorEventBus.off('format-html', formatHtml);
});
diff --git a/packages/frontend/editor-ui/src/components/SqlEditor/SqlEditor.vue b/packages/frontend/editor-ui/src/components/SqlEditor/SqlEditor.vue
index 398f66c866..29e8d5aa4e 100644
--- a/packages/frontend/editor-ui/src/components/SqlEditor/SqlEditor.vue
+++ b/packages/frontend/editor-ui/src/components/SqlEditor/SqlEditor.vue
@@ -112,27 +112,21 @@ const extensions = computed(() => {
}
return baseExtensions;
});
-const editorValue = ref(props.modelValue);
const {
editor,
segments: { all: segments },
readEditorValue,
hasFocus: editorHasFocus,
- isDirty,
} = useExpressionEditor({
editorRef: sqlEditor,
- editorValue,
+ editorValue: () => props.modelValue,
extensions,
skipSegments: ['Statement', 'CompositeIdentifier', 'Parens', 'Brackets'],
isReadOnly: props.isReadOnly,
-});
-
-watch(
- () => props.modelValue,
- (newValue) => {
- editorValue.value = newValue;
+ onChange: () => {
+ emit('update:model-value', readEditorValue());
},
-);
+});
watch(editorHasFocus, (focus) => {
if (focus) {
@@ -140,10 +134,6 @@ watch(editorHasFocus, (focus) => {
}
});
-watch(segments, () => {
- emit('update:model-value', readEditorValue());
-});
-
onMounted(() => {
codeNodeEditorEventBus.on('highlightLine', highlightLine);
@@ -153,7 +143,6 @@ onMounted(() => {
});
onBeforeUnmount(() => {
- if (isDirty.value) emit('update:model-value', readEditorValue());
codeNodeEditorEventBus.off('highlightLine', highlightLine);
});
diff --git a/packages/frontend/editor-ui/src/composables/useExpressionEditor.ts b/packages/frontend/editor-ui/src/composables/useExpressionEditor.ts
index ed845e3501..2c519617e6 100644
--- a/packages/frontend/editor-ui/src/composables/useExpressionEditor.ts
+++ b/packages/frontend/editor-ui/src/composables/useExpressionEditor.ts
@@ -38,6 +38,7 @@ import { useRouter } from 'vue-router';
import { useI18n } from '../composables/useI18n';
import { useWorkflowsStore } from '../stores/workflows.store';
import { useAutocompleteTelemetry } from './useAutocompleteTelemetry';
+import { ignoreUpdateAnnotation } from '../utils/forceParse';
export const useExpressionEditor = ({
editorRef,
@@ -47,6 +48,7 @@ export const useExpressionEditor = ({
skipSegments = [],
autocompleteTelemetry,
isReadOnly = false,
+ onChange = () => {},
}: {
editorRef: MaybeRefOrGetter;
editorValue?: MaybeRefOrGetter;
@@ -55,6 +57,7 @@ export const useExpressionEditor = ({
skipSegments?: MaybeRefOrGetter;
autocompleteTelemetry?: MaybeRefOrGetter<{ enabled: true; parameterPath: string }>;
isReadOnly?: MaybeRefOrGetter;
+ onChange?: (viewUpdate: ViewUpdate) => void;
}) => {
const ndvStore = useNDVStore();
const workflowsStore = useWorkflowsStore();
@@ -70,7 +73,9 @@ export const useExpressionEditor = ({
const telemetryExtensions = ref(new Compartment());
const autocompleteStatus = ref<'pending' | 'active' | null>(null);
const dragging = ref(false);
- const isDirty = ref(false);
+ const hasChanges = ref(false);
+
+ const emitChanges = debounce(onChange, 300);
const updateSegments = (): void => {
const state = editor.value?.state;
@@ -157,13 +162,18 @@ export const useExpressionEditor = ({
const debouncedUpdateSegments = debounce(updateSegments, 200);
function onEditorUpdate(viewUpdate: ViewUpdate) {
- isDirty.value = true;
autocompleteStatus.value = completionStatus(viewUpdate.view.state);
updateSelection(viewUpdate);
- if (!viewUpdate.docChanged) return;
+ const shouldIgnoreUpdate = viewUpdate.transactions.some((tr) =>
+ tr.annotation(ignoreUpdateAnnotation),
+ );
- debouncedUpdateSegments();
+ if (viewUpdate.docChanged && !shouldIgnoreUpdate) {
+ hasChanges.value = true;
+ emitChanges(viewUpdate);
+ debouncedUpdateSegments();
+ }
}
function blur() {
@@ -265,6 +275,8 @@ export const useExpressionEditor = ({
onBeforeUnmount(() => {
document.removeEventListener('click', blurOnClickOutside);
+ debouncedUpdateSegments.flush();
+ emitChanges.flush();
editor.value?.destroy();
});
@@ -465,6 +477,6 @@ export const useExpressionEditor = ({
select,
selectAll,
focus,
- isDirty,
+ hasChanges,
};
};