mirror of
https://github.com/Abdulazizzn/n8n-enterprise-unlocked.git
synced 2025-12-18 10:31:15 +00:00
fix(editor): Fix switching between connected SQL/HTML editors (#15297)
This commit is contained in:
@@ -110,6 +110,40 @@ describe('Editors', () => {
|
|||||||
ndv.actions.close();
|
ndv.actions.close();
|
||||||
workflowPage.getters.isWorkflowSaved().should('not.be.true');
|
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', () => {
|
describe('HTML Editor', () => {
|
||||||
@@ -173,5 +207,38 @@ describe('Editors', () => {
|
|||||||
ndv.actions.close();
|
ndv.actions.close();
|
||||||
workflowPage.getters.isWorkflowSaved().should('not.be.true');
|
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('<div>First</div>');
|
||||||
|
ndv.actions.close();
|
||||||
|
|
||||||
|
workflowPage.actions.addNodeToCanvas('HTML', true, true, 'Generate HTML template');
|
||||||
|
ndv.getters
|
||||||
|
.htmlEditorContainer()
|
||||||
|
.click()
|
||||||
|
.find('.cm-content')
|
||||||
|
.type('{selectall}')
|
||||||
|
.paste('<div>Second</div>');
|
||||||
|
ndv.actions.close();
|
||||||
|
|
||||||
|
workflowPage.actions.openNode('HTML');
|
||||||
|
ndv.actions.clickFloatingNode('HTML1');
|
||||||
|
ndv.getters
|
||||||
|
.htmlEditorContainer()
|
||||||
|
.find('.cm-content')
|
||||||
|
.should('have.text', '<div>Second</div>');
|
||||||
|
|
||||||
|
ndv.actions.clickFloatingNode('HTML');
|
||||||
|
ndv.getters.htmlEditorContainer().find('.cm-content').should('have.text', '<div>First</div>');
|
||||||
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -387,7 +387,6 @@ describe('NDV', () => {
|
|||||||
|
|
||||||
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); // 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();
|
ndv.actions.close();
|
||||||
@@ -400,9 +399,8 @@ describe('NDV', () => {
|
|||||||
.codeEditorFullscreen()
|
.codeEditorFullscreen()
|
||||||
.type('{selectall}')
|
.type('{selectall}')
|
||||||
.type('{backspace}')
|
.type('{backspace}')
|
||||||
.type('SELECT * FROM workflows');
|
.paste('SELECT * FROM workflows');
|
||||||
ndv.getters.codeEditorFullscreen().should('contain.text', '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.codeEditorDialog().find('.el-dialog__close').click();
|
||||||
ndv.getters
|
ndv.getters
|
||||||
.parameterInput('query')
|
.parameterInput('query')
|
||||||
@@ -418,10 +416,8 @@ describe('NDV', () => {
|
|||||||
.codeEditorFullscreen()
|
.codeEditorFullscreen()
|
||||||
.type('{selectall}')
|
.type('{selectall}')
|
||||||
.type('{backspace}')
|
.type('{backspace}')
|
||||||
.type('<div>Hello World');
|
.type('<div>Hello World</div>');
|
||||||
ndv.getters.codeEditorFullscreen().should('contain.text', '<div>Hello World</div>');
|
ndv.getters.codeEditorFullscreen().should('contain.text', '<div>Hello World</div>');
|
||||||
cy.wait(200);
|
|
||||||
|
|
||||||
ndv.getters.codeEditorDialog().find('.el-dialog__close').click();
|
ndv.getters.codeEditorDialog().find('.el-dialog__close').click();
|
||||||
ndv.getters
|
ndv.getters
|
||||||
.parameterInput('html')
|
.parameterInput('html')
|
||||||
|
|||||||
@@ -10,18 +10,18 @@ import {
|
|||||||
keymap,
|
keymap,
|
||||||
lineNumbers,
|
lineNumbers,
|
||||||
} from '@codemirror/view';
|
} from '@codemirror/view';
|
||||||
import { computed, onMounted, ref, toRaw, watch } from 'vue';
|
import { computed, ref, toRaw } from 'vue';
|
||||||
|
|
||||||
import { useExpressionEditor } from '@/composables/useExpressionEditor';
|
import { useExpressionEditor } from '@/composables/useExpressionEditor';
|
||||||
import { n8nCompletionSources } from '@/plugins/codemirror/completions/addCompletions';
|
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 { dropInExpressionEditor, mappingDropCursor } from '@/plugins/codemirror/dragAndDrop';
|
||||||
import {
|
import {
|
||||||
expressionCloseBrackets,
|
expressionCloseBrackets,
|
||||||
expressionCloseBracketsConfig,
|
expressionCloseBracketsConfig,
|
||||||
} from '@/plugins/codemirror/expressionCloseBrackets';
|
} from '@/plugins/codemirror/expressionCloseBrackets';
|
||||||
|
import { editorKeymap } from '@/plugins/codemirror/keymap';
|
||||||
|
import { n8nAutocompletion } from '@/plugins/codemirror/n8nLang';
|
||||||
|
import { codeEditorTheme } from '../CodeNodeEditor/theme';
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
modelValue: string;
|
modelValue: string;
|
||||||
@@ -69,23 +69,13 @@ const extensions = computed(() => [
|
|||||||
mappingDropCursor(),
|
mappingDropCursor(),
|
||||||
]);
|
]);
|
||||||
|
|
||||||
const {
|
const { editor: editorRef, readEditorValue } = useExpressionEditor({
|
||||||
editor: editorRef,
|
|
||||||
segments,
|
|
||||||
readEditorValue,
|
|
||||||
isDirty,
|
|
||||||
} = useExpressionEditor({
|
|
||||||
editorRef: cssEditor,
|
editorRef: cssEditor,
|
||||||
editorValue,
|
editorValue,
|
||||||
extensions,
|
extensions,
|
||||||
});
|
onChange: () => {
|
||||||
|
emit('update:model-value', readEditorValue());
|
||||||
watch(segments.display, () => {
|
},
|
||||||
emit('update:model-value', readEditorValue());
|
|
||||||
});
|
|
||||||
|
|
||||||
onMounted(() => {
|
|
||||||
if (isDirty.value) emit('update:model-value', readEditorValue());
|
|
||||||
});
|
});
|
||||||
|
|
||||||
async function onDrop(value: string, event: MouseEvent) {
|
async function onDrop(value: string, event: MouseEvent) {
|
||||||
|
|||||||
@@ -20,22 +20,22 @@ import jsParser from 'prettier/plugins/babel';
|
|||||||
import * as estree from 'prettier/plugins/estree';
|
import * as estree from 'prettier/plugins/estree';
|
||||||
import htmlParser from 'prettier/plugins/html';
|
import htmlParser from 'prettier/plugins/html';
|
||||||
import cssParser from 'prettier/plugins/postcss';
|
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 { useExpressionEditor } from '@/composables/useExpressionEditor';
|
||||||
import { htmlEditorEventBus } from '@/event-bus';
|
import { htmlEditorEventBus } from '@/event-bus';
|
||||||
import { n8nCompletionSources } from '@/plugins/codemirror/completions/addCompletions';
|
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 { editorKeymap } from '@/plugins/codemirror/keymap';
|
||||||
import { n8nAutocompletion } from '@/plugins/codemirror/n8nLang';
|
import { n8nAutocompletion } from '@/plugins/codemirror/n8nLang';
|
||||||
import { autoCloseTags, htmlLanguage } from 'codemirror-lang-html-n8n';
|
import { autoCloseTags, htmlLanguage } from 'codemirror-lang-html-n8n';
|
||||||
import { codeEditorTheme } from '../CodeNodeEditor/theme';
|
import { codeEditorTheme } from '../CodeNodeEditor/theme';
|
||||||
import type { Range, Section } from './types';
|
import type { Range, Section } from './types';
|
||||||
import { nonTakenRanges } from './utils';
|
import { nonTakenRanges } from './utils';
|
||||||
import { dropInExpressionEditor, mappingDropCursor } from '@/plugins/codemirror/dragAndDrop';
|
|
||||||
import {
|
|
||||||
expressionCloseBrackets,
|
|
||||||
expressionCloseBracketsConfig,
|
|
||||||
} from '@/plugins/codemirror/expressionCloseBrackets';
|
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
modelValue: string;
|
modelValue: string;
|
||||||
@@ -55,7 +55,6 @@ const emit = defineEmits<{
|
|||||||
}>();
|
}>();
|
||||||
|
|
||||||
const htmlEditor = ref<HTMLElement>();
|
const htmlEditor = ref<HTMLElement>();
|
||||||
const editorValue = ref<string>(props.modelValue);
|
|
||||||
const extensions = computed(() => [
|
const extensions = computed(() => [
|
||||||
bracketMatching(),
|
bracketMatching(),
|
||||||
n8nAutocompletion(),
|
n8nAutocompletion(),
|
||||||
@@ -82,15 +81,13 @@ const extensions = computed(() => [
|
|||||||
highlightActiveLine(),
|
highlightActiveLine(),
|
||||||
mappingDropCursor(),
|
mappingDropCursor(),
|
||||||
]);
|
]);
|
||||||
const {
|
const { editor: editorRef, readEditorValue } = useExpressionEditor({
|
||||||
editor: editorRef,
|
|
||||||
segments,
|
|
||||||
readEditorValue,
|
|
||||||
isDirty,
|
|
||||||
} = useExpressionEditor({
|
|
||||||
editorRef: htmlEditor,
|
editorRef: htmlEditor,
|
||||||
editorValue,
|
editorValue: () => props.modelValue,
|
||||||
extensions,
|
extensions,
|
||||||
|
onChange: () => {
|
||||||
|
emit('update:model-value', readEditorValue());
|
||||||
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
const sections = computed(() => {
|
const sections = computed(() => {
|
||||||
@@ -225,16 +222,11 @@ async function formatHtml() {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
watch(segments.display, () => {
|
|
||||||
emit('update:model-value', readEditorValue());
|
|
||||||
});
|
|
||||||
|
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
htmlEditorEventBus.on('format-html', formatHtml);
|
htmlEditorEventBus.on('format-html', formatHtml);
|
||||||
});
|
});
|
||||||
|
|
||||||
onBeforeUnmount(() => {
|
onBeforeUnmount(() => {
|
||||||
if (isDirty.value) emit('update:model-value', readEditorValue());
|
|
||||||
htmlEditorEventBus.off('format-html', formatHtml);
|
htmlEditorEventBus.off('format-html', formatHtml);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -112,27 +112,21 @@ const extensions = computed(() => {
|
|||||||
}
|
}
|
||||||
return baseExtensions;
|
return baseExtensions;
|
||||||
});
|
});
|
||||||
const editorValue = ref(props.modelValue);
|
|
||||||
const {
|
const {
|
||||||
editor,
|
editor,
|
||||||
segments: { all: segments },
|
segments: { all: segments },
|
||||||
readEditorValue,
|
readEditorValue,
|
||||||
hasFocus: editorHasFocus,
|
hasFocus: editorHasFocus,
|
||||||
isDirty,
|
|
||||||
} = useExpressionEditor({
|
} = useExpressionEditor({
|
||||||
editorRef: sqlEditor,
|
editorRef: sqlEditor,
|
||||||
editorValue,
|
editorValue: () => props.modelValue,
|
||||||
extensions,
|
extensions,
|
||||||
skipSegments: ['Statement', 'CompositeIdentifier', 'Parens', 'Brackets'],
|
skipSegments: ['Statement', 'CompositeIdentifier', 'Parens', 'Brackets'],
|
||||||
isReadOnly: props.isReadOnly,
|
isReadOnly: props.isReadOnly,
|
||||||
});
|
onChange: () => {
|
||||||
|
emit('update:model-value', readEditorValue());
|
||||||
watch(
|
|
||||||
() => props.modelValue,
|
|
||||||
(newValue) => {
|
|
||||||
editorValue.value = newValue;
|
|
||||||
},
|
},
|
||||||
);
|
});
|
||||||
|
|
||||||
watch(editorHasFocus, (focus) => {
|
watch(editorHasFocus, (focus) => {
|
||||||
if (focus) {
|
if (focus) {
|
||||||
@@ -140,10 +134,6 @@ watch(editorHasFocus, (focus) => {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
watch(segments, () => {
|
|
||||||
emit('update:model-value', readEditorValue());
|
|
||||||
});
|
|
||||||
|
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
codeNodeEditorEventBus.on('highlightLine', highlightLine);
|
codeNodeEditorEventBus.on('highlightLine', highlightLine);
|
||||||
|
|
||||||
@@ -153,7 +143,6 @@ onMounted(() => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
onBeforeUnmount(() => {
|
onBeforeUnmount(() => {
|
||||||
if (isDirty.value) emit('update:model-value', readEditorValue());
|
|
||||||
codeNodeEditorEventBus.off('highlightLine', highlightLine);
|
codeNodeEditorEventBus.off('highlightLine', highlightLine);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -38,6 +38,7 @@ import { useRouter } from 'vue-router';
|
|||||||
import { useI18n } from '../composables/useI18n';
|
import { useI18n } from '../composables/useI18n';
|
||||||
import { useWorkflowsStore } from '../stores/workflows.store';
|
import { useWorkflowsStore } from '../stores/workflows.store';
|
||||||
import { useAutocompleteTelemetry } from './useAutocompleteTelemetry';
|
import { useAutocompleteTelemetry } from './useAutocompleteTelemetry';
|
||||||
|
import { ignoreUpdateAnnotation } from '../utils/forceParse';
|
||||||
|
|
||||||
export const useExpressionEditor = ({
|
export const useExpressionEditor = ({
|
||||||
editorRef,
|
editorRef,
|
||||||
@@ -47,6 +48,7 @@ export const useExpressionEditor = ({
|
|||||||
skipSegments = [],
|
skipSegments = [],
|
||||||
autocompleteTelemetry,
|
autocompleteTelemetry,
|
||||||
isReadOnly = false,
|
isReadOnly = false,
|
||||||
|
onChange = () => {},
|
||||||
}: {
|
}: {
|
||||||
editorRef: MaybeRefOrGetter<HTMLElement | undefined>;
|
editorRef: MaybeRefOrGetter<HTMLElement | undefined>;
|
||||||
editorValue?: MaybeRefOrGetter<string>;
|
editorValue?: MaybeRefOrGetter<string>;
|
||||||
@@ -55,6 +57,7 @@ export const useExpressionEditor = ({
|
|||||||
skipSegments?: MaybeRefOrGetter<string[]>;
|
skipSegments?: MaybeRefOrGetter<string[]>;
|
||||||
autocompleteTelemetry?: MaybeRefOrGetter<{ enabled: true; parameterPath: string }>;
|
autocompleteTelemetry?: MaybeRefOrGetter<{ enabled: true; parameterPath: string }>;
|
||||||
isReadOnly?: MaybeRefOrGetter<boolean>;
|
isReadOnly?: MaybeRefOrGetter<boolean>;
|
||||||
|
onChange?: (viewUpdate: ViewUpdate) => void;
|
||||||
}) => {
|
}) => {
|
||||||
const ndvStore = useNDVStore();
|
const ndvStore = useNDVStore();
|
||||||
const workflowsStore = useWorkflowsStore();
|
const workflowsStore = useWorkflowsStore();
|
||||||
@@ -70,7 +73,9 @@ 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 hasChanges = ref(false);
|
||||||
|
|
||||||
|
const emitChanges = debounce(onChange, 300);
|
||||||
|
|
||||||
const updateSegments = (): void => {
|
const updateSegments = (): void => {
|
||||||
const state = editor.value?.state;
|
const state = editor.value?.state;
|
||||||
@@ -157,13 +162,18 @@ 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);
|
||||||
|
|
||||||
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() {
|
function blur() {
|
||||||
@@ -265,6 +275,8 @@ export const useExpressionEditor = ({
|
|||||||
|
|
||||||
onBeforeUnmount(() => {
|
onBeforeUnmount(() => {
|
||||||
document.removeEventListener('click', blurOnClickOutside);
|
document.removeEventListener('click', blurOnClickOutside);
|
||||||
|
debouncedUpdateSegments.flush();
|
||||||
|
emitChanges.flush();
|
||||||
editor.value?.destroy();
|
editor.value?.destroy();
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -465,6 +477,6 @@ export const useExpressionEditor = ({
|
|||||||
select,
|
select,
|
||||||
selectAll,
|
selectAll,
|
||||||
focus,
|
focus,
|
||||||
isDirty,
|
hasChanges,
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|||||||
Reference in New Issue
Block a user