diff --git a/cypress/e2e/14-mapping.cy.ts b/cypress/e2e/14-mapping.cy.ts index 2a33aee5c0..2e405e69e8 100644 --- a/cypress/e2e/14-mapping.cy.ts +++ b/cypress/e2e/14-mapping.cy.ts @@ -44,7 +44,7 @@ describe('Data mapping', () => { ndv.actions.mapDataFromHeader(2, 'value'); ndv.getters .inlineExpressionEditorInput() - .should('have.text', "{{ $json.timestamp }} {{ $json['Readable date'] }}"); + .should('have.text', "{{ $json['Readable date'] }}{{ $json.timestamp }}"); }); it('maps expressions from table json, and resolves value based on hover', () => { @@ -145,8 +145,8 @@ describe('Data mapping', () => { ndv.actions.mapToParameter('value'); ndv.getters .inlineExpressionEditorInput() - .should('have.text', '{{ $json.input[0].count }} {{ $json.input }}'); - ndv.actions.validateExpressionPreview('value', '0 [object Object]'); + .should('have.text', '{{ $json.input }}{{ $json.input[0].count }}'); + ndv.actions.validateExpressionPreview('value', '[object Object]0'); }); it('maps expressions from schema view', () => { @@ -170,8 +170,8 @@ describe('Data mapping', () => { ndv.actions.mapToParameter('value'); ndv.getters .inlineExpressionEditorInput() - .should('have.text', '{{ $json.input[0].count }} {{ $json.input }}'); - ndv.actions.validateExpressionPreview('value', '0 [object Object]'); + .should('have.text', '{{ $json.input }}{{ $json.input[0].count }}'); + ndv.actions.validateExpressionPreview('value', '[object Object]0'); }); it('maps expressions from previous nodes', () => { @@ -200,17 +200,17 @@ describe('Data mapping', () => { .inlineExpressionEditorInput() .should( 'have.text', - `{{ $('${SCHEDULE_TRIGGER_NODE_NAME}').item.json.input[0].count }} {{ $('${SCHEDULE_TRIGGER_NODE_NAME}').item.json.input }}`, + `{{ $('${SCHEDULE_TRIGGER_NODE_NAME}').item.json.input }}{{ $('${SCHEDULE_TRIGGER_NODE_NAME}').item.json.input[0].count }}`, ); ndv.actions.selectInputNode('Set'); ndv.getters.executingLoader().should('not.exist'); ndv.getters.inputDataContainer().should('exist'); - ndv.actions.validateExpressionPreview('value', '0 [object Object]'); + ndv.actions.validateExpressionPreview('value', '[object Object]0'); ndv.getters.inputTbodyCell(2, 0).realHover(); - ndv.actions.validateExpressionPreview('value', '1 [object Object]'); + ndv.actions.validateExpressionPreview('value', '[object Object]1'); }); it('maps keys to path', () => { @@ -284,8 +284,8 @@ describe('Data mapping', () => { ndv.actions.mapToParameter('value'); ndv.getters .inlineExpressionEditorInput() - .should('have.text', '{{ $json.input[0].count }} {{ $json.input }}'); - ndv.actions.validateExpressionPreview('value', '0 [object Object]'); + .should('have.text', '{{ $json.input }}{{ $json.input[0].count }}'); + ndv.actions.validateExpressionPreview('value', '[object Object]0'); }); it('renders expression preview when a previous node is selected', () => { @@ -342,4 +342,27 @@ describe('Data mapping', () => { .invoke('css', 'border') .should('include', 'dashed rgb(90, 76, 194)'); }); + + it('maps expressions to a specific location in the editor', () => { + cy.fixture('Test_workflow_3.json').then((data) => { + cy.get('body').paste(JSON.stringify(data)); + }); + workflowPage.actions.zoomToFit(); + + workflowPage.actions.openNode('Set'); + ndv.actions.clearParameterInput('value'); + ndv.actions.typeIntoParameterInput('value', '='); + ndv.actions.typeIntoParameterInput('value', 'hello world{enter}{enter}newline'); + + ndv.getters.inputDataContainer().should('exist').find('span').contains('count').realMouseDown(); + + ndv.actions.mapToParameter('value'); + + ndv.getters.inputDataContainer().find('span').contains('input').realMouseDown(); + ndv.actions.mapToParameter('value', 'bottom'); + + ndv.getters + .inlineExpressionEditorInput() + .should('have.text', '{{ $json.input[0].count }}hello worldnewline{{ $json.input }}'); + }); }); diff --git a/cypress/pages/ndv.ts b/cypress/pages/ndv.ts index 24e1922663..1a80d79707 100644 --- a/cypress/pages/ndv.ts +++ b/cypress/pages/ndv.ts @@ -205,9 +205,9 @@ export class NDV extends BasePage { const droppable = `[data-test-id="parameter-input-${parameterName}"]`; cy.draganddrop(draggable, droppable); }, - mapToParameter: (parameterName: string) => { + mapToParameter: (parameterName: string, position?: 'top' | 'center' | 'bottom') => { const droppable = `[data-test-id="parameter-input-${parameterName}"]`; - cy.draganddrop('', droppable); + cy.draganddrop('', droppable, { position }); }, switchInputMode: (type: 'Schema' | 'Table' | 'JSON' | 'Binary') => { this.getters.inputDisplayMode().find('label').contains(type).click({ force: true }); diff --git a/cypress/support/commands.ts b/cypress/support/commands.ts index a7fa994289..1f353ab7c5 100644 --- a/cypress/support/commands.ts +++ b/cypress/support/commands.ts @@ -175,7 +175,7 @@ Cypress.Commands.add('drag', (selector, pos, options) => { }); }); -Cypress.Commands.add('draganddrop', (draggableSelector, droppableSelector) => { +Cypress.Commands.add('draganddrop', (draggableSelector, droppableSelector, options) => { if (draggableSelector) { cy.get(draggableSelector).should('exist'); } @@ -197,7 +197,7 @@ Cypress.Commands.add('draganddrop', (draggableSelector, droppableSelector) => { cy.get(droppableSelector).realMouseMove(0, 0); cy.get(droppableSelector).realMouseMove(pageX, pageY); cy.get(droppableSelector).realHover(); - cy.get(droppableSelector).realMouseUp(); + cy.get(droppableSelector).realMouseUp({ position: options?.position ?? 'top' }); if (draggableSelector) { cy.get(draggableSelector).realMouseUp(); } diff --git a/cypress/support/index.ts b/cypress/support/index.ts index 9819e7c3a1..7c1897b11f 100644 --- a/cypress/support/index.ts +++ b/cypress/support/index.ts @@ -12,6 +12,10 @@ interface SigninPayload { password: string; } +interface DragAndDropOptions { + position: 'top' | 'center' | 'bottom'; +} + declare global { namespace Cypress { interface SuiteConfigOverrides { @@ -56,7 +60,11 @@ declare global { target: [number, number], options?: { abs?: boolean; index?: number; realMouse?: boolean; clickToFinish?: boolean }, ): void; - draganddrop(draggableSelector: string, droppableSelector: string): void; + draganddrop( + draggableSelector: string, + droppableSelector: string, + options?: Partial, + ): void; push(type: string, data: unknown): void; shouldNotHaveConsoleErrors(): void; window(): Chainable< diff --git a/packages/design-system/src/css/_tokens.dark.scss b/packages/design-system/src/css/_tokens.dark.scss index 4c3c078384..15930499e4 100644 --- a/packages/design-system/src/css/_tokens.dark.scss +++ b/packages/design-system/src/css/_tokens.dark.scss @@ -101,6 +101,7 @@ --color-pending-resolvable-foreground: var(--color-text-base); --color-pending-resolvable-background: var(--prim-gray-70-alpha-01); --color-expression-editor-background: var(--prim-gray-800); + --color-expression-editor-modal-background: var(--prim-gray-800); --color-expression-syntax-example: var(--prim-gray-670); --color-autocomplete-item-selected: var(--prim-color-secondary-tint-200); --color-autocomplete-section-header-border: var(--prim-gray-540); diff --git a/packages/design-system/src/css/_tokens.scss b/packages/design-system/src/css/_tokens.scss index 40bf465cfa..c63c5eb64b 100644 --- a/packages/design-system/src/css/_tokens.scss +++ b/packages/design-system/src/css/_tokens.scss @@ -135,6 +135,7 @@ --color-pending-resolvable-foreground: var(--color-text-base); --color-pending-resolvable-background: var(--prim-gray-40); --color-expression-editor-background: var(--prim-gray-0); + --color-expression-editor-modal-background: var(--color-background-base); --color-expression-syntax-example: var(--prim-gray-40); --color-autocomplete-item-selected: var(--color-secondary); --color-autocomplete-section-header-border: var(--color-foreground-light); diff --git a/packages/editor-ui/src/Interface.ts b/packages/editor-ui/src/Interface.ts index f7d683e278..986336a396 100644 --- a/packages/editor-ui/src/Interface.ts +++ b/packages/editor-ui/src/Interface.ts @@ -207,19 +207,6 @@ export interface ITableData { hasJson: { [key: string]: boolean }; } -export interface IVariableItemSelected { - variable: string; -} - -export interface IVariableSelectorOption { - name: string; - key?: string; - value?: string; - options?: IVariableSelectorOption[] | null; - allowParentSelect?: boolean; - dataType?: string; -} - // Simple version of n8n-workflow.Workflow export interface IWorkflowData { id?: string; diff --git a/packages/editor-ui/src/components/DraggableTarget.vue b/packages/editor-ui/src/components/DraggableTarget.vue index 0dcadd856e..6197d3c347 100644 --- a/packages/editor-ui/src/components/DraggableTarget.vue +++ b/packages/editor-ui/src/components/DraggableTarget.vue @@ -26,7 +26,7 @@ const props = withDefaults(defineProps(), { }); const emit = defineEmits<{ - drop: [value: string]; + drop: [value: string, event: MouseEvent]; }>(); const hovering = ref(false); @@ -60,10 +60,10 @@ function onMouseLeave() { hovering.value = false; } -function onMouseUp() { +function onMouseUp(event: MouseEvent) { if (activeDrop.value) { const data = ndvStore.draggableData; - emit('drop', data); + emit('drop', data, event); } } diff --git a/packages/editor-ui/src/components/ExpressionEdit.vue b/packages/editor-ui/src/components/ExpressionEdit.vue deleted file mode 100644 index e61b4dbe05..0000000000 --- a/packages/editor-ui/src/components/ExpressionEdit.vue +++ /dev/null @@ -1,388 +0,0 @@ - - - - - diff --git a/packages/editor-ui/src/components/ExpressionEditModal.vue b/packages/editor-ui/src/components/ExpressionEditModal.vue new file mode 100644 index 0000000000..cb58f67a58 --- /dev/null +++ b/packages/editor-ui/src/components/ExpressionEditModal.vue @@ -0,0 +1,336 @@ + + + + + diff --git a/packages/editor-ui/src/components/ExpressionEditorModal/ExpressionEditorModalInput.vue b/packages/editor-ui/src/components/ExpressionEditorModal/ExpressionEditorModalInput.vue index 78c4400030..68a65ada6e 100644 --- a/packages/editor-ui/src/components/ExpressionEditorModal/ExpressionEditorModalInput.vue +++ b/packages/editor-ui/src/components/ExpressionEditorModal/ExpressionEditorModalInput.vue @@ -5,8 +5,8 @@ - diff --git a/packages/editor-ui/src/components/ExpressionEditorModal/theme.ts b/packages/editor-ui/src/components/ExpressionEditorModal/theme.ts index f47f7c1535..a35a18643f 100644 --- a/packages/editor-ui/src/components/ExpressionEditorModal/theme.ts +++ b/packages/editor-ui/src/components/ExpressionEditorModal/theme.ts @@ -1,7 +1,7 @@ import { EditorView } from '@codemirror/view'; import { highlighter } from '@/plugins/codemirror/resolvableHighlighter'; -const commonThemeProps = { +const commonThemeProps = (isReadOnly = false) => ({ '&': { borderWidth: 'var(--border-width-base)', borderStyle: 'var(--input-border-style, var(--border-style-base))', @@ -9,31 +9,33 @@ const commonThemeProps = { borderRadius: 'var(--input-border-radius, var(--border-radius-base))', backgroundColor: 'var(--color-expression-editor-background)', }, + '.cm-cursor, .cm-dropCursor': { + borderLeftColor: 'var(--color-code-caret)', + }, '&.cm-focused': { borderColor: 'var(--color-secondary)', outline: '0 !important', }, '.cm-content': { fontFamily: 'var(--font-family-monospace)', - height: '220px', padding: 'var(--spacing-xs)', color: 'var(--input-font-color, var(--color-text-dark))', - caretColor: 'var(--color-code-caret)', + caretColor: isReadOnly ? 'transparent' : 'var(--color-code-caret)', }, '.cm-line': { padding: '0', }, -}; +}); -export const inputTheme = () => { - const theme = EditorView.theme(commonThemeProps); +export const inputTheme = (isReadOnly = false) => { + const theme = EditorView.theme(commonThemeProps(isReadOnly)); return [theme, highlighter.resolvableStyle]; }; export const outputTheme = () => { const theme = EditorView.theme({ - ...commonThemeProps, + ...commonThemeProps(true), '.cm-valid-resolvable': { padding: '0 2px', borderRadius: '2px', diff --git a/packages/editor-ui/src/components/ExpressionParameterInput.vue b/packages/editor-ui/src/components/ExpressionParameterInput.vue index 5618c3ae26..86e4c540a2 100644 --- a/packages/editor-ui/src/components/ExpressionParameterInput.vue +++ b/packages/editor-ui/src/components/ExpressionParameterInput.vue @@ -1,5 +1,5 @@ diff --git a/packages/editor-ui/src/components/InlineExpressionEditor/InlineExpressionEditorOutput.vue b/packages/editor-ui/src/components/InlineExpressionEditor/InlineExpressionEditorOutput.vue index ff0eaa6732..634c8d340f 100644 --- a/packages/editor-ui/src/components/InlineExpressionEditor/InlineExpressionEditorOutput.vue +++ b/packages/editor-ui/src/components/InlineExpressionEditor/InlineExpressionEditorOutput.vue @@ -2,13 +2,13 @@ import type { EditorState, SelectionRange } from '@codemirror/state'; import { useI18n } from '@/composables/useI18n'; +import { useNDVStore } from '@/stores/ndv.store'; import type { Segment } from '@/types/expressions'; +import { onBeforeUnmount } from 'vue'; import ExpressionOutput from './ExpressionOutput.vue'; +import OutputItemSelect from './OutputItemSelect.vue'; import InlineExpressionTip from './InlineExpressionTip.vue'; import { outputTheme } from './theme'; -import { computed, onBeforeUnmount } from 'vue'; -import { useNDVStore } from '@/stores/ndv.store'; -import { N8nTooltip } from 'n8n-design-system/components'; interface InlineExpressionEditorOutputProps { segments: Segment[]; @@ -29,31 +29,6 @@ const i18n = useI18n(); const theme = outputTheme(); const ndvStore = useNDVStore(); -const hideTableHoverHint = computed(() => ndvStore.isTableHoverOnboarded); -const hoveringItem = computed(() => ndvStore.getHoveringItem); -const hoveringItemIndex = computed(() => hoveringItem.value?.itemIndex); -const isHoveringItem = computed(() => Boolean(hoveringItem.value)); -const itemsLength = computed(() => ndvStore.ndvInputDataWithPinnedData.length); -const itemIndex = computed(() => hoveringItemIndex.value ?? ndvStore.expressionOutputItemIndex); -const max = computed(() => Math.max(itemsLength.value - 1, 0)); -const isItemIndexEditable = computed(() => !isHoveringItem.value && itemsLength.value > 0); -const canSelectPrevItem = computed(() => isItemIndexEditable.value && itemIndex.value !== 0); -const canSelectNextItem = computed( - () => isItemIndexEditable.value && itemIndex.value < itemsLength.value - 1, -); - -function updateItemIndex(index: number) { - ndvStore.expressionOutputItemIndex = index; -} - -function nextItem() { - ndvStore.expressionOutputItemIndex = ndvStore.expressionOutputItemIndex + 1; -} - -function prevItem() { - ndvStore.expressionOutputItemIndex = ndvStore.expressionOutputItemIndex - 1; -} - onBeforeUnmount(() => { ndvStore.expressionOutputItemIndex = 0; }); @@ -66,48 +41,7 @@ onBeforeUnmount(() => { {{ i18n.baseText('parameterInput.result') }} -
- - {{ i18n.baseText('parameterInput.item') }} - - -
- - - - - - - -
-
+ { padding-top: var(--spacing-2xs); } - .item { - display: flex; - align-items: center; - gap: var(--spacing-4xs); - } - .body { padding-top: 0; padding-left: var(--spacing-2xs); @@ -179,33 +107,5 @@ onBeforeUnmount(() => { padding-top: var(--spacing-2xs); } } - - .controls { - display: flex; - align-items: center; - } - - .input { - --input-height: 22px; - --input-width: 26px; - --input-border-top-left-radius: var(--border-radius-base); - --input-border-bottom-left-radius: var(--border-radius-base); - --input-border-top-right-radius: var(--border-radius-base); - --input-border-bottom-right-radius: var(--border-radius-base); - max-width: var(--input-width); - line-height: calc(var(--input-height) - var(--spacing-4xs)); - - &.hovering { - --input-font-color: var(--color-secondary); - } - - :global(.el-input__inner) { - height: var(--input-height); - min-height: var(--input-height); - line-height: var(--input-height); - text-align: center; - padding: 0 var(--spacing-4xs); - } - } } diff --git a/packages/editor-ui/src/components/InlineExpressionEditor/OutputItemSelect.vue b/packages/editor-ui/src/components/InlineExpressionEditor/OutputItemSelect.vue new file mode 100644 index 0000000000..6845b77bf5 --- /dev/null +++ b/packages/editor-ui/src/components/InlineExpressionEditor/OutputItemSelect.vue @@ -0,0 +1,114 @@ + + + + + diff --git a/packages/editor-ui/src/components/InlineExpressionEditor/theme.ts b/packages/editor-ui/src/components/InlineExpressionEditor/theme.ts index 384cf4a01f..732996f6b7 100644 --- a/packages/editor-ui/src/components/InlineExpressionEditor/theme.ts +++ b/packages/editor-ui/src/components/InlineExpressionEditor/theme.ts @@ -41,6 +41,9 @@ export const inputTheme = ({ rows, isReadOnly } = { rows: 5, isReadOnly: false } 'var(--input-border-bottom-right-radius, var(--input-border-radius, var(--border-radius-base)))', backgroundColor: 'white', }, + '.cm-cursor, .cm-dropCursor': { + borderLeftColor: 'var(--color-code-caret)', + }, '.cm-scroller': { lineHeight: '1.68', }, diff --git a/packages/editor-ui/src/components/ParameterInput.vue b/packages/editor-ui/src/components/ParameterInput.vue index 0e6517ced6..533ad6c49d 100644 --- a/packages/editor-ui/src/components/ParameterInput.vue +++ b/packages/editor-ui/src/components/ParameterInput.vue @@ -1,16 +1,18 @@