Skip to content

Commit bb2cfc2

Browse files
authored
fix(editor): Fix switching between connected SQL/HTML editors (#15297)
1 parent 102c676 commit bb2cfc2

File tree

6 files changed

+109
-63
lines changed

6 files changed

+109
-63
lines changed

cypress/e2e/41-editors.cy.ts

Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -110,6 +110,40 @@ describe('Editors', () => {
110110
ndv.actions.close();
111111
workflowPage.getters.isWorkflowSaved().should('not.be.true');
112112
});
113+
114+
it('should allow switching between SQL editors in connected nodes', () => {
115+
workflowPage.actions.addInitialNodeToCanvas('Postgres', {
116+
action: 'Execute a SQL query',
117+
keepNdvOpen: true,
118+
});
119+
ndv.getters
120+
.sqlEditorContainer()
121+
.click()
122+
.find('.cm-content')
123+
.paste('SELECT * FROM `firstTable`');
124+
ndv.actions.close();
125+
126+
workflowPage.actions.addNodeToCanvas('Postgres', true, true, 'Execute a SQL query');
127+
ndv.getters
128+
.sqlEditorContainer()
129+
.click()
130+
.find('.cm-content')
131+
.paste('SELECT * FROM `secondTable`');
132+
ndv.actions.close();
133+
134+
workflowPage.actions.openNode('Postgres');
135+
ndv.actions.clickFloatingNode('Postgres1');
136+
ndv.getters
137+
.sqlEditorContainer()
138+
.find('.cm-content')
139+
.should('have.text', 'SELECT * FROM `secondTable`');
140+
141+
ndv.actions.clickFloatingNode('Postgres');
142+
ndv.getters
143+
.sqlEditorContainer()
144+
.find('.cm-content')
145+
.should('have.text', 'SELECT * FROM `firstTable`');
146+
});
113147
});
114148

115149
describe('HTML Editor', () => {
@@ -173,5 +207,38 @@ describe('Editors', () => {
173207
ndv.actions.close();
174208
workflowPage.getters.isWorkflowSaved().should('not.be.true');
175209
});
210+
211+
it('should allow switching between HTML editors in connected nodes', () => {
212+
workflowPage.actions.addInitialNodeToCanvas('HTML', {
213+
action: 'Generate HTML template',
214+
keepNdvOpen: true,
215+
});
216+
ndv.getters
217+
.htmlEditorContainer()
218+
.click()
219+
.find('.cm-content')
220+
.type('{selectall}')
221+
.paste('<div>First</div>');
222+
ndv.actions.close();
223+
224+
workflowPage.actions.addNodeToCanvas('HTML', true, true, 'Generate HTML template');
225+
ndv.getters
226+
.htmlEditorContainer()
227+
.click()
228+
.find('.cm-content')
229+
.type('{selectall}')
230+
.paste('<div>Second</div>');
231+
ndv.actions.close();
232+
233+
workflowPage.actions.openNode('HTML');
234+
ndv.actions.clickFloatingNode('HTML1');
235+
ndv.getters
236+
.htmlEditorContainer()
237+
.find('.cm-content')
238+
.should('have.text', '<div>Second</div>');
239+
240+
ndv.actions.clickFloatingNode('HTML');
241+
ndv.getters.htmlEditorContainer().find('.cm-content').should('have.text', '<div>First</div>');
242+
});
176243
});
177244
});

cypress/e2e/5-ndv.cy.ts

Lines changed: 2 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -387,7 +387,6 @@ describe('NDV', () => {
387387

388388
ndv.getters.codeEditorFullscreen().type('{selectall}').type('{backspace}').type('foo()');
389389
ndv.getters.codeEditorFullscreen().should('contain.text', 'foo()');
390-
cy.wait(200); // allow change to emit before closing modal
391390
ndv.getters.codeEditorDialog().find('.el-dialog__close').click();
392391
ndv.getters.parameterInput('jsCode').get('.cm-content').should('contain.text', 'foo()');
393392
ndv.actions.close();
@@ -400,9 +399,8 @@ describe('NDV', () => {
400399
.codeEditorFullscreen()
401400
.type('{selectall}')
402401
.type('{backspace}')
403-
.type('SELECT * FROM workflows');
402+
.paste('SELECT * FROM workflows');
404403
ndv.getters.codeEditorFullscreen().should('contain.text', 'SELECT * FROM workflows');
405-
cy.wait(200);
406404
ndv.getters.codeEditorDialog().find('.el-dialog__close').click();
407405
ndv.getters
408406
.parameterInput('query')
@@ -418,10 +416,8 @@ describe('NDV', () => {
418416
.codeEditorFullscreen()
419417
.type('{selectall}')
420418
.type('{backspace}')
421-
.type('<div>Hello World');
419+
.type('<div>Hello World</div>');
422420
ndv.getters.codeEditorFullscreen().should('contain.text', '<div>Hello World</div>');
423-
cy.wait(200);
424-
425421
ndv.getters.codeEditorDialog().find('.el-dialog__close').click();
426422
ndv.getters
427423
.parameterInput('html')

packages/frontend/editor-ui/src/components/CssEditor/CssEditor.vue

Lines changed: 8 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -10,18 +10,18 @@ import {
1010
keymap,
1111
lineNumbers,
1212
} from '@codemirror/view';
13-
import { computed, onMounted, ref, toRaw, watch } from 'vue';
13+
import { computed, ref, toRaw } from 'vue';
1414
1515
import { useExpressionEditor } from '@/composables/useExpressionEditor';
1616
import { n8nCompletionSources } from '@/plugins/codemirror/completions/addCompletions';
17-
import { editorKeymap } from '@/plugins/codemirror/keymap';
18-
import { n8nAutocompletion } from '@/plugins/codemirror/n8nLang';
19-
import { codeEditorTheme } from '../CodeNodeEditor/theme';
2017
import { dropInExpressionEditor, mappingDropCursor } from '@/plugins/codemirror/dragAndDrop';
2118
import {
2219
expressionCloseBrackets,
2320
expressionCloseBracketsConfig,
2421
} from '@/plugins/codemirror/expressionCloseBrackets';
22+
import { editorKeymap } from '@/plugins/codemirror/keymap';
23+
import { n8nAutocompletion } from '@/plugins/codemirror/n8nLang';
24+
import { codeEditorTheme } from '../CodeNodeEditor/theme';
2525
2626
type Props = {
2727
modelValue: string;
@@ -69,23 +69,13 @@ const extensions = computed(() => [
6969
mappingDropCursor(),
7070
]);
7171
72-
const {
73-
editor: editorRef,
74-
segments,
75-
readEditorValue,
76-
isDirty,
77-
} = useExpressionEditor({
72+
const { editor: editorRef, readEditorValue } = useExpressionEditor({
7873
editorRef: cssEditor,
7974
editorValue,
8075
extensions,
81-
});
82-
83-
watch(segments.display, () => {
84-
emit('update:model-value', readEditorValue());
85-
});
86-
87-
onMounted(() => {
88-
if (isDirty.value) emit('update:model-value', readEditorValue());
76+
onChange: () => {
77+
emit('update:model-value', readEditorValue());
78+
},
8979
});
9080
9181
async function onDrop(value: string, event: MouseEvent) {

packages/frontend/editor-ui/src/components/HtmlEditor/HtmlEditor.vue

Lines changed: 11 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -20,22 +20,22 @@ import jsParser from 'prettier/plugins/babel';
2020
import * as estree from 'prettier/plugins/estree';
2121
import htmlParser from 'prettier/plugins/html';
2222
import cssParser from 'prettier/plugins/postcss';
23-
import { computed, onBeforeUnmount, onMounted, ref, toRaw, toValue, watch } from 'vue';
23+
import { computed, onBeforeUnmount, onMounted, ref, toRaw, toValue } from 'vue';
2424
2525
import { useExpressionEditor } from '@/composables/useExpressionEditor';
2626
import { htmlEditorEventBus } from '@/event-bus';
2727
import { n8nCompletionSources } from '@/plugins/codemirror/completions/addCompletions';
28+
import { dropInExpressionEditor, mappingDropCursor } from '@/plugins/codemirror/dragAndDrop';
29+
import {
30+
expressionCloseBrackets,
31+
expressionCloseBracketsConfig,
32+
} from '@/plugins/codemirror/expressionCloseBrackets';
2833
import { editorKeymap } from '@/plugins/codemirror/keymap';
2934
import { n8nAutocompletion } from '@/plugins/codemirror/n8nLang';
3035
import { autoCloseTags, htmlLanguage } from 'codemirror-lang-html-n8n';
3136
import { codeEditorTheme } from '../CodeNodeEditor/theme';
3237
import type { Range, Section } from './types';
3338
import { nonTakenRanges } from './utils';
34-
import { dropInExpressionEditor, mappingDropCursor } from '@/plugins/codemirror/dragAndDrop';
35-
import {
36-
expressionCloseBrackets,
37-
expressionCloseBracketsConfig,
38-
} from '@/plugins/codemirror/expressionCloseBrackets';
3939
4040
type Props = {
4141
modelValue: string;
@@ -55,7 +55,6 @@ const emit = defineEmits<{
5555
}>();
5656
5757
const htmlEditor = ref<HTMLElement>();
58-
const editorValue = ref<string>(props.modelValue);
5958
const extensions = computed(() => [
6059
bracketMatching(),
6160
n8nAutocompletion(),
@@ -82,15 +81,13 @@ const extensions = computed(() => [
8281
highlightActiveLine(),
8382
mappingDropCursor(),
8483
]);
85-
const {
86-
editor: editorRef,
87-
segments,
88-
readEditorValue,
89-
isDirty,
90-
} = useExpressionEditor({
84+
const { editor: editorRef, readEditorValue } = useExpressionEditor({
9185
editorRef: htmlEditor,
92-
editorValue,
86+
editorValue: () => props.modelValue,
9387
extensions,
88+
onChange: () => {
89+
emit('update:model-value', readEditorValue());
90+
},
9491
});
9592
9693
const sections = computed(() => {
@@ -225,16 +222,11 @@ async function formatHtml() {
225222
});
226223
}
227224
228-
watch(segments.display, () => {
229-
emit('update:model-value', readEditorValue());
230-
});
231-
232225
onMounted(() => {
233226
htmlEditorEventBus.on('format-html', formatHtml);
234227
});
235228
236229
onBeforeUnmount(() => {
237-
if (isDirty.value) emit('update:model-value', readEditorValue());
238230
htmlEditorEventBus.off('format-html', formatHtml);
239231
});
240232

packages/frontend/editor-ui/src/components/SqlEditor/SqlEditor.vue

Lines changed: 4 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -112,38 +112,28 @@ const extensions = computed(() => {
112112
}
113113
return baseExtensions;
114114
});
115-
const editorValue = ref(props.modelValue);
116115
const {
117116
editor,
118117
segments: { all: segments },
119118
readEditorValue,
120119
hasFocus: editorHasFocus,
121-
isDirty,
122120
} = useExpressionEditor({
123121
editorRef: sqlEditor,
124-
editorValue,
122+
editorValue: () => props.modelValue,
125123
extensions,
126124
skipSegments: ['Statement', 'CompositeIdentifier', 'Parens', 'Brackets'],
127125
isReadOnly: props.isReadOnly,
128-
});
129-
130-
watch(
131-
() => props.modelValue,
132-
(newValue) => {
133-
editorValue.value = newValue;
126+
onChange: () => {
127+
emit('update:model-value', readEditorValue());
134128
},
135-
);
129+
});
136130
137131
watch(editorHasFocus, (focus) => {
138132
if (focus) {
139133
isFocused.value = true;
140134
}
141135
});
142136
143-
watch(segments, () => {
144-
emit('update:model-value', readEditorValue());
145-
});
146-
147137
onMounted(() => {
148138
codeNodeEditorEventBus.on('highlightLine', highlightLine);
149139
@@ -153,7 +143,6 @@ onMounted(() => {
153143
});
154144
155145
onBeforeUnmount(() => {
156-
if (isDirty.value) emit('update:model-value', readEditorValue());
157146
codeNodeEditorEventBus.off('highlightLine', highlightLine);
158147
});
159148

packages/frontend/editor-ui/src/composables/useExpressionEditor.ts

Lines changed: 17 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,7 @@ import { useRouter } from 'vue-router';
3838
import { useI18n } from '../composables/useI18n';
3939
import { useWorkflowsStore } from '../stores/workflows.store';
4040
import { useAutocompleteTelemetry } from './useAutocompleteTelemetry';
41+
import { ignoreUpdateAnnotation } from '../utils/forceParse';
4142

4243
export const useExpressionEditor = ({
4344
editorRef,
@@ -47,6 +48,7 @@ export const useExpressionEditor = ({
4748
skipSegments = [],
4849
autocompleteTelemetry,
4950
isReadOnly = false,
51+
onChange = () => {},
5052
}: {
5153
editorRef: MaybeRefOrGetter<HTMLElement | undefined>;
5254
editorValue?: MaybeRefOrGetter<string>;
@@ -55,6 +57,7 @@ export const useExpressionEditor = ({
5557
skipSegments?: MaybeRefOrGetter<string[]>;
5658
autocompleteTelemetry?: MaybeRefOrGetter<{ enabled: true; parameterPath: string }>;
5759
isReadOnly?: MaybeRefOrGetter<boolean>;
60+
onChange?: (viewUpdate: ViewUpdate) => void;
5861
}) => {
5962
const ndvStore = useNDVStore();
6063
const workflowsStore = useWorkflowsStore();
@@ -70,7 +73,9 @@ export const useExpressionEditor = ({
7073
const telemetryExtensions = ref<Compartment>(new Compartment());
7174
const autocompleteStatus = ref<'pending' | 'active' | null>(null);
7275
const dragging = ref(false);
73-
const isDirty = ref(false);
76+
const hasChanges = ref(false);
77+
78+
const emitChanges = debounce(onChange, 300);
7479

7580
const updateSegments = (): void => {
7681
const state = editor.value?.state;
@@ -157,13 +162,18 @@ export const useExpressionEditor = ({
157162
const debouncedUpdateSegments = debounce(updateSegments, 200);
158163

159164
function onEditorUpdate(viewUpdate: ViewUpdate) {
160-
isDirty.value = true;
161165
autocompleteStatus.value = completionStatus(viewUpdate.view.state);
162166
updateSelection(viewUpdate);
163167

164-
if (!viewUpdate.docChanged) return;
168+
const shouldIgnoreUpdate = viewUpdate.transactions.some((tr) =>
169+
tr.annotation(ignoreUpdateAnnotation),
170+
);
165171

166-
debouncedUpdateSegments();
172+
if (viewUpdate.docChanged && !shouldIgnoreUpdate) {
173+
hasChanges.value = true;
174+
emitChanges(viewUpdate);
175+
debouncedUpdateSegments();
176+
}
167177
}
168178

169179
function blur() {
@@ -265,6 +275,8 @@ export const useExpressionEditor = ({
265275

266276
onBeforeUnmount(() => {
267277
document.removeEventListener('click', blurOnClickOutside);
278+
debouncedUpdateSegments.flush();
279+
emitChanges.flush();
268280
editor.value?.destroy();
269281
});
270282

@@ -465,6 +477,6 @@ export const useExpressionEditor = ({
465477
select,
466478
selectAll,
467479
focus,
468-
isDirty,
480+
hasChanges,
469481
};
470482
};

0 commit comments

Comments
 (0)