From 071e6d6b6e32b7196f34043710c23331ad28fac0 Mon Sep 17 00:00:00 2001 From: Elias Meire Date: Thu, 4 Jan 2024 17:23:24 +0100 Subject: [PATCH] feat(editor): Add fullscreen view to code editor (#8084) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## Summary image image 1. Add code node and open it 3. Click the fullscreen button in the bottom right 4. A fullscreen dialog should appear and allow editing the code 5. Changes made in the fullscreen dialog should be applied to the original code editor when closed It should work the same way for HTML/SQL/JSON editors ⚠️ Modal layout was updated so that modals/dialogs are centered, try to test some modals ## Related tickets and issues https://linear.app/n8n/issue/NODE-1009/add-fullscreen-view-to-code-node ## Review / Merge checklist - [ ] PR title and summary are descriptive. **Remember, the title automatically goes into the changelog. Use `(no-changelog)` otherwise.** ([conventions](https://github.com/n8n-io/n8n/blob/master/.github/pull_request_title_conventions.md)) - [ ] [Docs updated](https://github.com/n8n-io/n8n-docs) or follow-up ticket created. - [ ] Tests included. > A bug is not considered fixed, unless a test is added to prevent it from happening again. > A feature is not complete without tests. --------- Co-authored-by: Giulio Andreini --- cypress/e2e/26-resource-locator.cy.ts | 8 +- cypress/e2e/5-ndv.cy.ts | 180 +++++++++++------- cypress/pages/ndv.ts | 11 +- cypress/support/commands.ts | 1 + packages/design-system/src/css/dialog.scss | 5 +- .../CodeNodeEditor/CodeNodeEditor.vue | 32 ++-- .../CodeNodeEditor/baseExtensions.ts | 64 +++++-- .../src/components/CodeNodeEditor/theme.ts | 156 ++++++++++----- .../src/components/HtmlEditor/HtmlEditor.vue | 70 ++++--- .../src/components/HtmlEditor/theme.ts | 97 ---------- .../src/components/JsEditor/JsEditor.vue | 55 ++++-- .../src/components/JsonEditor/JsonEditor.vue | 52 +++-- .../editor-ui/src/components/NodeSettings.vue | 2 +- .../src/components/ParameterInput.vue | 163 +++++++++++++--- .../src/components/SqlEditor/SqlEditor.vue | 75 +++++--- packages/editor-ui/src/n8n-theme.scss | 8 - .../descriptions/JavascriptCodeDescription.ts | 1 - .../descriptions/PythonCodeDescription.ts | 1 - .../database/executeQuery.operation.ts | 2 - .../nodes/Microsoft/Sql/MicrosoftSql.node.ts | 1 - .../nodes-base/nodes/MySql/v1/MySqlV1.node.ts | 1 - .../database/executeQuery.operation.ts | 1 - .../nodes/Postgres/v1/PostgresV1.node.ts | 1 - .../database/executeQuery.operation.ts | 1 - .../nodes-base/nodes/QuestDb/QuestDb.node.ts | 1 - packages/nodes-base/nodes/Set/v2/raw.mode.ts | 2 +- .../nodes/Snowflake/Snowflake.node.ts | 1 - .../nodes/TimescaleDb/TimescaleDb.node.ts | 1 - 28 files changed, 617 insertions(+), 376 deletions(-) delete mode 100644 packages/editor-ui/src/components/HtmlEditor/theme.ts diff --git a/cypress/e2e/26-resource-locator.cy.ts b/cypress/e2e/26-resource-locator.cy.ts index 60d33374d7..9cea4e25a3 100644 --- a/cypress/e2e/26-resource-locator.cy.ts +++ b/cypress/e2e/26-resource-locator.cy.ts @@ -1,5 +1,5 @@ import { WorkflowPage, NDV, CredentialsModal } from '../pages'; -import { getPopper, getVisiblePopper, getVisibleSelect } from '../utils'; +import { getVisiblePopper, getVisibleSelect } from '../utils'; const workflowPage = new WorkflowPage(); const ndv = new NDV(); @@ -66,6 +66,8 @@ describe('Resource Locator', () => { workflowPage.actions.addInitialNodeToCanvas('E2e Test', { action: 'Resource Locator' }); ndv.getters.resourceLocatorInput('rlc').click(); + + cy.getByTestId('rlc-item').should('exist'); getVisiblePopper() .should('have.length', 1) .findChildByTestId('rlc-item') @@ -73,9 +75,11 @@ describe('Resource Locator', () => { ndv.actions.setInvalidExpression({ fieldName: 'fieldId' }); - ndv.getters.container().click(); // remove focus from input, hide expression preview + ndv.getters.nodeParameters().click(); // remove focus from input, hide expression preview ndv.getters.resourceLocatorInput('rlc').click(); + + cy.getByTestId('rlc-item').should('exist'); getVisiblePopper() .should('have.length', 1) .findChildByTestId('rlc-item') diff --git a/cypress/e2e/5-ndv.cy.ts b/cypress/e2e/5-ndv.cy.ts index 0599d4798d..14097255f2 100644 --- a/cypress/e2e/5-ndv.cy.ts +++ b/cypress/e2e/5-ndv.cy.ts @@ -302,7 +302,7 @@ describe('NDV', () => { ndv.actions.setInvalidExpression({ fieldName: 'fieldId', delay: 200 }); - ndv.getters.container().click(); // remove focus from input, hide expression preview + ndv.getters.nodeParameters().click(); // remove focus from input, hide expression preview ndv.getters.parameterInput('remoteOptions').click(); @@ -320,7 +320,7 @@ describe('NDV', () => { ndv.actions.setInvalidExpression({ fieldName: 'otherField', delay: 50 }); - ndv.getters.container().click(); // remove focus from input, hide expression preview + ndv.getters.nodeParameters().click(); // remove focus from input, hide expression preview ndv.getters.parameterInput('remoteOptions').click(); getVisibleSelect().find('.el-select-dropdown__item').should('have.length', 3); @@ -360,6 +360,15 @@ describe('NDV', () => { ndv.getters.nodeExecuteButton().should('be.visible'); }); + it('should allow editing code in fullscreen in the Code node', () => { + workflowPage.actions.addInitialNodeToCanvas('Code', { keepNdvOpen: true }); + ndv.actions.openCodeEditorFullscreen(); + + ndv.getters.codeEditorFullscreen().type('{selectall}').type('{backspace}').type('foo()'); + ndv.getters.codeEditorDialog().find('.el-dialog__close').click(); + ndv.getters.parameterInput('jsCode').get('.cm-content').should('contain.text', 'foo()'); + }); + it('should not retrieve remote options when a parameter value changes', () => { cy.intercept('/rest/dynamic-node-parameters/options?**', cy.spy().as('fetchParameterOptions')); workflowPage.actions.addInitialNodeToCanvas('E2e Test', { action: 'Remote Options' }); @@ -370,106 +379,126 @@ describe('NDV', () => { }); describe('floating nodes', () => { - function getFloatingNodeByPosition(position: 'inputMain' | 'outputMain' | 'outputSub'| 'inputSub') { + function getFloatingNodeByPosition( + position: 'inputMain' | 'outputMain' | 'outputSub' | 'inputSub', + ) { return cy.get(`[data-node-placement=${position}]`); } beforeEach(() => { cy.createFixtureWorkflow('Floating_Nodes.json', `Floating Nodes`); - workflowPage.getters.canvasNodes().first().dblclick() - getFloatingNodeByPosition("inputMain").should('not.exist'); - getFloatingNodeByPosition("outputMain").should('exist'); + workflowPage.getters.canvasNodes().first().dblclick(); + getFloatingNodeByPosition('inputMain').should('not.exist'); + getFloatingNodeByPosition('outputMain').should('exist'); }); it('should traverse floating nodes with mouse', () => { // Traverse 4 connected node forwards - Array.from(Array(4).keys()).forEach(i => { - getFloatingNodeByPosition("outputMain").click({ force: true}); + Array.from(Array(4).keys()).forEach((i) => { + getFloatingNodeByPosition('outputMain').click({ force: true }); ndv.getters.nodeNameContainer().should('contain', `Node ${i + 1}`); - getFloatingNodeByPosition("inputMain").should('exist'); - getFloatingNodeByPosition("outputMain").should('exist'); + getFloatingNodeByPosition('inputMain').should('exist'); + getFloatingNodeByPosition('outputMain').should('exist'); ndv.actions.close(); workflowPage.getters.selectedNodes().should('have.length', 1); - workflowPage.getters.selectedNodes().first().should('contain', `Node ${i + 1}`); + workflowPage.getters + .selectedNodes() + .first() + .should('contain', `Node ${i + 1}`); workflowPage.getters.selectedNodes().first().dblclick(); - }) + }); - getFloatingNodeByPosition("outputMain").click({ force: true}); + getFloatingNodeByPosition('outputMain').click({ force: true }); ndv.getters.nodeNameContainer().should('contain', 'Chain'); - getFloatingNodeByPosition("inputSub").should('exist'); - getFloatingNodeByPosition("inputSub").click({ force: true}); + getFloatingNodeByPosition('inputSub').should('exist'); + getFloatingNodeByPosition('inputSub').click({ force: true }); ndv.getters.nodeNameContainer().should('contain', 'Model'); - getFloatingNodeByPosition("inputSub").should('not.exist'); - getFloatingNodeByPosition("inputMain").should('not.exist'); - getFloatingNodeByPosition("outputMain").should('not.exist'); - getFloatingNodeByPosition("outputSub").should('exist'); + getFloatingNodeByPosition('inputSub').should('not.exist'); + getFloatingNodeByPosition('inputMain').should('not.exist'); + getFloatingNodeByPosition('outputMain').should('not.exist'); + getFloatingNodeByPosition('outputSub').should('exist'); ndv.actions.close(); workflowPage.getters.selectedNodes().should('have.length', 1); workflowPage.getters.selectedNodes().first().should('contain', 'Model'); workflowPage.getters.selectedNodes().first().dblclick(); - getFloatingNodeByPosition("outputSub").click({ force: true}); + getFloatingNodeByPosition('outputSub').click({ force: true }); ndv.getters.nodeNameContainer().should('contain', 'Chain'); // Traverse 4 connected node backwards - Array.from(Array(4).keys()).forEach(i => { - getFloatingNodeByPosition("inputMain").click({ force: true}); - ndv.getters.nodeNameContainer().should('contain', `Node ${4 - (i)}`); - getFloatingNodeByPosition("outputMain").should('exist'); - getFloatingNodeByPosition("inputMain").should('exist'); - }) - getFloatingNodeByPosition("inputMain").click({ force: true}); - workflowPage.getters.selectedNodes().first().should('contain', MANUAL_TRIGGER_NODE_DISPLAY_NAME); - getFloatingNodeByPosition("inputMain").should('not.exist'); - getFloatingNodeByPosition("inputSub").should('not.exist'); - getFloatingNodeByPosition("outputSub").should('not.exist'); + Array.from(Array(4).keys()).forEach((i) => { + getFloatingNodeByPosition('inputMain').click({ force: true }); + ndv.getters.nodeNameContainer().should('contain', `Node ${4 - i}`); + getFloatingNodeByPosition('outputMain').should('exist'); + getFloatingNodeByPosition('inputMain').should('exist'); + }); + getFloatingNodeByPosition('inputMain').click({ force: true }); + workflowPage.getters + .selectedNodes() + .first() + .should('contain', MANUAL_TRIGGER_NODE_DISPLAY_NAME); + getFloatingNodeByPosition('inputMain').should('not.exist'); + getFloatingNodeByPosition('inputSub').should('not.exist'); + getFloatingNodeByPosition('outputSub').should('not.exist'); ndv.actions.close(); workflowPage.getters.selectedNodes().should('have.length', 1); - workflowPage.getters.selectedNodes().first().should('contain', MANUAL_TRIGGER_NODE_DISPLAY_NAME); + workflowPage.getters + .selectedNodes() + .first() + .should('contain', MANUAL_TRIGGER_NODE_DISPLAY_NAME); }); it('should traverse floating nodes with mouse', () => { // Traverse 4 connected node forwards - Array.from(Array(4).keys()).forEach(i => { - cy.realPress(['ShiftLeft', 'Meta', 'AltLeft', 'ArrowRight']) + Array.from(Array(4).keys()).forEach((i) => { + cy.realPress(['ShiftLeft', 'Meta', 'AltLeft', 'ArrowRight']); ndv.getters.nodeNameContainer().should('contain', `Node ${i + 1}`); - getFloatingNodeByPosition("inputMain").should('exist'); - getFloatingNodeByPosition("outputMain").should('exist'); + getFloatingNodeByPosition('inputMain').should('exist'); + getFloatingNodeByPosition('outputMain').should('exist'); ndv.actions.close(); workflowPage.getters.selectedNodes().should('have.length', 1); - workflowPage.getters.selectedNodes().first().should('contain', `Node ${i + 1}`); + workflowPage.getters + .selectedNodes() + .first() + .should('contain', `Node ${i + 1}`); workflowPage.getters.selectedNodes().first().dblclick(); - }) + }); - cy.realPress(['ShiftLeft', 'Meta', 'AltLeft', 'ArrowRight']) + cy.realPress(['ShiftLeft', 'Meta', 'AltLeft', 'ArrowRight']); ndv.getters.nodeNameContainer().should('contain', 'Chain'); - getFloatingNodeByPosition("inputSub").should('exist'); - cy.realPress(['ShiftLeft', 'Meta', 'AltLeft', 'ArrowDown']) + getFloatingNodeByPosition('inputSub').should('exist'); + cy.realPress(['ShiftLeft', 'Meta', 'AltLeft', 'ArrowDown']); ndv.getters.nodeNameContainer().should('contain', 'Model'); - getFloatingNodeByPosition("inputSub").should('not.exist'); - getFloatingNodeByPosition("inputMain").should('not.exist'); - getFloatingNodeByPosition("outputMain").should('not.exist'); - getFloatingNodeByPosition("outputSub").should('exist'); + getFloatingNodeByPosition('inputSub').should('not.exist'); + getFloatingNodeByPosition('inputMain').should('not.exist'); + getFloatingNodeByPosition('outputMain').should('not.exist'); + getFloatingNodeByPosition('outputSub').should('exist'); ndv.actions.close(); workflowPage.getters.selectedNodes().should('have.length', 1); workflowPage.getters.selectedNodes().first().should('contain', 'Model'); workflowPage.getters.selectedNodes().first().dblclick(); - cy.realPress(['ShiftLeft', 'Meta', 'AltLeft', 'ArrowUp']) + cy.realPress(['ShiftLeft', 'Meta', 'AltLeft', 'ArrowUp']); ndv.getters.nodeNameContainer().should('contain', 'Chain'); // Traverse 4 connected node backwards - Array.from(Array(4).keys()).forEach(i => { - cy.realPress(['ShiftLeft', 'Meta', 'AltLeft', 'ArrowLeft']) - ndv.getters.nodeNameContainer().should('contain', `Node ${4 - (i)}`); - getFloatingNodeByPosition("outputMain").should('exist'); - getFloatingNodeByPosition("inputMain").should('exist'); - }) - cy.realPress(['ShiftLeft', 'Meta', 'AltLeft', 'ArrowLeft']) - workflowPage.getters.selectedNodes().first().should('contain', MANUAL_TRIGGER_NODE_DISPLAY_NAME); - getFloatingNodeByPosition("inputMain").should('not.exist'); - getFloatingNodeByPosition("inputSub").should('not.exist'); - getFloatingNodeByPosition("outputSub").should('not.exist'); + Array.from(Array(4).keys()).forEach((i) => { + cy.realPress(['ShiftLeft', 'Meta', 'AltLeft', 'ArrowLeft']); + ndv.getters.nodeNameContainer().should('contain', `Node ${4 - i}`); + getFloatingNodeByPosition('outputMain').should('exist'); + getFloatingNodeByPosition('inputMain').should('exist'); + }); + cy.realPress(['ShiftLeft', 'Meta', 'AltLeft', 'ArrowLeft']); + workflowPage.getters + .selectedNodes() + .first() + .should('contain', MANUAL_TRIGGER_NODE_DISPLAY_NAME); + getFloatingNodeByPosition('inputMain').should('not.exist'); + getFloatingNodeByPosition('inputSub').should('not.exist'); + getFloatingNodeByPosition('outputSub').should('not.exist'); ndv.actions.close(); workflowPage.getters.selectedNodes().should('have.length', 1); - workflowPage.getters.selectedNodes().first().should('contain', MANUAL_TRIGGER_NODE_DISPLAY_NAME); + workflowPage.getters + .selectedNodes() + .first() + .should('contain', MANUAL_TRIGGER_NODE_DISPLAY_NAME); }); }); @@ -501,23 +530,34 @@ describe('NDV', () => { ndv.getters.outputDisplayMode().find('[class*=active]').should('contain', 'Table'); - ndv.getters.outputTableRow(1).should('include.text', ' '); + ndv.getters + .outputTableRow(1) + .should('include.text', ' '); cy.document().trigger('keyup', { key: '/' }); ndv.getters.searchInput().filter(':focus').type(' Introduction to XML John Doe 2020 1234567890 Data Science Basics Jane Smith 2019 0987654321 Programming in Python Bob Johnson 2021 5432109876 "}]'); - ndv.getters.outputDataContainer().find('mark').should('have.text', ' Introduction to XML John Doe 2020 1234567890 Data Science Basics Jane Smith 2019 0987654321 Programming in Python Bob Johnson 2021 5432109876 "}]', + ); + ndv.getters.outputDataContainer().find('mark').should('have.text', ' span').should('include.text', ''); + ndv.getters.outputDisplayMode().find('label').eq(2).click({ force: true }); + ndv.getters + .outputDataContainer() + .findChildByTestId('run-data-schema-item') + .find('> span') + .should('include.text', ''); }); it('should properly show node execution indicator', () => { @@ -546,7 +586,10 @@ describe('NDV', () => { }); it('Should handle mismatched option attributes', () => { - workflowPage.actions.addInitialNodeToCanvas('LDAP', { keepNdvOpen: true, action: 'Create a new entry' }); + workflowPage.actions.addInitialNodeToCanvas('LDAP', { + keepNdvOpen: true, + action: 'Create a new entry', + }); // Add some attributes in Create operation cy.getByTestId('parameter-item').contains('Add Attributes').click(); ndv.actions.changeNodeOperation('Update'); @@ -556,7 +599,10 @@ describe('NDV', () => { it('Should keep RLC values after operation change', () => { const TEST_DOC_ID = '1111'; - workflowPage.actions.addInitialNodeToCanvas('Google Sheets', { keepNdvOpen: true, action: 'Append row in sheet' }); + workflowPage.actions.addInitialNodeToCanvas('Google Sheets', { + keepNdvOpen: true, + action: 'Append row in sheet', + }); ndv.actions.setRLCValue('documentId', TEST_DOC_ID); ndv.actions.changeNodeOperation('Update Row'); ndv.getters.resourceLocatorInput('documentId').find('input').should('have.value', TEST_DOC_ID); diff --git a/cypress/pages/ndv.ts b/cypress/pages/ndv.ts index 66116b0fc4..449ad75eb4 100644 --- a/cypress/pages/ndv.ts +++ b/cypress/pages/ndv.ts @@ -98,6 +98,9 @@ export class NDV extends BasePage { pagination: () => cy.getByTestId('ndv-data-pagination'), nodeVersion: () => cy.getByTestId('node-version'), nodeSettingsTab: () => cy.getByTestId('tab-settings'), + codeEditorFullscreenButton: () => cy.getByTestId('code-editor-fullscreen-button'), + codeEditorDialog: () => cy.getByTestId('code-editor-fullscreen'), + codeEditorFullscreen: () => this.getters.codeEditorDialog().find('.cm-content'), nodeRunSuccessIndicator: () => cy.getByTestId('node-run-info-success'), nodeRunErrorIndicator: () => cy.getByTestId('node-run-info-danger'), }; @@ -251,9 +254,15 @@ export class NDV extends BasePage { openSettings: () => { this.getters.nodeSettingsTab().click(); }, + + openCodeEditorFullscreen: () => { + this.getters.codeEditorFullscreenButton().click({ force: true }); + }, changeNodeOperation: (operation: string) => { this.getters.parameterInput('operation').click(); - cy.get('.el-select-dropdown__item').contains(new RegExp(`^${operation}$`)).click({ force: true }); + cy.get('.el-select-dropdown__item') + .contains(new RegExp(`^${operation}$`)) + .click({ force: true }); this.getters.parameterInput('operation').find('input').should('have.value', operation); }, }; diff --git a/cypress/support/commands.ts b/cypress/support/commands.ts index 6a0f122464..23460c365f 100644 --- a/cypress/support/commands.ts +++ b/cypress/support/commands.ts @@ -160,6 +160,7 @@ Cypress.Commands.add('draganddrop', (draggableSelector, droppableSelector) => { cy.get(draggableSelector).trigger('mousedown'); } // We don't chain these commands to make sure cy.get is re-trying correctly + cy.get(droppableSelector).realMouseMove(0, 0); cy.get(droppableSelector).realMouseMove(pageX, pageY); cy.get(droppableSelector).realHover(); cy.get(droppableSelector).realMouseUp(); diff --git a/packages/design-system/src/css/dialog.scss b/packages/design-system/src/css/dialog.scss index e7bc062106..2ddb2bfcde 100644 --- a/packages/design-system/src/css/dialog.scss +++ b/packages/design-system/src/css/dialog.scss @@ -18,7 +18,8 @@ height: 100%; display: flex; flex-direction: column; - align-items: inherit; + align-items: center; + justify-content: center; overflow-x: hidden; overflow-y: auto; } @@ -60,7 +61,6 @@ @include mixins.e(header) { padding: var.$dialog-padding-primary; - padding-bottom: 0px; } @include mixins.e(headerbtn) { @@ -95,6 +95,7 @@ @include mixins.e(body) { padding: var.$dialog-padding-primary; + padding-top: 0; color: var(--color-text-base); } diff --git a/packages/editor-ui/src/components/CodeNodeEditor/CodeNodeEditor.vue b/packages/editor-ui/src/components/CodeNodeEditor/CodeNodeEditor.vue index 9827cd5567..d45468210b 100644 --- a/packages/editor-ui/src/components/CodeNodeEditor/CodeNodeEditor.vue +++ b/packages/editor-ui/src/components/CodeNodeEditor/CodeNodeEditor.vue @@ -17,7 +17,11 @@ name="code" data-test-id="code-node-tab-code" > -
+
+ -
+
+
+ +
@@ -82,6 +89,10 @@ export default defineComponent({ type: Boolean, default: false, }, + fillParent: { + type: Boolean, + default: false, + }, mode: { type: String as PropType, validator: (value: CodeExecutionMode): boolean => CODE_EXECUTION_MODES.includes(value), @@ -193,7 +204,12 @@ export default defineComponent({ ...readOnlyEditorExtensions, EditorState.readOnly.of(isReadOnly), EditorView.editable.of(!isReadOnly), - codeNodeEditorTheme({ isReadOnly, customMinHeight: this.rows }), + codeNodeEditorTheme({ + isReadOnly, + maxHeight: this.fillParent ? '100%' : '40vh', + minHeight: '20vh', + rows: this.rows, + }), ]; if (!isReadOnly) { @@ -384,15 +400,9 @@ export default defineComponent({ diff --git a/packages/editor-ui/src/components/CodeNodeEditor/baseExtensions.ts b/packages/editor-ui/src/components/CodeNodeEditor/baseExtensions.ts index 45855d3bc4..bed55df677 100644 --- a/packages/editor-ui/src/components/CodeNodeEditor/baseExtensions.ts +++ b/packages/editor-ui/src/components/CodeNodeEditor/baseExtensions.ts @@ -6,12 +6,14 @@ import { highlightSpecialChars, keymap, lineNumbers, + type KeyBinding, } from '@codemirror/view'; import { bracketMatching, foldGutter, indentOnInput } from '@codemirror/language'; -import { acceptCompletion } from '@codemirror/autocomplete'; +import { acceptCompletion, selectedCompletion } from '@codemirror/autocomplete'; import { history, - indentWithTab, + indentLess, + indentMore, insertNewlineAndIndent, toggleComment, redo, @@ -19,7 +21,7 @@ import { undo, } from '@codemirror/commands'; import { lintGutter } from '@codemirror/lint'; -import type { Extension } from '@codemirror/state'; +import { type Extension, Prec } from '@codemirror/state'; import { codeInputHandler } from '@/plugins/codemirror/inputHandlers/code.inputHandler'; @@ -29,6 +31,42 @@ export const readOnlyEditorExtensions: readonly Extension[] = [ highlightSpecialChars(), ]; +export const tabKeyMap: KeyBinding[] = [ + { + any(editor, event) { + if (event.key === 'Tab' || (event.key === 'Escape' && selectedCompletion(editor.state))) { + event.stopPropagation(); + } + + return false; + }, + }, + { + key: 'Tab', + run: (editor) => { + if (selectedCompletion(editor.state)) { + return acceptCompletion(editor); + } + + return indentMore(editor); + }, + }, + { key: 'Shift-Tab', run: indentLess }, +]; + +export const enterKeyMap: KeyBinding[] = [ + { + key: 'Enter', + run: (editor) => { + if (selectedCompletion(editor.state)) { + return acceptCompletion(editor); + } + + return insertNewlineAndIndent(editor); + }, + }, +]; + export const writableEditorExtensions: readonly Extension[] = [ history(), lintGutter(), @@ -39,14 +77,14 @@ export const writableEditorExtensions: readonly Extension[] = [ bracketMatching(), highlightActiveLine(), highlightActiveLineGutter(), - keymap.of([ - { key: 'Enter', run: insertNewlineAndIndent }, - { key: 'Tab', run: acceptCompletion }, - { key: 'Enter', run: acceptCompletion }, - { key: 'Mod-/', run: toggleComment }, - { key: 'Mod-z', run: undo }, - { key: 'Mod-Shift-z', run: redo }, - { key: 'Backspace', run: deleteCharBackward, shift: deleteCharBackward }, - indentWithTab, - ]), + Prec.highest( + keymap.of([ + ...tabKeyMap, + ...enterKeyMap, + { key: 'Mod-/', run: toggleComment }, + { key: 'Mod-z', run: undo }, + { key: 'Mod-Shift-z', run: redo }, + { key: 'Backspace', run: deleteCharBackward, shift: deleteCharBackward }, + ]), + ), ]; diff --git a/packages/editor-ui/src/components/CodeNodeEditor/theme.ts b/packages/editor-ui/src/components/CodeNodeEditor/theme.ts index 64184abd7d..b39c310ab4 100644 --- a/packages/editor-ui/src/components/CodeNodeEditor/theme.ts +++ b/packages/editor-ui/src/components/CodeNodeEditor/theme.ts @@ -29,14 +29,18 @@ const BASE_STYLING = { interface ThemeSettings { isReadOnly?: boolean; - customMaxHeight?: string; - customMinHeight?: number; + maxHeight?: string; + minHeight?: string; + rows?: number; + highlightColors?: 'default' | 'html'; } export const codeNodeEditorTheme = ({ isReadOnly, - customMaxHeight, - customMinHeight, + minHeight, + maxHeight, + rows, + highlightColors, }: ThemeSettings) => [ EditorView.theme({ '&': { @@ -85,11 +89,13 @@ export const codeNodeEditorTheme = ({ }, '.cm-scroller': { overflow: 'auto', - - maxHeight: customMaxHeight ?? '100%', + maxHeight: maxHeight ?? '100%', ...(isReadOnly ? {} - : { minHeight: customMinHeight ? `${Number(customMinHeight) * 1.3}em` : '10em' }), + : { minHeight: rows && rows !== -1 ? `${Number(rows + 1) * 1.3}em` : 'auto' }), + }, + '.cm-gutter,.cm-content': { + minHeight: rows && rows !== -1 ? 'auto' : minHeight ?? 'calc(35vh - var(--spacing-2xl))', }, '.cm-diagnosticAction': { backgroundColor: BASE_STYLING.diagnosticButton.backgroundColor, @@ -106,47 +112,97 @@ export const codeNodeEditorTheme = ({ color: 'var(--color-text-base)', }, }), - syntaxHighlighting( - HighlightStyle.define([ - { - tag: tags.comment, - color: 'var(--color-code-tags-comment)', - }, - { - tag: [tags.string, tags.special(tags.brace)], - color: 'var(--color-code-tags-string)', - }, - { - tag: [tags.number, tags.self, tags.bool, tags.null], - color: 'var(--color-code-tags-primitive)', - }, - { - tag: tags.keyword, - color: 'var(--color-code-tags-keyword)', - }, - { - tag: tags.operator, - color: 'var(--color-code-tags-operator)', - }, - { - tag: [ - tags.variableName, - tags.propertyName, - tags.attributeName, - tags.regexp, - tags.className, - tags.typeName, - ], - color: 'var(--color-code-tags-variable)', - }, - { - tag: [ - tags.definition(tags.typeName), - tags.definition(tags.propertyName), - tags.function(tags.variableName), - ], - color: 'var(--color-code-tags-definition)', - }, - ]), - ), + highlightColors === 'html' + ? syntaxHighlighting( + HighlightStyle.define([ + { tag: tags.keyword, color: '#c678dd' }, + { + tag: [tags.name, tags.deleted, tags.character, tags.propertyName, tags.macroName], + color: '#e06c75', + }, + { tag: [tags.function(tags.variableName), tags.labelName], color: '#61afef' }, + { + tag: [tags.color, tags.constant(tags.name), tags.standard(tags.name)], + color: '#d19a66', + }, + { tag: [tags.definition(tags.name), tags.separator], color: '#abb2bf' }, + { + tag: [ + tags.typeName, + tags.className, + tags.number, + tags.changed, + tags.annotation, + tags.modifier, + tags.self, + tags.namespace, + ], + color: '#e06c75', + }, + { + tag: [ + tags.operator, + tags.operatorKeyword, + tags.url, + tags.escape, + tags.regexp, + tags.link, + tags.special(tags.string), + ], + color: '#56b6c2', + }, + { tag: [tags.meta, tags.comment], color: '#7d8799' }, + { tag: tags.strong, fontWeight: 'bold' }, + { tag: tags.emphasis, fontStyle: 'italic' }, + { tag: tags.strikethrough, textDecoration: 'line-through' }, + { tag: tags.link, color: '#7d8799', textDecoration: 'underline' }, + { tag: tags.heading, fontWeight: 'bold', color: '#e06c75' }, + { tag: [tags.atom, tags.bool, tags.special(tags.variableName)], color: '#d19a66' }, + { tag: [tags.processingInstruction, tags.string, tags.inserted], color: '#98c379' }, + { tag: tags.invalid, color: 'red', 'font-weight': 'bold' }, + ]), + ) + : syntaxHighlighting( + HighlightStyle.define([ + { + tag: tags.comment, + color: 'var(--color-code-tags-comment)', + }, + { + tag: [tags.string, tags.special(tags.brace)], + color: 'var(--color-code-tags-string)', + }, + { + tag: [tags.number, tags.self, tags.bool, tags.null], + color: 'var(--color-code-tags-primitive)', + }, + { + tag: tags.keyword, + color: 'var(--color-code-tags-keyword)', + }, + { + tag: tags.operator, + color: 'var(--color-code-tags-operator)', + }, + { + tag: [ + tags.variableName, + tags.propertyName, + tags.attributeName, + tags.regexp, + tags.className, + tags.typeName, + ], + color: 'var(--color-code-tags-variable)', + }, + { + tag: [ + tags.definition(tags.typeName), + tags.definition(tags.propertyName), + tags.function(tags.variableName), + ], + color: 'var(--color-code-tags-definition)', + }, + ]), + ), ]; diff --git a/packages/editor-ui/src/components/HtmlEditor/HtmlEditor.vue b/packages/editor-ui/src/components/HtmlEditor/HtmlEditor.vue index 1b91d9c00d..5aba6b063a 100644 --- a/packages/editor-ui/src/components/HtmlEditor/HtmlEditor.vue +++ b/packages/editor-ui/src/components/HtmlEditor/HtmlEditor.vue @@ -1,44 +1,48 @@ - + diff --git a/packages/editor-ui/src/components/HtmlEditor/theme.ts b/packages/editor-ui/src/components/HtmlEditor/theme.ts deleted file mode 100644 index 78e90039ad..0000000000 --- a/packages/editor-ui/src/components/HtmlEditor/theme.ts +++ /dev/null @@ -1,97 +0,0 @@ -import { HighlightStyle, syntaxHighlighting } from '@codemirror/language'; -import { EditorView } from '@codemirror/view'; -import { tags } from '@lezer/highlight'; - -export const theme = ({ isReadOnly }: { isReadOnly: boolean }) => [ - EditorView.theme({ - '&': { - 'font-size': '0.8em', - border: 'var(--border-base)', - borderRadius: 'var(--border-radius-base)', - backgroundColor: 'var(--color-code-background)', - color: 'var(--color-code-foreground)', - }, - '.cm-content': { - fontFamily: "Menlo, Consolas, 'DejaVu Sans Mono', monospace !important", - caretColor: 'var(--color-code-caret)', - }, - '.cm-cursor, .cm-dropCursor': { - borderLeftColor: 'var(--color-code-caret)', - }, - '&.cm-editor': { - ...(isReadOnly ? { backgroundColor: 'var(--color-code-background-readonly)' } : {}), - borderColor: 'var(--border-color-base)', - }, - '&.cm-editor.cm-focused': { - outline: '0', - }, - '&.cm-focused .cm-selectionBackgroundm .cm-selectionBackground, .cm-content ::selection': { - backgroundColor: 'var(--color-code-selection)', - }, - '.cm-activeLine': { - backgroundColor: 'var(--color-code-lineHighlight)', - }, - '.cm-activeLineGutter': { - backgroundColor: 'var(--color-code-lineHighlight)', - }, - '.cm-gutters': { - backgroundColor: isReadOnly - ? 'var(--color-code-background-readonly)' - : 'var(--color-code-gutterBackground)', - color: 'var(--color-code-gutterForeground)', - borderTopLeftRadius: 'var(--border-radius-base)', - borderBottomLeftRadius: 'var(--border-radius-base)', - borderRightColor: 'var(--border-color-base)', - }, - '.cm-scroller': { - overflow: 'auto', - maxHeight: '350px', - }, - }), - syntaxHighlighting( - HighlightStyle.define([ - { tag: tags.keyword, color: '#c678dd' }, - { - tag: [tags.name, tags.deleted, tags.character, tags.propertyName, tags.macroName], - color: '#e06c75', - }, - { tag: [tags.function(tags.variableName), tags.labelName], color: '#61afef' }, - { tag: [tags.color, tags.constant(tags.name), tags.standard(tags.name)], color: '#d19a66' }, - { tag: [tags.definition(tags.name), tags.separator], color: '#abb2bf' }, - { - tag: [ - tags.typeName, - tags.className, - tags.number, - tags.changed, - tags.annotation, - tags.modifier, - tags.self, - tags.namespace, - ], - color: '#e06c75', - }, - { - tag: [ - tags.operator, - tags.operatorKeyword, - tags.url, - tags.escape, - tags.regexp, - tags.link, - tags.special(tags.string), - ], - color: '#56b6c2', - }, - { tag: [tags.meta, tags.comment], color: '#7d8799' }, - { tag: tags.strong, fontWeight: 'bold' }, - { tag: tags.emphasis, fontStyle: 'italic' }, - { tag: tags.strikethrough, textDecoration: 'line-through' }, - { tag: tags.link, color: '#7d8799', textDecoration: 'underline' }, - { tag: tags.heading, fontWeight: 'bold', color: '#e06c75' }, - { tag: [tags.atom, tags.bool, tags.special(tags.variableName)], color: '#d19a66' }, - { tag: [tags.processingInstruction, tags.string, tags.inserted], color: '#98c379' }, - { tag: tags.invalid, color: 'red', 'font-weight': 'bold' }, - ]), - ), -]; diff --git a/packages/editor-ui/src/components/JsEditor/JsEditor.vue b/packages/editor-ui/src/components/JsEditor/JsEditor.vue index 805eb13893..cb9c60c85b 100644 --- a/packages/editor-ui/src/components/JsEditor/JsEditor.vue +++ b/packages/editor-ui/src/components/JsEditor/JsEditor.vue @@ -1,26 +1,30 @@ + + diff --git a/packages/editor-ui/src/components/JsonEditor/JsonEditor.vue b/packages/editor-ui/src/components/JsonEditor/JsonEditor.vue index be5762f5d8..82ab0bfe99 100644 --- a/packages/editor-ui/src/components/JsonEditor/JsonEditor.vue +++ b/packages/editor-ui/src/components/JsonEditor/JsonEditor.vue @@ -1,26 +1,30 @@ + + diff --git a/packages/editor-ui/src/components/NodeSettings.vue b/packages/editor-ui/src/components/NodeSettings.vue index 2186e23717..79a0e6c8f3 100644 --- a/packages/editor-ui/src/components/NodeSettings.vue +++ b/packages/editor-ui/src/components/NodeSettings.vue @@ -1166,7 +1166,7 @@ export default defineComponent({ .node-parameters-wrapper { overflow-y: auto; - padding: 0 var(--spacing-m) 200px var(--spacing-m); + padding: 0 var(--spacing-m) var(--spacing-l) var(--spacing-m); flex-grow: 1; } diff --git a/packages/editor-ui/src/components/ParameterInput.vue b/packages/editor-ui/src/components/ParameterInput.vue index 7860c2030d..e57702929f 100644 --- a/packages/editor-ui/src/components/ParameterInput.vue +++ b/packages/editor-ui/src/components/ParameterInput.vue @@ -15,7 +15,7 @@ :is-read-only="isReadOnly" :redact-values="shouldRedactValue" @closeDialog="closeExpressionEditDialog" - @update:modelValue="expressionUpdated" + @update:model-value="expressionUpdated" >
-
+
+ + + + +
@@ -90,11 +127,12 @@ :path="path" :is-read-only="isReadOnly" @closeDialog="closeTextEditDialog" - @update:modelValue="expressionUpdated" + @update:model-value="expressionUpdated" > + @update:model-value="valueChangedDebounced" + > + + + @update:model-value="valueChangedDebounced" + > + + + @update:model-value="valueChangedDebounced" + > + + + @update:model-value="valueChangedDebounced" + > + + + @update:model-value="valueChangedDebounced" + > + +
@@ -283,7 +380,7 @@ :loading="remoteParameterOptionsLoading" :disabled="isReadOnly || remoteParameterOptionsLoading" :title="displayTitle" - @update:modelValue="valueChanged" + @update:model-value="valueChanged" @keydown.stop @focus="setFocus" @blur="onBlur" @@ -321,7 +418,7 @@ :disabled="isReadOnly || remoteParameterOptionsLoading" :title="displayTitle" :placeholder="i18n.baseText('parameterInput.select')" - @update:modelValue="valueChanged" + @update:model-value="valueChanged" @keydown.stop @focus="setFocus" @blur="onBlur" @@ -358,7 +455,7 @@ active-color="#13ce66" :model-value="displayValue" :disabled="isReadOnly" - @update:modelValue="valueChanged" + @update:model-value="valueChanged" />
@@ -1056,7 +1153,7 @@ export default defineComponent({ return; } - if (this.editorType) { + if (this.editorType || this.parameter.type === 'json') { this.codeEditDialogVisible = true; } else { this.textEditDialogVisible = true; @@ -1418,4 +1515,12 @@ export default defineComponent({ .invalid { border-color: var(--color-danger); } + +.code-edit-dialog { + height: 70vh; + + .code-node-editor { + height: 100%; + } +} diff --git a/packages/editor-ui/src/components/SqlEditor/SqlEditor.vue b/packages/editor-ui/src/components/SqlEditor/SqlEditor.vue index 7847cc75f8..fd33c1ea02 100644 --- a/packages/editor-ui/src/components/SqlEditor/SqlEditor.vue +++ b/packages/editor-ui/src/components/SqlEditor/SqlEditor.vue @@ -1,7 +1,9 @@