mirror of
https://github.com/n8n-io/n8n.git
synced 2025-03-05 20:50:17 -08:00
fix(editor): Fix Code node bug erasing and overwriting code when switching between nodes (#12637)
Some checks are pending
Test Master / install-and-build (push) Waiting to run
Test Master / Unit tests (18.x) (push) Blocked by required conditions
Test Master / Unit tests (20.x) (push) Blocked by required conditions
Test Master / Unit tests (22.4) (push) Blocked by required conditions
Test Master / Lint (push) Blocked by required conditions
Test Master / Notify Slack on failure (push) Blocked by required conditions
Some checks are pending
Test Master / install-and-build (push) Waiting to run
Test Master / Unit tests (18.x) (push) Blocked by required conditions
Test Master / Unit tests (20.x) (push) Blocked by required conditions
Test Master / Unit tests (22.4) (push) Blocked by required conditions
Test Master / Lint (push) Blocked by required conditions
Test Master / Notify Slack on failure (push) Blocked by required conditions
Co-authored-by: Elias Meire <elias@meire.dev>
This commit is contained in:
parent
0b0f532367
commit
02d953db34
|
@ -61,3 +61,25 @@ Object.defineProperty(window, 'matchMedia', {
|
||||||
dispatchEvent: vi.fn(),
|
dispatchEvent: vi.fn(),
|
||||||
})),
|
})),
|
||||||
});
|
});
|
||||||
|
|
||||||
|
class Worker {
|
||||||
|
onmessage: (message: string) => void;
|
||||||
|
|
||||||
|
url: string;
|
||||||
|
|
||||||
|
constructor(url: string) {
|
||||||
|
this.url = url;
|
||||||
|
this.onmessage = () => {};
|
||||||
|
}
|
||||||
|
|
||||||
|
postMessage(message: string) {
|
||||||
|
this.onmessage(message);
|
||||||
|
}
|
||||||
|
|
||||||
|
addEventListener() {}
|
||||||
|
}
|
||||||
|
|
||||||
|
Object.defineProperty(window, 'Worker', {
|
||||||
|
writable: true,
|
||||||
|
value: Worker,
|
||||||
|
});
|
||||||
|
|
103
packages/editor-ui/src/composables/useCodeEditor.test.ts
Normal file
103
packages/editor-ui/src/composables/useCodeEditor.test.ts
Normal file
|
@ -0,0 +1,103 @@
|
||||||
|
import { renderComponent } from '@/__tests__/render';
|
||||||
|
import { EditorView } from '@codemirror/view';
|
||||||
|
import { createTestingPinia } from '@pinia/testing';
|
||||||
|
import { waitFor } from '@testing-library/vue';
|
||||||
|
import { setActivePinia } from 'pinia';
|
||||||
|
import { beforeEach, describe, vi } from 'vitest';
|
||||||
|
import { defineComponent, h, ref, toValue } from 'vue';
|
||||||
|
import { useCodeEditor } from './useCodeEditor';
|
||||||
|
import userEvent from '@testing-library/user-event';
|
||||||
|
|
||||||
|
describe('useCodeEditor', () => {
|
||||||
|
const defaultOptions: Omit<Parameters<typeof useCodeEditor>[0], 'editorRef'> = {
|
||||||
|
language: 'javaScript',
|
||||||
|
};
|
||||||
|
|
||||||
|
const renderCodeEditor = async (options: Partial<typeof defaultOptions> = defaultOptions) => {
|
||||||
|
let codeEditor!: ReturnType<typeof useCodeEditor>;
|
||||||
|
const renderResult = renderComponent(
|
||||||
|
defineComponent({
|
||||||
|
setup() {
|
||||||
|
const root = ref<HTMLElement>();
|
||||||
|
codeEditor = useCodeEditor({ ...defaultOptions, ...options, editorRef: root });
|
||||||
|
|
||||||
|
return () => h('div', { ref: root, 'data-test-id': 'editor-root' });
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
{ props: { options } },
|
||||||
|
);
|
||||||
|
expect(renderResult.getByTestId('editor-root')).toBeInTheDocument();
|
||||||
|
await waitFor(() => toValue(codeEditor.editor));
|
||||||
|
return { renderResult, codeEditor };
|
||||||
|
};
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
setActivePinia(createTestingPinia());
|
||||||
|
});
|
||||||
|
|
||||||
|
afterEach(() => {
|
||||||
|
vi.clearAllMocks();
|
||||||
|
vi.useRealTimers();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should create an editor', async () => {
|
||||||
|
const { codeEditor } = await renderCodeEditor();
|
||||||
|
|
||||||
|
await waitFor(() => expect(toValue(codeEditor.editor)).toBeInstanceOf(EditorView));
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should focus editor', async () => {
|
||||||
|
const { renderResult, codeEditor } = await renderCodeEditor({});
|
||||||
|
|
||||||
|
const root = renderResult.getByTestId('editor-root');
|
||||||
|
const input = root.querySelector('.cm-line') as HTMLDivElement;
|
||||||
|
|
||||||
|
await userEvent.click(input);
|
||||||
|
|
||||||
|
expect(codeEditor.editor.value?.hasFocus).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should emit changes', async () => {
|
||||||
|
vi.useFakeTimers();
|
||||||
|
|
||||||
|
const user = userEvent.setup({
|
||||||
|
advanceTimers: vi.advanceTimersByTime,
|
||||||
|
});
|
||||||
|
|
||||||
|
const onChange = vi.fn();
|
||||||
|
const { renderResult } = await renderCodeEditor({
|
||||||
|
onChange,
|
||||||
|
});
|
||||||
|
|
||||||
|
const root = renderResult.getByTestId('editor-root');
|
||||||
|
const input = root.querySelector('.cm-line') as HTMLDivElement;
|
||||||
|
|
||||||
|
await user.type(input, 'test');
|
||||||
|
|
||||||
|
vi.advanceTimersByTime(300);
|
||||||
|
|
||||||
|
expect(onChange.mock.calls[0][0].state.doc.toString()).toEqual('test');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should emit debounced changes before unmount', async () => {
|
||||||
|
vi.useFakeTimers();
|
||||||
|
|
||||||
|
const user = userEvent.setup({
|
||||||
|
advanceTimers: vi.advanceTimersByTime,
|
||||||
|
});
|
||||||
|
|
||||||
|
const onChange = vi.fn();
|
||||||
|
const { renderResult } = await renderCodeEditor({
|
||||||
|
onChange,
|
||||||
|
});
|
||||||
|
|
||||||
|
const root = renderResult.getByTestId('editor-root');
|
||||||
|
const input = root.querySelector('.cm-line') as HTMLDivElement;
|
||||||
|
|
||||||
|
await user.type(input, 'test');
|
||||||
|
|
||||||
|
renderResult.unmount();
|
||||||
|
|
||||||
|
expect(onChange.mock.calls[0][0].state.doc.toString()).toEqual('test');
|
||||||
|
});
|
||||||
|
});
|
|
@ -30,7 +30,6 @@ import {
|
||||||
} from '@codemirror/view';
|
} from '@codemirror/view';
|
||||||
import { indentationMarkers } from '@replit/codemirror-indentation-markers';
|
import { indentationMarkers } from '@replit/codemirror-indentation-markers';
|
||||||
import { html } from 'codemirror-lang-html-n8n';
|
import { html } from 'codemirror-lang-html-n8n';
|
||||||
import { debounce } from 'lodash-es';
|
|
||||||
import { jsonParse, type CodeExecutionMode, type IDataObject } from 'n8n-workflow';
|
import { jsonParse, type CodeExecutionMode, type IDataObject } from 'n8n-workflow';
|
||||||
import { v4 as uuid } from 'uuid';
|
import { v4 as uuid } from 'uuid';
|
||||||
import {
|
import {
|
||||||
|
@ -47,6 +46,8 @@ import {
|
||||||
import { useCompleter } from '../components/CodeNodeEditor/completer';
|
import { useCompleter } from '../components/CodeNodeEditor/completer';
|
||||||
import { mappingDropCursor } from '../plugins/codemirror/dragAndDrop';
|
import { mappingDropCursor } from '../plugins/codemirror/dragAndDrop';
|
||||||
import { languageFacet, type CodeEditorLanguage } from '../plugins/codemirror/format';
|
import { languageFacet, type CodeEditorLanguage } from '../plugins/codemirror/format';
|
||||||
|
import { debounce } from 'lodash-es';
|
||||||
|
import { ignoreUpdateAnnotation } from '../utils/forceParse';
|
||||||
|
|
||||||
export type CodeEditorLanguageParamsMap = {
|
export type CodeEditorLanguageParamsMap = {
|
||||||
json: {};
|
json: {};
|
||||||
|
@ -85,7 +86,6 @@ export const useCodeEditor = <L extends CodeEditorLanguage>({
|
||||||
const editor = ref<EditorView>();
|
const editor = ref<EditorView>();
|
||||||
const hasFocus = ref(false);
|
const hasFocus = ref(false);
|
||||||
const hasChanges = ref(false);
|
const hasChanges = ref(false);
|
||||||
const lastChange = ref<ViewUpdate>();
|
|
||||||
const selection = ref<SelectionRange>(EditorSelection.cursor(0)) as Ref<SelectionRange>;
|
const selection = ref<SelectionRange>(EditorSelection.cursor(0)) as Ref<SelectionRange>;
|
||||||
const customExtensions = ref<Compartment>(new Compartment());
|
const customExtensions = ref<Compartment>(new Compartment());
|
||||||
const readOnlyExtensions = ref<Compartment>(new Compartment());
|
const readOnlyExtensions = ref<Compartment>(new Compartment());
|
||||||
|
@ -157,14 +157,19 @@ export const useCodeEditor = <L extends CodeEditorLanguage>({
|
||||||
const emitChanges = debounce((update: ViewUpdate) => {
|
const emitChanges = debounce((update: ViewUpdate) => {
|
||||||
onChange(update);
|
onChange(update);
|
||||||
}, 300);
|
}, 300);
|
||||||
|
const lastChange = ref<ViewUpdate>();
|
||||||
|
|
||||||
function onEditorUpdate(update: ViewUpdate) {
|
function onEditorUpdate(update: ViewUpdate) {
|
||||||
autocompleteStatus.value = completionStatus(update.view.state);
|
autocompleteStatus.value = completionStatus(update.view.state);
|
||||||
updateSelection(update);
|
updateSelection(update);
|
||||||
|
|
||||||
if (update.docChanged) {
|
const shouldIgnoreUpdate = update.transactions.some((tr) =>
|
||||||
hasChanges.value = true;
|
tr.annotation(ignoreUpdateAnnotation),
|
||||||
|
);
|
||||||
|
|
||||||
|
if (update.docChanged && !shouldIgnoreUpdate) {
|
||||||
lastChange.value = update;
|
lastChange.value = update;
|
||||||
|
hasChanges.value = true;
|
||||||
emitChanges(update);
|
emitChanges(update);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -369,7 +374,10 @@ export const useCodeEditor = <L extends CodeEditorLanguage>({
|
||||||
// Code is too large, localStorage quota exceeded
|
// Code is too large, localStorage quota exceeded
|
||||||
localStorage.removeItem(storedStateId.value);
|
localStorage.removeItem(storedStateId.value);
|
||||||
}
|
}
|
||||||
if (lastChange.value) onChange(lastChange.value);
|
|
||||||
|
if (lastChange.value) {
|
||||||
|
onChange(lastChange.value);
|
||||||
|
}
|
||||||
editor.value.destroy();
|
editor.value.destroy();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
|
@ -1,14 +1,19 @@
|
||||||
|
import { Annotation } from '@codemirror/state';
|
||||||
import type { EditorView } from '@codemirror/view';
|
import type { EditorView } from '@codemirror/view';
|
||||||
|
|
||||||
|
export const ignoreUpdateAnnotation = Annotation.define<boolean>();
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Simulate user action to force parser to catch up during scroll.
|
* Simulate user action to force parser to catch up during scroll.
|
||||||
*/
|
*/
|
||||||
export function forceParse(view: EditorView) {
|
export function forceParse(view: EditorView) {
|
||||||
view.dispatch({
|
view.dispatch({
|
||||||
changes: { from: view.viewport.to, insert: '_' },
|
changes: { from: view.viewport.to, insert: '_' },
|
||||||
|
annotations: [ignoreUpdateAnnotation.of(true)],
|
||||||
});
|
});
|
||||||
|
|
||||||
view.dispatch({
|
view.dispatch({
|
||||||
changes: { from: view.viewport.to - 1, to: view.viewport.to, insert: '' },
|
changes: { from: view.viewport.to - 1, to: view.viewport.to, insert: '' },
|
||||||
|
annotations: [ignoreUpdateAnnotation.of(true)],
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in a new issue