From b0243ffc91e6e5613a6506162d728c4c8b96193c Mon Sep 17 00:00:00 2001 From: Elias Meire Date: Tue, 28 May 2024 11:06:55 +0200 Subject: [PATCH 1/5] Prevent editor focus being lost when user is selecting --- .../__tests__/useExpressionEditor.test.ts | 166 +++++++++--------- .../src/composables/useExpressionEditor.ts | 9 +- 2 files changed, 94 insertions(+), 81 deletions(-) diff --git a/packages/editor-ui/src/composables/__tests__/useExpressionEditor.test.ts b/packages/editor-ui/src/composables/__tests__/useExpressionEditor.test.ts index 916b80952559e..0aef8ed7b691a 100644 --- a/packages/editor-ui/src/composables/__tests__/useExpressionEditor.test.ts +++ b/packages/editor-ui/src/composables/__tests__/useExpressionEditor.test.ts @@ -1,14 +1,16 @@ import * as workflowHelpers from '@/composables/useWorkflowHelpers'; import { EditorView } from '@codemirror/view'; import { createTestingPinia } from '@pinia/testing'; -import { waitFor } from '@testing-library/vue'; +import { waitFor, fireEvent } from '@testing-library/vue'; import { setActivePinia } from 'pinia'; import { beforeEach, describe, vi } from 'vitest'; -import { ref, toValue } from 'vue'; +import { defineComponent, h, ref, toValue } from 'vue'; import { n8nLang } from '../../plugins/codemirror/n8nLang'; import { useExpressionEditor } from '../useExpressionEditor'; import { useRouter } from 'vue-router'; import { EditorSelection } from '@codemirror/state'; +import userEvent from '@testing-library/user-event'; +import { renderComponent } from '../../__tests__/render'; vi.mock('@/composables/useAutocompleteTelemetry', () => ({ useAutocompleteTelemetry: vi.fn(), @@ -31,6 +33,26 @@ describe('useExpressionEditor', () => { return mock; }; + const renderExpressionEditor = async ( + options: Omit[0], 'editorRef'> = {}, + ) => { + let expressionEditor!: ReturnType; + const renderResult = renderComponent( + defineComponent({ + setup() { + const root = ref(); + expressionEditor = useExpressionEditor({ ...options, editorRef: root }); + + return () => h('div', { ref: root, 'data-test-id': 'editor-root' }); + }, + }), + { props: { options } }, + ); + expect(renderResult.getByTestId('editor-root')).toBeInTheDocument(); + await waitFor(() => toValue(expressionEditor.editor)); + return { renderResult, expressionEditor }; + }; + beforeEach(() => { setActivePinia(createTestingPinia()); }); @@ -40,27 +62,21 @@ describe('useExpressionEditor', () => { }); test('should create an editor', async () => { - const root = ref(); - const { editor } = useExpressionEditor({ - editorRef: root, - }); - - root.value = document.createElement('div'); + const { expressionEditor } = await renderExpressionEditor(); - await waitFor(() => expect(toValue(editor)).toBeInstanceOf(EditorView)); + await waitFor(() => expect(toValue(expressionEditor.editor)).toBeInstanceOf(EditorView)); }); test('should calculate segments', async () => { mockResolveExpression().mockReturnValueOnce(15); - const root = ref(); - const { segments } = useExpressionEditor({ - editorRef: root, + + const { + expressionEditor: { segments }, + } = await renderExpressionEditor({ editorValue: 'before {{ $json.test.length }} after', extensions: [n8nLang()], }); - root.value = document.createElement('div'); - await waitFor(() => { expect(toValue(segments.all)).toEqual([ { @@ -118,134 +134,124 @@ describe('useExpressionEditor', () => { describe('readEditorValue()', () => { test('should return the full editor value (unresolved)', async () => { mockResolveExpression().mockReturnValueOnce(15); - const root = ref(); - const { readEditorValue } = useExpressionEditor({ - editorRef: root, + const { + expressionEditor: { readEditorValue }, + } = await renderExpressionEditor({ editorValue: 'before {{ $json.test.length }} after', extensions: [n8nLang()], }); - root.value = document.createElement('div'); - - await waitFor(() => - expect(readEditorValue()).toEqual('before {{ $json.test.length }} after'), - ); + expect(readEditorValue()).toEqual('before {{ $json.test.length }} after'); }); }); describe('setCursorPosition()', () => { test('should set cursor position to number correctly', async () => { - const root = ref(); const editorValue = 'text here'; - const { editor, setCursorPosition } = useExpressionEditor({ - editorRef: root, + const { + expressionEditor: { editor, setCursorPosition }, + } = await renderExpressionEditor({ editorValue, - extensions: [], }); - root.value = document.createElement('div'); - await waitFor(() => toValue(editor)); setCursorPosition(4); - - await waitFor(() => - expect(toValue(editor)?.state.selection).toEqual(EditorSelection.single(4)), - ); + expect(toValue(editor)?.state.selection).toEqual(EditorSelection.single(4)); }); test('should set cursor position to end correctly', async () => { - const root = ref(); const editorValue = 'text here'; const correctPosition = editorValue.length; - const { editor, setCursorPosition } = useExpressionEditor({ - editorRef: root, + const { + expressionEditor: { editor, setCursorPosition }, + } = await renderExpressionEditor({ editorValue, - extensions: [], }); - root.value = document.createElement('div'); - await waitFor(() => toValue(editor)); setCursorPosition('end'); - - await waitFor(() => - expect(toValue(editor)?.state.selection).toEqual(EditorSelection.single(correctPosition)), - ); + expect(toValue(editor)?.state.selection).toEqual(EditorSelection.single(correctPosition)); }); test('should set cursor position to last expression correctly', async () => { - const root = ref(); const editorValue = 'text {{ $json.foo }} {{ $json.bar }} here'; const correctPosition = editorValue.indexOf('bar') + 'bar'.length; - const { editor, setCursorPosition } = useExpressionEditor({ - editorRef: root, + const { + expressionEditor: { editor, setCursorPosition }, + } = await renderExpressionEditor({ editorValue, extensions: [n8nLang()], }); - root.value = document.createElement('div'); - await waitFor(() => toValue(editor)); setCursorPosition('lastExpression'); - - await waitFor(() => - expect(toValue(editor)?.state.selection).toEqual(EditorSelection.single(correctPosition)), - ); + expect(toValue(editor)?.state.selection).toEqual(EditorSelection.single(correctPosition)); }); }); describe('select()', () => { test('should select number range', async () => { - const root = ref(); const editorValue = 'text here'; - const { editor, select } = useExpressionEditor({ - editorRef: root, + const { + expressionEditor: { editor, select }, + } = await renderExpressionEditor({ editorValue, - extensions: [], }); - root.value = document.createElement('div'); - await waitFor(() => toValue(editor)); select(4, 7); - - await waitFor(() => - expect(toValue(editor)?.state.selection).toEqual(EditorSelection.single(4, 7)), - ); + expect(toValue(editor)?.state.selection).toEqual(EditorSelection.single(4, 7)); }); test('should select until end', async () => { - const root = ref(); const editorValue = 'text here'; - const { editor, select } = useExpressionEditor({ - editorRef: root, + const { + expressionEditor: { editor, select }, + } = await renderExpressionEditor({ editorValue, - extensions: [], }); - root.value = document.createElement('div'); - await waitFor(() => toValue(editor)); select(4, 'end'); - - await waitFor(() => - expect(toValue(editor)?.state.selection).toEqual(EditorSelection.single(4, 9)), - ); + expect(toValue(editor)?.state.selection).toEqual(EditorSelection.single(4, 9)); }); }); describe('selectAll()', () => { test('should select all', async () => { - const root = ref(); const editorValue = 'text here'; - const { editor, selectAll } = useExpressionEditor({ - editorRef: root, + const { + expressionEditor: { editor, selectAll }, + } = await renderExpressionEditor({ editorValue, - extensions: [], }); - root.value = document.createElement('div'); - await waitFor(() => toValue(editor)); selectAll(); + expect(toValue(editor)?.state.selection).toEqual(EditorSelection.single(0, 9)); + }); + }); + + describe('blur on click outside', () => { + test('should blur when another element is clicked', async () => { + const { renderResult, expressionEditor } = await renderExpressionEditor(); + + const root = renderResult.getByTestId('editor-root'); + const input = root.querySelector('.cm-line') as HTMLDivElement; + + await userEvent.click(input); + expect(expressionEditor.editor.value?.hasFocus).toBe(true); + + await fireEvent(document, new MouseEvent('click')); + expect(expressionEditor.editor.value?.hasFocus).toBe(false); + }); + + test('should NOT blur when another element is clicked while selecting', async () => { + const { renderResult, expressionEditor } = await renderExpressionEditor(); + + const root = renderResult.getByTestId('editor-root'); + const input = root.querySelector('.cm-line') as HTMLDivElement; + + await userEvent.click(input); + expect(expressionEditor.editor.value?.hasFocus).toBe(true); + await fireEvent(input, new MouseEvent('mousedown', { bubbles: true })); - await waitFor(() => - expect(toValue(editor)?.state.selection).toEqual(EditorSelection.single(0, 9)), - ); + await fireEvent(document, new MouseEvent('click')); + expect(expressionEditor.editor.value?.hasFocus).toBe(true); }); }); }); diff --git a/packages/editor-ui/src/composables/useExpressionEditor.ts b/packages/editor-ui/src/composables/useExpressionEditor.ts index c7017aa05c49d..8f615ef2f509d 100644 --- a/packages/editor-ui/src/composables/useExpressionEditor.ts +++ b/packages/editor-ui/src/composables/useExpressionEditor.ts @@ -71,6 +71,7 @@ export const useExpressionEditor = ({ const readOnlyExtensions = ref(new Compartment()); const telemetryExtensions = ref(new Compartment()); const autocompleteStatus = ref<'pending' | 'active' | null>(null); + const dragging = ref(false); const updateSegments = (): void => { const state = editor.value?.state; @@ -167,9 +168,10 @@ export const useExpressionEditor = ({ } function blurOnClickOutside(event: MouseEvent) { - if (event.target && !editor.value?.dom.contains(event.target as Node)) { + if (event.target && !dragging.value && !editor.value?.dom.contains(event.target as Node)) { blur(); } + dragging.value = false; } watch(editorRef, () => { @@ -197,6 +199,11 @@ export const useExpressionEditor = ({ return null; }), EditorView.contentAttributes.of({ 'data-gramm': 'false' }), // disable grammarly + EditorView.domEventHandlers({ + mousedown: () => { + dragging.value = true; + }, + }), ], }); From 265094359f78167920cd287ebe218aa868169cae Mon Sep 17 00:00:00 2001 From: Elias Meire Date: Tue, 28 May 2024 11:07:03 +0200 Subject: [PATCH 2/5] Update codemirror dependencies --- packages/editor-ui/package.json | 16 ++-- pnpm-lock.yaml | 161 ++++++++++++++++++++------------ 2 files changed, 109 insertions(+), 68 deletions(-) diff --git a/packages/editor-ui/package.json b/packages/editor-ui/package.json index c3a123b8a13ff..e331a188f96c2 100644 --- a/packages/editor-ui/package.json +++ b/packages/editor-ui/package.json @@ -26,15 +26,15 @@ "test:dev": "vitest" }, "dependencies": { - "@codemirror/autocomplete": "^6.11.1", - "@codemirror/commands": "^6.3.2", - "@codemirror/lang-javascript": "^6.2.1", + "@codemirror/autocomplete": "^6.16.0", + "@codemirror/commands": "^6.5.0", + "@codemirror/lang-javascript": "^6.2.2", "@codemirror/lang-json": "^6.0.1", - "@codemirror/lang-python": "^6.1.3", - "@codemirror/language": "^6.9.3", - "@codemirror/lint": "^6.4.2", - "@codemirror/state": "^6.3.3", - "@codemirror/view": "^6.22.3", + "@codemirror/lang-python": "^6.1.6", + "@codemirror/language": "^6.10.1", + "@codemirror/lint": "^6.8.0", + "@codemirror/state": "^6.4.1", + "@codemirror/view": "^6.26.3", "@fontsource/open-sans": "^4.5.0", "@jsplumb/browser-ui": "^5.13.2", "@jsplumb/common": "^5.13.2", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 091cf59f30b46..a521b767c60de 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -1069,32 +1069,32 @@ importers: packages/editor-ui: dependencies: '@codemirror/autocomplete': - specifier: ^6.11.1 - version: 6.11.1(@codemirror/language@6.9.3)(@codemirror/state@6.3.3)(@codemirror/view@6.22.3)(@lezer/common@1.1.0) + specifier: ^6.16.0 + version: 6.16.0(@codemirror/language@6.10.1)(@codemirror/state@6.4.1)(@codemirror/view@6.26.3)(@lezer/common@1.1.0) '@codemirror/commands': - specifier: ^6.3.2 - version: 6.3.2 + specifier: ^6.5.0 + version: 6.5.0 '@codemirror/lang-javascript': - specifier: ^6.2.1 - version: 6.2.1 + specifier: ^6.2.2 + version: 6.2.2 '@codemirror/lang-json': specifier: ^6.0.1 version: 6.0.1 '@codemirror/lang-python': - specifier: ^6.1.3 - version: 6.1.3(@codemirror/state@6.3.3)(@codemirror/view@6.22.3)(@lezer/common@1.1.0) + specifier: ^6.1.6 + version: 6.1.6(@codemirror/view@6.26.3) '@codemirror/language': - specifier: ^6.9.3 - version: 6.9.3 + specifier: ^6.10.1 + version: 6.10.1 '@codemirror/lint': - specifier: ^6.4.2 - version: 6.4.2 + specifier: ^6.8.0 + version: 6.8.0 '@codemirror/state': - specifier: ^6.3.3 - version: 6.3.3 + specifier: ^6.4.1 + version: 6.4.1 '@codemirror/view': - specifier: ^6.22.3 - version: 6.22.3 + specifier: ^6.26.3 + version: 6.26.3 '@fontsource/open-sans': specifier: ^4.5.0 version: 4.5.12 @@ -1136,7 +1136,7 @@ importers: version: link:../@n8n/codemirror-lang '@n8n/codemirror-lang-sql': specifier: ^1.0.2 - version: 1.0.2(@codemirror/view@6.22.3)(@lezer/common@1.1.0) + version: 1.0.2(@codemirror/view@6.26.3)(@lezer/common@1.1.0) '@n8n/permissions': specifier: workspace:* version: link:../@n8n/permissions @@ -4670,49 +4670,63 @@ packages: - react dev: true - /@codemirror/autocomplete@6.11.1(@codemirror/language@6.9.3)(@codemirror/state@6.3.3)(@codemirror/view@6.22.3)(@lezer/common@1.1.0): - resolution: {integrity: sha512-L5UInv8Ffd6BPw0P3EF7JLYAMeEbclY7+6Q11REt8vhih8RuLreKtPy/xk8wPxs4EQgYqzI7cdgpiYwWlbS/ow==} + /@codemirror/autocomplete@6.16.0(@codemirror/language@6.10.1)(@codemirror/state@6.4.1)(@codemirror/view@6.26.3)(@lezer/common@1.1.0): + resolution: {integrity: sha512-P/LeCTtZHRTCU4xQsa89vSKWecYv1ZqwzOd5topheGRf+qtacFgBeIMQi3eL8Kt/BUNvxUWkx+5qP2jlGoARrg==} peerDependencies: '@codemirror/language': ^6.0.0 '@codemirror/state': ^6.0.0 '@codemirror/view': ^6.0.0 '@lezer/common': ^1.0.0 dependencies: - '@codemirror/language': 6.9.3 - '@codemirror/state': 6.3.3 - '@codemirror/view': 6.22.3 + '@codemirror/language': 6.10.1 + '@codemirror/state': 6.4.1 + '@codemirror/view': 6.26.3 '@lezer/common': 1.1.0 dev: false - /@codemirror/commands@6.3.2: - resolution: {integrity: sha512-tjoi4MCWDNxgIpoLZ7+tezdS9OEB6pkiDKhfKx9ReJ/XBcs2G2RXIu+/FxXBlWsPTsz6C9q/r4gjzrsxpcnqCQ==} + /@codemirror/autocomplete@6.16.0(@codemirror/language@6.10.1)(@codemirror/state@6.4.1)(@codemirror/view@6.26.3)(@lezer/common@1.2.1): + resolution: {integrity: sha512-P/LeCTtZHRTCU4xQsa89vSKWecYv1ZqwzOd5topheGRf+qtacFgBeIMQi3eL8Kt/BUNvxUWkx+5qP2jlGoARrg==} + peerDependencies: + '@codemirror/language': ^6.0.0 + '@codemirror/state': ^6.0.0 + '@codemirror/view': ^6.0.0 + '@lezer/common': ^1.0.0 dependencies: - '@codemirror/language': 6.9.3 - '@codemirror/state': 6.3.3 - '@codemirror/view': 6.22.3 + '@codemirror/language': 6.10.1 + '@codemirror/state': 6.4.1 + '@codemirror/view': 6.26.3 + '@lezer/common': 1.2.1 + dev: false + + /@codemirror/commands@6.5.0: + resolution: {integrity: sha512-rK+sj4fCAN/QfcY9BEzYMgp4wwL/q5aj/VfNSoH1RWPF9XS/dUwBkvlL3hpWgEjOqlpdN1uLC9UkjJ4tmyjJYg==} + dependencies: + '@codemirror/language': 6.10.1 + '@codemirror/state': 6.4.1 + '@codemirror/view': 6.26.3 '@lezer/common': 1.1.0 dev: false - /@codemirror/lang-css@6.0.1(@codemirror/view@6.22.3)(@lezer/common@1.1.0): + /@codemirror/lang-css@6.0.1(@codemirror/view@6.26.3)(@lezer/common@1.1.0): resolution: {integrity: sha512-rlLq1Dt0WJl+2epLQeAsfqIsx3lGu4HStHCJu95nGGuz2P2fNugbU3dQYafr2VRjM4eMC9HviI6jvS98CNtG5w==} dependencies: - '@codemirror/autocomplete': 6.11.1(@codemirror/language@6.9.3)(@codemirror/state@6.3.3)(@codemirror/view@6.22.3)(@lezer/common@1.1.0) - '@codemirror/language': 6.9.3 - '@codemirror/state': 6.3.3 + '@codemirror/autocomplete': 6.16.0(@codemirror/language@6.10.1)(@codemirror/state@6.4.1)(@codemirror/view@6.26.3)(@lezer/common@1.1.0) + '@codemirror/language': 6.10.1 + '@codemirror/state': 6.4.1 '@lezer/css': 1.1.1 transitivePeerDependencies: - '@codemirror/view' - '@lezer/common' dev: false - /@codemirror/lang-javascript@6.2.1: - resolution: {integrity: sha512-jlFOXTejVyiQCW3EQwvKH0m99bUYIw40oPmFjSX2VS78yzfe0HELZ+NEo9Yfo1MkGRpGlj3Gnu4rdxV1EnAs5A==} + /@codemirror/lang-javascript@6.2.2: + resolution: {integrity: sha512-VGQfY+FCc285AhWuwjYxQyUQcYurWlxdKYT4bqwr3Twnd5wP5WSeu52t4tvvuWmljT4EmgEgZCqSieokhtY8hg==} dependencies: - '@codemirror/autocomplete': 6.11.1(@codemirror/language@6.9.3)(@codemirror/state@6.3.3)(@codemirror/view@6.22.3)(@lezer/common@1.1.0) - '@codemirror/language': 6.9.3 - '@codemirror/lint': 6.4.2 - '@codemirror/state': 6.3.3 - '@codemirror/view': 6.22.3 + '@codemirror/autocomplete': 6.16.0(@codemirror/language@6.10.1)(@codemirror/state@6.4.1)(@codemirror/view@6.26.3)(@lezer/common@1.1.0) + '@codemirror/language': 6.10.1 + '@codemirror/lint': 6.8.0 + '@codemirror/state': 6.4.1 + '@codemirror/view': 6.26.3 '@lezer/common': 1.1.0 '@lezer/javascript': 1.0.2 dev: false @@ -4720,20 +4734,31 @@ packages: /@codemirror/lang-json@6.0.1: resolution: {integrity: sha512-+T1flHdgpqDDlJZ2Lkil/rLiRy684WMLc74xUnjJH48GQdfJo/pudlTRreZmKwzP8/tGdKf83wlbAdOCzlJOGQ==} dependencies: - '@codemirror/language': 6.9.3 + '@codemirror/language': 6.10.1 '@lezer/json': 1.0.0 dev: false - /@codemirror/lang-python@6.1.3(@codemirror/state@6.3.3)(@codemirror/view@6.22.3)(@lezer/common@1.1.0): - resolution: {integrity: sha512-S9w2Jl74hFlD5nqtUMIaXAq9t5WlM0acCkyuQWUUSvZclk1sV+UfnpFiZzuZSG+hfEaOmxKR5UxY/Uxswn7EhQ==} + /@codemirror/lang-python@6.1.6(@codemirror/view@6.26.3): + resolution: {integrity: sha512-ai+01WfZhWqM92UqjnvorkxosZ2aq2u28kHvr+N3gu012XqY2CThD67JPMHnGceRfXPDBmn1HnyqowdpF57bNg==} dependencies: - '@codemirror/autocomplete': 6.11.1(@codemirror/language@6.9.3)(@codemirror/state@6.3.3)(@codemirror/view@6.22.3)(@lezer/common@1.1.0) - '@codemirror/language': 6.9.3 + '@codemirror/autocomplete': 6.16.0(@codemirror/language@6.10.1)(@codemirror/state@6.4.1)(@codemirror/view@6.26.3)(@lezer/common@1.2.1) + '@codemirror/language': 6.10.1 + '@codemirror/state': 6.4.1 + '@lezer/common': 1.2.1 '@lezer/python': 1.1.5 transitivePeerDependencies: - - '@codemirror/state' - '@codemirror/view' - - '@lezer/common' + dev: false + + /@codemirror/language@6.10.1: + resolution: {integrity: sha512-5GrXzrhq6k+gL5fjkAwt90nYDmjlzTIJV8THnxNFtNKWotMIlzzN+CpqxqwXOECnUdOndmSeWntVrVcv5axWRQ==} + dependencies: + '@codemirror/state': 6.4.1 + '@codemirror/view': 6.26.3 + '@lezer/common': 1.1.0 + '@lezer/highlight': 1.1.1 + '@lezer/lr': 1.4.0 + style-mod: 4.1.0 dev: false /@codemirror/language@6.9.3: @@ -4747,11 +4772,11 @@ packages: style-mod: 4.1.0 dev: false - /@codemirror/lint@6.4.2: - resolution: {integrity: sha512-wzRkluWb1ptPKdzlsrbwwjYCPLgzU6N88YBAmlZi8WFyuiEduSd05MnJYNogzyc8rPK7pj6m95ptUApc8sHKVA==} + /@codemirror/lint@6.8.0: + resolution: {integrity: sha512-lsFofvaw0lnPRJlQylNsC4IRt/1lI4OD/yYslrSGVndOJfStc58v+8p9dgGiD90ktOfL7OhBWns1ZETYgz0EJA==} dependencies: - '@codemirror/state': 6.3.3 - '@codemirror/view': 6.22.3 + '@codemirror/state': 6.4.1 + '@codemirror/view': 6.26.3 crelt: 1.0.5 dev: false @@ -4759,10 +4784,22 @@ packages: resolution: {integrity: sha512-0wufKcTw2dEwEaADajjHf6hBy1sh3M6V0e+q4JKIhLuiMSe5td5HOWpUdvKth1fT1M9VYOboajoBHpkCd7PG7A==} dev: false + /@codemirror/state@6.4.1: + resolution: {integrity: sha512-QkEyUiLhsJoZkbumGZlswmAhA7CBU02Wrz7zvH4SrcifbsqwlXShVXg65f3v/ts57W3dqyamEriMhij1Z3Zz4A==} + dev: false + /@codemirror/view@6.22.3: resolution: {integrity: sha512-rqnq+Zospwoi3x1vZ8BGV1MlRsaGljX+6qiGYmIpJ++M+LCC+wjfDaPklhwpWSgv7pr/qx29KiAKQBH5+DOn4w==} dependencies: - '@codemirror/state': 6.3.3 + '@codemirror/state': 6.4.1 + style-mod: 4.1.0 + w3c-keyname: 2.2.6 + dev: false + + /@codemirror/view@6.26.3: + resolution: {integrity: sha512-gmqxkPALZjkgSxIeeweY/wGQXBfwTUaLs8h7OKtSwfbj9Ct3L11lD+u1sS7XHppxFQoMDiMDp07P9f3I2jWOHw==} + dependencies: + '@codemirror/state': 6.4.1 style-mod: 4.1.0 w3c-keyname: 2.2.6 dev: false @@ -6666,6 +6703,10 @@ packages: /@lezer/common@1.1.0: resolution: {integrity: sha512-XPIN3cYDXsoJI/oDWoR2tD++juVrhgIago9xyKhZ7IhGlzdDM9QgC8D8saKNCz5pindGcznFr2HBSsEQSWnSjw==} + /@lezer/common@1.2.1: + resolution: {integrity: sha512-yemX0ZD2xS/73llMZIK6KplkjIjf2EvAHcinDi/TfJ9hS25G0388+ClHt6/3but0oOxinTcQHJLDXh6w1crzFQ==} + dev: false + /@lezer/css@1.1.1: resolution: {integrity: sha512-mSjx+unLLapEqdOYDejnGBokB5+AiJKZVclmud0MKQOKx3DLJ5b5VTCstgDDknR6iIV4gVrN6euzsCnj0A2gQA==} dependencies: @@ -6835,12 +6876,12 @@ packages: dev: false optional: true - /@n8n/codemirror-lang-sql@1.0.2(@codemirror/view@6.22.3)(@lezer/common@1.1.0): + /@n8n/codemirror-lang-sql@1.0.2(@codemirror/view@6.26.3)(@lezer/common@1.1.0): resolution: {integrity: sha512-sOf/KyewSu3Ikij0CkRtzJJDhRDZcwNCEYl8UdH4U/riL0/XZGcBD7MYofCCcKszanJZiEWRZ2KU1sRp234iMg==} dependencies: - '@codemirror/autocomplete': 6.11.1(@codemirror/language@6.9.3)(@codemirror/state@6.3.3)(@codemirror/view@6.22.3)(@lezer/common@1.1.0) - '@codemirror/language': 6.9.3 - '@codemirror/state': 6.3.3 + '@codemirror/autocomplete': 6.16.0(@codemirror/language@6.10.1)(@codemirror/state@6.4.1)(@codemirror/view@6.26.3)(@lezer/common@1.1.0) + '@codemirror/language': 6.10.1 + '@codemirror/state': 6.4.1 '@lezer/highlight': 1.1.1 '@lezer/lr': 1.4.0 transitivePeerDependencies: @@ -12662,12 +12703,12 @@ packages: /codemirror-lang-html-n8n@1.0.0: resolution: {integrity: sha512-ofNP6VTDGJ5rue+kTCZlDZdF1PnE0sl2cAkfrsCAd5MlBgDmqTwuFJIkTI6KXOJXs0ucdTYH6QLhy9BSW7EaOQ==} dependencies: - '@codemirror/autocomplete': 6.11.1(@codemirror/language@6.9.3)(@codemirror/state@6.3.3)(@codemirror/view@6.22.3)(@lezer/common@1.1.0) - '@codemirror/lang-css': 6.0.1(@codemirror/view@6.22.3)(@lezer/common@1.1.0) - '@codemirror/lang-javascript': 6.2.1 - '@codemirror/language': 6.9.3 - '@codemirror/state': 6.3.3 - '@codemirror/view': 6.22.3 + '@codemirror/autocomplete': 6.16.0(@codemirror/language@6.10.1)(@codemirror/state@6.4.1)(@codemirror/view@6.26.3)(@lezer/common@1.1.0) + '@codemirror/lang-css': 6.0.1(@codemirror/view@6.26.3)(@lezer/common@1.1.0) + '@codemirror/lang-javascript': 6.2.2 + '@codemirror/language': 6.10.1 + '@codemirror/state': 6.4.1 + '@codemirror/view': 6.26.3 '@lezer/common': 1.1.0 '@lezer/css': 1.1.1 '@lezer/highlight': 1.1.1 From 9bff86387452159a29b6d141979555c5763b6d41 Mon Sep 17 00:00:00 2001 From: Elias Meire Date: Tue, 28 May 2024 11:08:50 +0200 Subject: [PATCH 3/5] Use absolute imports --- .../src/composables/__tests__/useExpressionEditor.test.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/editor-ui/src/composables/__tests__/useExpressionEditor.test.ts b/packages/editor-ui/src/composables/__tests__/useExpressionEditor.test.ts index 0aef8ed7b691a..35ceb88bde588 100644 --- a/packages/editor-ui/src/composables/__tests__/useExpressionEditor.test.ts +++ b/packages/editor-ui/src/composables/__tests__/useExpressionEditor.test.ts @@ -5,12 +5,12 @@ import { waitFor, fireEvent } from '@testing-library/vue'; import { setActivePinia } from 'pinia'; import { beforeEach, describe, vi } from 'vitest'; import { defineComponent, h, ref, toValue } from 'vue'; -import { n8nLang } from '../../plugins/codemirror/n8nLang'; +import { n8nLang } from '@/plugins/codemirror/n8nLang'; import { useExpressionEditor } from '../useExpressionEditor'; import { useRouter } from 'vue-router'; import { EditorSelection } from '@codemirror/state'; import userEvent from '@testing-library/user-event'; -import { renderComponent } from '../../__tests__/render'; +import { renderComponent } from '@/__tests__/render'; vi.mock('@/composables/useAutocompleteTelemetry', () => ({ useAutocompleteTelemetry: vi.fn(), From 6c203713703c183a76c63fef5b05a76a38099882 Mon Sep 17 00:00:00 2001 From: Elias Meire Date: Tue, 28 May 2024 17:10:18 +0200 Subject: [PATCH 4/5] Fix e2e tests --- cypress/e2e/11-inline-expression-editor.cy.ts | 28 +++++++++---------- cypress/e2e/9-expression-editor-modal.cy.ts | 22 ++++++++------- 2 files changed, 26 insertions(+), 24 deletions(-) diff --git a/cypress/e2e/11-inline-expression-editor.cy.ts b/cypress/e2e/11-inline-expression-editor.cy.ts index 1d95b2db8e889..45fb7752edf18 100644 --- a/cypress/e2e/11-inline-expression-editor.cy.ts +++ b/cypress/e2e/11-inline-expression-editor.cy.ts @@ -20,33 +20,33 @@ describe('Inline expression editor', () => { it('should resolve primitive resolvables', () => { WorkflowPage.getters.inlineExpressionEditorInput().clear(); - WorkflowPage.getters.inlineExpressionEditorInput().type('{{'); + WorkflowPage.getters.inlineExpressionEditorInput().click().type('{{'); WorkflowPage.getters.inlineExpressionEditorInput().type('1 + 2'); WorkflowPage.getters.inlineExpressionEditorOutput().contains(/^3$/); WorkflowPage.getters.inlineExpressionEditorInput().clear(); - WorkflowPage.getters.inlineExpressionEditorInput().type('{{'); + WorkflowPage.getters.inlineExpressionEditorInput().click().type('{{'); WorkflowPage.getters.inlineExpressionEditorInput().type('"ab"'); WorkflowPage.getters.inlineExpressionEditorInput().type('{rightArrow}+'); WorkflowPage.getters.inlineExpressionEditorInput().type('"cd"'); WorkflowPage.getters.inlineExpressionEditorOutput().contains(/^abcd$/); WorkflowPage.getters.inlineExpressionEditorInput().clear(); - WorkflowPage.getters.inlineExpressionEditorInput().type('{{'); + WorkflowPage.getters.inlineExpressionEditorInput().click().type('{{'); WorkflowPage.getters.inlineExpressionEditorInput().type('true && false'); WorkflowPage.getters.inlineExpressionEditorOutput().contains(/^false$/); }); it('should resolve object resolvables', () => { WorkflowPage.getters.inlineExpressionEditorInput().clear(); - WorkflowPage.getters.inlineExpressionEditorInput().type('{{'); + WorkflowPage.getters.inlineExpressionEditorInput().click().type('{{'); WorkflowPage.getters .inlineExpressionEditorInput() .type('{ a: 1 }', { parseSpecialCharSequences: false }); WorkflowPage.getters.inlineExpressionEditorOutput().contains(/^\[Object: \{"a": 1\}\]$/); WorkflowPage.getters.inlineExpressionEditorInput().clear(); - WorkflowPage.getters.inlineExpressionEditorInput().type('{{'); + WorkflowPage.getters.inlineExpressionEditorInput().click().type('{{'); WorkflowPage.getters .inlineExpressionEditorInput() .type('{ a: 1 }.a', { parseSpecialCharSequences: false }); @@ -55,13 +55,13 @@ describe('Inline expression editor', () => { it('should resolve array resolvables', () => { WorkflowPage.getters.inlineExpressionEditorInput().clear(); - WorkflowPage.getters.inlineExpressionEditorInput().type('{{'); + WorkflowPage.getters.inlineExpressionEditorInput().click().type('{{'); WorkflowPage.getters.inlineExpressionEditorInput().type('[1, 2, 3]'); WorkflowPage.getters.inlineExpressionEditorOutput().contains(/^\[Array: \[1,2,3\]\]$/); WorkflowPage.getters.inlineExpressionEditorInput().clear(); - WorkflowPage.getters.inlineExpressionEditorInput().type('{{'); + WorkflowPage.getters.inlineExpressionEditorInput().click().type('{{'); WorkflowPage.getters.inlineExpressionEditorInput().type('[1, 2, 3]'); WorkflowPage.getters.inlineExpressionEditorInput().type('[0]'); WorkflowPage.getters.inlineExpressionEditorOutput().contains(/^1$/); @@ -81,7 +81,7 @@ describe('Inline expression editor', () => { it('should resolve $parameter[]', () => { WorkflowPage.getters.inlineExpressionEditorInput().clear(); - WorkflowPage.getters.inlineExpressionEditorInput().type('{{'); + WorkflowPage.getters.inlineExpressionEditorInput().click().type('{{'); // Resolving $parameter is slow, especially on CI runner WorkflowPage.getters.inlineExpressionEditorInput().type('$parameter["operation"]'); WorkflowPage.getters.inlineExpressionEditorOutput().should('have.text', 'getAll'); @@ -90,19 +90,19 @@ describe('Inline expression editor', () => { it('should resolve input: $json,$input,$(nodeName)', () => { // Previous nodes have not run, input is empty WorkflowPage.getters.inlineExpressionEditorInput().clear(); - WorkflowPage.getters.inlineExpressionEditorInput().type('{{'); + WorkflowPage.getters.inlineExpressionEditorInput().click().type('{{'); WorkflowPage.getters.inlineExpressionEditorInput().type('$json.myStr'); WorkflowPage.getters .inlineExpressionEditorOutput() .should('have.text', '[Execute previous nodes for preview]'); WorkflowPage.getters.inlineExpressionEditorInput().clear(); - WorkflowPage.getters.inlineExpressionEditorInput().type('{{'); + WorkflowPage.getters.inlineExpressionEditorInput().click().type('{{'); WorkflowPage.getters.inlineExpressionEditorInput().type('$input.item.json.myStr'); WorkflowPage.getters .inlineExpressionEditorOutput() .should('have.text', '[Execute previous nodes for preview]'); WorkflowPage.getters.inlineExpressionEditorInput().clear(); - WorkflowPage.getters.inlineExpressionEditorInput().type('{{'); + WorkflowPage.getters.inlineExpressionEditorInput().click().type('{{'); WorkflowPage.getters .inlineExpressionEditorInput() .type("$('Schedule Trigger').item.json.myStr"); @@ -118,15 +118,15 @@ describe('Inline expression editor', () => { // Previous nodes have run, input can be resolved WorkflowPage.getters.inlineExpressionEditorInput().clear(); - WorkflowPage.getters.inlineExpressionEditorInput().type('{{'); + WorkflowPage.getters.inlineExpressionEditorInput().click().type('{{'); WorkflowPage.getters.inlineExpressionEditorInput().type('$json.myStr'); WorkflowPage.getters.inlineExpressionEditorOutput().should('have.text', 'Monday'); WorkflowPage.getters.inlineExpressionEditorInput().clear(); - WorkflowPage.getters.inlineExpressionEditorInput().type('{{'); + WorkflowPage.getters.inlineExpressionEditorInput().click().type('{{'); WorkflowPage.getters.inlineExpressionEditorInput().type('$input.item.json.myStr'); WorkflowPage.getters.inlineExpressionEditorOutput().should('have.text', 'Monday'); WorkflowPage.getters.inlineExpressionEditorInput().clear(); - WorkflowPage.getters.inlineExpressionEditorInput().type('{{'); + WorkflowPage.getters.inlineExpressionEditorInput().click().type('{{'); WorkflowPage.getters .inlineExpressionEditorInput() .type("$('Schedule Trigger').item.json.myStr"); diff --git a/cypress/e2e/9-expression-editor-modal.cy.ts b/cypress/e2e/9-expression-editor-modal.cy.ts index 66758cabd3e0f..4b3dd929b973b 100644 --- a/cypress/e2e/9-expression-editor-modal.cy.ts +++ b/cypress/e2e/9-expression-editor-modal.cy.ts @@ -20,16 +20,16 @@ describe('Expression editor modal', () => { it('should resolve primitive resolvables', () => { WorkflowPage.getters.expressionModalInput().clear(); - WorkflowPage.getters.expressionModalInput().type('{{ 1 + 2'); + WorkflowPage.getters.expressionModalInput().click().type('{{ 1 + 2'); WorkflowPage.getters.expressionModalOutput().contains(/^3$/); WorkflowPage.getters.expressionModalInput().clear(); - WorkflowPage.getters.expressionModalInput().type('{{ "ab" + "cd"'); + WorkflowPage.getters.expressionModalInput().click().type('{{ "ab" + "cd"'); WorkflowPage.getters.expressionModalOutput().contains(/^abcd$/); WorkflowPage.getters.expressionModalInput().clear(); - WorkflowPage.getters.expressionModalInput().type('{{ true && false'); + WorkflowPage.getters.expressionModalInput().click().type('{{ true && false'); WorkflowPage.getters.expressionModalOutput().contains(/^false$/); }); @@ -37,6 +37,7 @@ describe('Expression editor modal', () => { WorkflowPage.getters.expressionModalInput().clear(); WorkflowPage.getters .expressionModalInput() + .click() .type('{{ { a : 1 }', { parseSpecialCharSequences: false }); WorkflowPage.getters.expressionModalOutput().contains(/^\[Object: \{"a": 1\}\]$/); @@ -44,18 +45,19 @@ describe('Expression editor modal', () => { WorkflowPage.getters .expressionModalInput() + .click() .type('{{ { a : 1 }.a', { parseSpecialCharSequences: false }); WorkflowPage.getters.expressionModalOutput().contains(/^1$/); }); it('should resolve array resolvables', () => { WorkflowPage.getters.expressionModalInput().clear(); - WorkflowPage.getters.expressionModalInput().type('{{ [1, 2, 3]'); + WorkflowPage.getters.expressionModalInput().click().type('{{ [1, 2, 3]'); WorkflowPage.getters.expressionModalOutput().contains(/^\[Array: \[1,2,3\]\]$/); WorkflowPage.getters.expressionModalInput().clear(); - WorkflowPage.getters.expressionModalInput().type('{{ [1, 2, 3][0]'); + WorkflowPage.getters.expressionModalInput().click().type('{{ [1, 2, 3][0]'); WorkflowPage.getters.expressionModalOutput().contains(/^1$/); }); }); @@ -73,19 +75,19 @@ describe('Expression editor modal', () => { it('should resolve $parameter[]', () => { WorkflowPage.getters.expressionModalInput().clear(); - WorkflowPage.getters.expressionModalInput().type('{{ $parameter["operation"]'); + WorkflowPage.getters.expressionModalInput().click().type('{{ $parameter["operation"]'); WorkflowPage.getters.expressionModalOutput().should('have.text', 'getAll'); }); it('should resolve input: $json,$input,$(nodeName)', () => { // Previous nodes have not run, input is empty WorkflowPage.getters.expressionModalInput().clear(); - WorkflowPage.getters.expressionModalInput().type('{{ $json.myStr'); + WorkflowPage.getters.expressionModalInput().click().type('{{ $json.myStr'); WorkflowPage.getters .expressionModalOutput() .should('have.text', '[Execute previous nodes for preview]'); WorkflowPage.getters.expressionModalInput().clear(); - WorkflowPage.getters.expressionModalInput().type('{{ $input.item.json.myStr'); + WorkflowPage.getters.expressionModalInput().click().type('{{ $input.item.json.myStr'); WorkflowPage.getters .expressionModalOutput() .should('have.text', '[Execute previous nodes for preview]'); @@ -104,10 +106,10 @@ describe('Expression editor modal', () => { // Previous nodes have run, input can be resolved WorkflowPage.getters.expressionModalInput().clear(); - WorkflowPage.getters.expressionModalInput().type('{{ $json.myStr'); + WorkflowPage.getters.expressionModalInput().click().type('{{ $json.myStr'); WorkflowPage.getters.expressionModalOutput().should('have.text', 'Monday'); WorkflowPage.getters.expressionModalInput().clear(); - WorkflowPage.getters.expressionModalInput().type('{{ $input.item.json.myStr'); + WorkflowPage.getters.expressionModalInput().click().type('{{ $input.item.json.myStr'); WorkflowPage.getters.expressionModalOutput().should('have.text', 'Monday'); WorkflowPage.getters.expressionModalInput().clear(); WorkflowPage.getters.expressionModalInput().type("{{ $('Schedule Trigger').item.json.myStr"); From 6ca18dfc5e43bb6425825ad40f152191191cd948 Mon Sep 17 00:00:00 2001 From: Elias Meire Date: Wed, 29 May 2024 11:51:26 +0200 Subject: [PATCH 5/5] one more e2e fix --- cypress/e2e/9-expression-editor-modal.cy.ts | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/cypress/e2e/9-expression-editor-modal.cy.ts b/cypress/e2e/9-expression-editor-modal.cy.ts index 4b3dd929b973b..ddf4ac3090285 100644 --- a/cypress/e2e/9-expression-editor-modal.cy.ts +++ b/cypress/e2e/9-expression-editor-modal.cy.ts @@ -92,7 +92,10 @@ describe('Expression editor modal', () => { .expressionModalOutput() .should('have.text', '[Execute previous nodes for preview]'); WorkflowPage.getters.expressionModalInput().clear(); - WorkflowPage.getters.expressionModalInput().type("{{ $('Schedule Trigger').item.json.myStr"); + WorkflowPage.getters + .expressionModalInput() + .click() + .type("{{ $('Schedule Trigger').item.json.myStr"); WorkflowPage.getters .expressionModalOutput() .should('have.text', '[Execute previous nodes for preview]'); @@ -112,7 +115,10 @@ describe('Expression editor modal', () => { WorkflowPage.getters.expressionModalInput().click().type('{{ $input.item.json.myStr'); WorkflowPage.getters.expressionModalOutput().should('have.text', 'Monday'); WorkflowPage.getters.expressionModalInput().clear(); - WorkflowPage.getters.expressionModalInput().type("{{ $('Schedule Trigger').item.json.myStr"); + WorkflowPage.getters + .expressionModalInput() + .click() + .type("{{ $('Schedule Trigger').item.json.myStr"); WorkflowPage.getters.expressionModalOutput().should('have.text', 'Monday'); }); });