+
+
+ {{ i18n.baseText('parameterInput.result') }}
+
+
+
+
+ {{ i18n.baseText('parameterInput.item') }}
+
+
+
+
+
+
+
+
+ {{ i18n.baseText('parameterInput.hoverTableItemTip') }}
+
+
+
+
+
+
+ >
+
diff --git a/packages/editor-ui/src/components/InlineExpressionEditor/__tests__/InlineExpressionEditorOutput.test.ts b/packages/editor-ui/src/components/InlineExpressionEditor/__tests__/InlineExpressionEditorOutput.test.ts
index b88a453d20..d996f4fa90 100644
--- a/packages/editor-ui/src/components/InlineExpressionEditor/__tests__/InlineExpressionEditorOutput.test.ts
+++ b/packages/editor-ui/src/components/InlineExpressionEditor/__tests__/InlineExpressionEditorOutput.test.ts
@@ -8,6 +8,7 @@ describe('InlineExpressionEditorOutput.vue', () => {
pinia: createTestingPinia(),
props: {
hoveringItemNumber: 0,
+ visible: true,
segments: [
{
from: 0,
@@ -56,6 +57,7 @@ describe('InlineExpressionEditorOutput.vue', () => {
pinia: createTestingPinia(),
props: {
hoveringItemNumber: 0,
+ visible: true,
segments: [
{
kind: 'plaintext',
diff --git a/packages/editor-ui/src/components/SqlEditor/SqlEditor.vue b/packages/editor-ui/src/components/SqlEditor/SqlEditor.vue
index daa2b33cd0..5c57108868 100644
--- a/packages/editor-ui/src/components/SqlEditor/SqlEditor.vue
+++ b/packages/editor-ui/src/components/SqlEditor/SqlEditor.vue
@@ -7,7 +7,6 @@
:segments="segments"
:is-read-only="isReadOnly"
:visible="hasFocus"
- :hovering-item-number="hoveringItemNumber"
/>
@@ -25,7 +24,6 @@ import {
tabKeyMap,
} from '@/plugins/codemirror/keymap';
import { n8nAutocompletion } from '@/plugins/codemirror/n8nLang';
-import { useNDVStore } from '@/stores/ndv.store';
import { ifNotIn } from '@codemirror/autocomplete';
import { history, toggleComment } from '@codemirror/commands';
import { LanguageSupport, bracketMatching, foldGutter, indentOnInput } from '@codemirror/language';
@@ -143,11 +141,6 @@ const {
skipSegments: ['Statement', 'CompositeIdentifier', 'Parens', 'Brackets'],
isReadOnly: props.isReadOnly,
});
-const ndvStore = useNDVStore();
-
-const hoveringItemNumber = computed(() => {
- return ndvStore.hoveringItemNumber;
-});
watch(
() => props.modelValue,
diff --git a/packages/editor-ui/src/components/__tests__/ExpressionParameterInput.test.ts b/packages/editor-ui/src/components/__tests__/ExpressionParameterInput.test.ts
index aaad6c9dc5..8943b1bc2b 100644
--- a/packages/editor-ui/src/components/__tests__/ExpressionParameterInput.test.ts
+++ b/packages/editor-ui/src/components/__tests__/ExpressionParameterInput.test.ts
@@ -2,6 +2,7 @@ import { createComponentRenderer } from '@/__tests__/render';
import ExpressionParameterInput from '@/components/ExpressionParameterInput.vue';
import { type TestingPinia, createTestingPinia } from '@pinia/testing';
import userEvent from '@testing-library/user-event';
+import { waitFor } from '@testing-library/vue';
import { setActivePinia } from 'pinia';
describe('ExpressionParameterInput', () => {
@@ -57,10 +58,12 @@ describe('ExpressionParameterInput', () => {
},
});
- const editor = container.querySelector('.cm-content') as HTMLDivElement;
- expect(editor).toBeInTheDocument();
- expect(editor.getAttribute('contenteditable')).toEqual('false');
- expect(editor.getAttribute('aria-readonly')).toEqual('true');
+ await waitFor(() => {
+ const editor = container.querySelector('.cm-content') as HTMLDivElement;
+ expect(editor).toBeInTheDocument();
+ expect(editor.getAttribute('contenteditable')).toEqual('false');
+ expect(editor.getAttribute('aria-readonly')).toEqual('true');
+ });
});
});
});
diff --git a/packages/editor-ui/src/components/__tests__/SQLEditor.test.ts b/packages/editor-ui/src/components/__tests__/SQLEditor.test.ts
index 6de5647cd4..234a7acc1b 100644
--- a/packages/editor-ui/src/components/__tests__/SQLEditor.test.ts
+++ b/packages/editor-ui/src/components/__tests__/SQLEditor.test.ts
@@ -6,6 +6,7 @@ import { createTestingPinia } from '@pinia/testing';
import SqlEditor from '@/components/SqlEditor/SqlEditor.vue';
import { renderComponent } from '@/__tests__/render';
import { waitFor } from '@testing-library/vue';
+import { userEvent } from '@testing-library/user-event';
import { setActivePinia } from 'pinia';
import { useRouter } from 'vue-router';
import { useWorkflowsStore } from '@/stores/workflows.store';
@@ -20,6 +21,10 @@ const DEFAULT_SETUP = {
},
};
+async function focusEditor(container: Element) {
+ await waitFor(() => expect(container.querySelector('.cm-line')).toBeInTheDocument());
+ await userEvent.click(container.querySelector('.cm-line') as Element);
+}
const nodes = [
{
id: '1',
@@ -70,7 +75,7 @@ describe('SqlEditor.vue', () => {
});
it('renders basic query', async () => {
- const { getByTestId } = renderComponent(SqlEditor, {
+ const { getByTestId, container } = renderComponent(SqlEditor, {
...DEFAULT_SETUP,
props: {
...DEFAULT_SETUP.props,
@@ -78,6 +83,7 @@ describe('SqlEditor.vue', () => {
},
});
+ await focusEditor(container);
await waitFor(() =>
expect(getByTestId(EXPRESSION_OUTPUT_TEST_ID)).toHaveTextContent('SELECT * FROM users'),
);
@@ -85,7 +91,7 @@ describe('SqlEditor.vue', () => {
it('renders basic query with expression', async () => {
mockResolveExpression().mockReturnValueOnce('users');
- const { getByTestId } = renderComponent(SqlEditor, {
+ const { getByTestId, container } = renderComponent(SqlEditor, {
...DEFAULT_SETUP,
props: {
...DEFAULT_SETUP.props,
@@ -93,6 +99,7 @@ describe('SqlEditor.vue', () => {
},
});
+ await focusEditor(container);
await waitFor(() =>
expect(getByTestId(EXPRESSION_OUTPUT_TEST_ID)).toHaveTextContent('SELECT * FROM users'),
);
@@ -100,13 +107,15 @@ describe('SqlEditor.vue', () => {
it('renders resolved expressions with dot between resolvables', async () => {
mockResolveExpression().mockReturnValueOnce('public.users');
- const { getByTestId } = renderComponent(SqlEditor, {
+ const { getByTestId, container } = renderComponent(SqlEditor, {
...DEFAULT_SETUP,
props: {
...DEFAULT_SETUP.props,
modelValue: 'SELECT * FROM {{ $json.schema }}.{{ $json.table }}',
},
});
+
+ await focusEditor(container);
await waitFor(() =>
expect(getByTestId(EXPRESSION_OUTPUT_TEST_ID)).toHaveTextContent(
'SELECT * FROM public.users',
@@ -120,7 +129,7 @@ describe('SqlEditor.vue', () => {
.mockReturnValueOnce('users')
.mockReturnValueOnce('id')
.mockReturnValueOnce(0);
- const { getByTestId } = renderComponent(SqlEditor, {
+ const { getByTestId, container } = renderComponent(SqlEditor, {
...DEFAULT_SETUP,
props: {
...DEFAULT_SETUP.props,
@@ -128,6 +137,8 @@ describe('SqlEditor.vue', () => {
'SELECT * FROM {{ $json.schema }}.{{ $json.table }} WHERE {{ $json.id }} > {{ $json.limit - 10 }}',
},
});
+
+ await focusEditor(container);
await waitFor(() =>
expect(getByTestId(EXPRESSION_OUTPUT_TEST_ID)).toHaveTextContent(
'SELECT * FROM public.users WHERE id > 0',
@@ -141,7 +152,7 @@ describe('SqlEditor.vue', () => {
.mockReturnValueOnce('users')
.mockReturnValueOnce(0)
.mockReturnValueOnce(false);
- const { getByTestId } = renderComponent(SqlEditor, {
+ const { getByTestId, container } = renderComponent(SqlEditor, {
...DEFAULT_SETUP,
props: {
...DEFAULT_SETUP.props,
@@ -149,6 +160,8 @@ describe('SqlEditor.vue', () => {
'SELECT * FROM {{ $json.schema }}.{{ $json.table }}\n WHERE id > {{ $json.limit - 10 }}\n AND active = {{ $json.active }};',
},
});
+
+ await focusEditor(container);
await waitFor(() =>
expect(getByTestId(EXPRESSION_OUTPUT_TEST_ID)).toHaveTextContent(
'SELECT * FROM public.users WHERE id > 0 AND active = false;',
diff --git a/packages/editor-ui/src/composables/useExpressionEditor.ts b/packages/editor-ui/src/composables/useExpressionEditor.ts
index a4a423f512..f28b687b06 100644
--- a/packages/editor-ui/src/composables/useExpressionEditor.ts
+++ b/packages/editor-ui/src/composables/useExpressionEditor.ts
@@ -105,7 +105,7 @@ export const useExpressionEditor = ({
const { from, to, text, token } = segment;
if (token === 'Resolvable') {
- const { resolved, error, fullError } = resolve(text, hoveringItem.value);
+ const { resolved, error, fullError } = resolve(text, targetItem.value);
acc.push({
kind: 'resolvable',
from,
@@ -253,7 +253,7 @@ export const useExpressionEditor = ({
return end !== undefined && expressionExtensionNames.value.has(end);
}
- function resolve(resolvable: string, hoverItem: TargetItem | null) {
+ function resolve(resolvable: string, target: TargetItem | null) {
const result: { resolved: unknown; error: boolean; fullError: Error | null } = {
resolved: undefined,
error: false,
@@ -268,7 +268,7 @@ export const useExpressionEditor = ({
let opts;
if (ndvStore.isInputParentOfActiveNode) {
opts = {
- targetItem: hoverItem ?? undefined,
+ targetItem: target ?? undefined,
inputNodeName: ndvStore.ndvInputNodeName,
inputRunIndex: ndvStore.ndvInputRunIndex,
inputBranchIndex: ndvStore.ndvInputBranchIndex,
@@ -306,8 +306,21 @@ export const useExpressionEditor = ({
return result;
}
- const hoveringItem = computed(() => {
- return ndvStore.hoveringItem;
+ const targetItem = computed
(() => {
+ if (ndvStore.hoveringItem) {
+ return ndvStore.hoveringItem;
+ }
+
+ if (ndvStore.expressionOutputItemIndex && ndvStore.ndvInputNodeName) {
+ return {
+ nodeName: ndvStore.ndvInputNodeName,
+ runIndex: ndvStore.ndvInputRunIndex ?? 0,
+ outputIndex: ndvStore.ndvInputBranchIndex ?? 0,
+ itemIndex: ndvStore.expressionOutputItemIndex,
+ };
+ }
+
+ return null;
});
const resolvableSegments = computed(() => {
@@ -372,14 +385,12 @@ export const useExpressionEditor = ({
});
watch(
- [
- () => workflowsStore.getWorkflowExecution,
- () => workflowsStore.getWorkflowRunData,
- () => ndvStore.hoveringItemNumber,
- ],
+ [() => workflowsStore.getWorkflowExecution, () => workflowsStore.getWorkflowRunData],
debouncedUpdateSegments,
);
+ watch(targetItem, updateSegments);
+
watch(resolvableSegments, updateHighlighting);
function setCursorPosition(pos: number | 'lastExpression' | 'end'): void {
diff --git a/packages/editor-ui/src/constants.ts b/packages/editor-ui/src/constants.ts
index 8a07115afa..aedd7743ab 100644
--- a/packages/editor-ui/src/constants.ts
+++ b/packages/editor-ui/src/constants.ts
@@ -401,6 +401,7 @@ export const LOCAL_STORAGE_PIN_DATA_DISCOVERY_NDV_FLAG = 'N8N_PIN_DATA_DISCOVERY
export const LOCAL_STORAGE_PIN_DATA_DISCOVERY_CANVAS_FLAG = 'N8N_PIN_DATA_DISCOVERY_CANVAS';
export const LOCAL_STORAGE_MAPPING_IS_ONBOARDED = 'N8N_MAPPING_ONBOARDED';
export const LOCAL_STORAGE_AUTOCOMPLETE_IS_ONBOARDED = 'N8N_AUTOCOMPLETE_ONBOARDED';
+export const LOCAL_STORAGE_TABLE_HOVER_IS_ONBOARDED = 'N8N_TABLE_HOVER_ONBOARDED';
export const LOCAL_STORAGE_MAIN_PANEL_RELATIVE_WIDTH = 'N8N_MAIN_PANEL_RELATIVE_WIDTH';
export const LOCAL_STORAGE_ACTIVE_MODAL = 'N8N_ACTIVE_MODAL';
export const LOCAL_STORAGE_THEME = 'N8N_THEME';
diff --git a/packages/editor-ui/src/plugins/i18n/locales/en.json b/packages/editor-ui/src/plugins/i18n/locales/en.json
index b14818fdb4..548db56c76 100644
--- a/packages/editor-ui/src/plugins/i18n/locales/en.json
+++ b/packages/editor-ui/src/plugins/i18n/locales/en.json
@@ -1244,7 +1244,9 @@
"parameterInput.inputField": "input field",
"parameterInput.dragTipAfterPill": "from the left to use it here.",
"parameterInput.learnMore": "Learn more",
- "parameterInput.resultForItem": "Result for Item",
+ "parameterInput.result": "Result",
+ "parameterInput.item": "Item",
+ "parameterInput.hoverTableItemTip": "You can also do this by hovering over input/output items in the table view",
"parameterInput.emptyString": "[empty]",
"parameterInput.customApiCall": "Custom API Call",
"parameterInput.error": "ERROR",
diff --git a/packages/editor-ui/src/stores/ndv.store.ts b/packages/editor-ui/src/stores/ndv.store.ts
index b2e7e315b3..1073cc44ff 100644
--- a/packages/editor-ui/src/stores/ndv.store.ts
+++ b/packages/editor-ui/src/stores/ndv.store.ts
@@ -10,6 +10,7 @@ import { useStorage } from '@/composables/useStorage';
import {
LOCAL_STORAGE_AUTOCOMPLETE_IS_ONBOARDED,
LOCAL_STORAGE_MAPPING_IS_ONBOARDED,
+ LOCAL_STORAGE_TABLE_HOVER_IS_ONBOARDED,
STORES,
} from '@/constants';
import type { INodeExecutionData, INodeIssues } from 'n8n-workflow';
@@ -47,6 +48,7 @@ export const useNDVStore = defineStore(STORES.NDV, {
focusedInputPath: '',
mappingTelemetry: {},
hoveringItem: null,
+ expressionOutputItemIndex: 0,
draggable: {
isDragging: false,
type: '',
@@ -55,6 +57,7 @@ export const useNDVStore = defineStore(STORES.NDV, {
activeTarget: null,
},
isMappingOnboarded: useStorage(LOCAL_STORAGE_MAPPING_IS_ONBOARDED).value === 'true',
+ isTableHoverOnboarded: useStorage(LOCAL_STORAGE_TABLE_HOVER_IS_ONBOARDED).value === 'true',
isAutocompleteOnboarded: useStorage(LOCAL_STORAGE_AUTOCOMPLETE_IS_ONBOARDED).value === 'true',
highlightDraggables: false,
}),
@@ -79,14 +82,20 @@ export const useNDVStore = defineStore(STORES.NDV, {
return [];
}
- return executionData.data?.resultData?.runData?.[inputNodeName]?.[inputRunIndex]?.data
- ?.main?.[inputBranchIndex];
+ return (
+ executionData.data?.resultData?.runData?.[inputNodeName]?.[inputRunIndex]?.data?.main?.[
+ inputBranchIndex
+ ] ?? []
+ );
+ },
+ ndvInputDataWithPinnedData(): INodeExecutionData[] {
+ const data = this.ndvInputData;
+ return this.ndvInputNodeName
+ ? useWorkflowsStore().pinDataByNodeName(this.ndvInputNodeName) ?? data
+ : data;
},
hasInputData(): boolean {
- const data = this.ndvInputData;
- const pinData =
- this.ndvInputNodeName && useWorkflowsStore().pinDataByNodeName(this.ndvInputNodeName);
- return !!(data && data.length > 0) || !!(pinData && pinData.length > 0);
+ return this.ndvInputDataWithPinnedData.length > 0;
},
getPanelDisplayMode() {
return (panel: NodePanelType) => this[panel].displayMode;
@@ -237,6 +246,7 @@ export const useNDVStore = defineStore(STORES.NDV, {
this.mappingTelemetry = {};
},
setHoveringItem(item: null | NDVState['hoveringItem']): void {
+ if (item) this.setTableHoverOnboarded();
this.hoveringItem = item;
},
setNDVBranchIndex(e: { pane: 'input' | 'output'; branchIndex: number }): void {
@@ -249,6 +259,10 @@ export const useNDVStore = defineStore(STORES.NDV, {
this.isMappingOnboarded = true;
useStorage(LOCAL_STORAGE_MAPPING_IS_ONBOARDED).value = 'true';
},
+ setTableHoverOnboarded() {
+ this.isTableHoverOnboarded = true;
+ useStorage(LOCAL_STORAGE_TABLE_HOVER_IS_ONBOARDED).value = 'true';
+ },
setAutocompleteOnboarded() {
this.isAutocompleteOnboarded = true;
useStorage(LOCAL_STORAGE_AUTOCOMPLETE_IS_ONBOARDED).value = 'true';