feat(editor): Add item selector to expression output (#9281)

This commit is contained in:
Elias Meire 2024-05-09 14:45:31 +02:00 committed by GitHub
parent 1c1e4443f4
commit dc5994b185
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
20 changed files with 313 additions and 98 deletions

View file

@ -75,7 +75,7 @@ describe('Resource Locator', () => {
ndv.actions.setInvalidExpression({ fieldName: 'fieldId' });
ndv.getters.inputDataContainer().click(); // remove focus from input, hide expression preview
ndv.getters.inputPanel().click(); // remove focus from input, hide expression preview
ndv.getters.resourceLocatorInput('rlc').click();

View file

@ -325,7 +325,7 @@ describe('NDV', () => {
ndv.actions.setInvalidExpression({ fieldName: 'fieldId', delay: 200 });
ndv.getters.inputDataContainer().click(); // remove focus from input, hide expression preview
ndv.getters.inputPanel().click(); // remove focus from input, hide expression preview
ndv.getters.parameterInput('remoteOptions').click();
@ -712,4 +712,31 @@ describe('NDV', () => {
workflowPage.getters.successToast().should('exist');
});
});
it('should allow selecting item for expressions', () => {
workflowPage.actions.visit();
cy.createFixtureWorkflow('Test_workflow_3.json', `My test workflow`);
workflowPage.actions.openNode('Set');
ndv.actions.typeIntoParameterInput('value', '='); // switch to expressions
ndv.actions.typeIntoParameterInput('value', '{{', {
parseSpecialCharSequences: false,
});
ndv.actions.typeIntoParameterInput('value', '$json.input[0].count');
ndv.getters.inlineExpressionEditorOutput().should('have.text', '0');
ndv.actions.expressionSelectNextItem();
ndv.getters.inlineExpressionEditorOutput().should('have.text', '1');
ndv.getters.inlineExpressionEditorItemInput().should('have.value', '1');
ndv.getters.inlineExpressionEditorItemNextButton().should('be.disabled');
ndv.actions.expressionSelectPrevItem();
ndv.getters.inlineExpressionEditorOutput().should('have.text', '0');
ndv.getters.inlineExpressionEditorItemInput().should('have.value', '0');
ndv.getters.inlineExpressionEditorItemPrevButton().should('be.disabled');
ndv.actions.expressionSelectItem(1);
ndv.getters.inlineExpressionEditorOutput().should('have.text', '1');
});
});

View file

@ -40,6 +40,12 @@ export class NDV extends BasePage {
this.getters.inputTableRow(row).find('td').eq(col),
inlineExpressionEditorInput: () => cy.getByTestId('inline-expression-editor-input'),
inlineExpressionEditorOutput: () => cy.getByTestId('inline-expression-editor-output'),
inlineExpressionEditorItemInput: () =>
cy.getByTestId('inline-expression-editor-item-input').find('input'),
inlineExpressionEditorItemPrevButton: () =>
cy.getByTestId('inline-expression-editor-item-prev'),
inlineExpressionEditorItemNextButton: () =>
cy.getByTestId('inline-expression-editor-item-next'),
nodeParameters: () => cy.getByTestId('node-parameters'),
parameterInput: (parameterName: string) => cy.getByTestId(`parameter-input-${parameterName}`),
parameterInputIssues: (parameterName: string) =>
@ -290,6 +296,15 @@ export class NDV extends BasePage {
.click({ force: true });
this.getters.parameterInput('operation').find('input').should('have.value', operation);
},
expressionSelectItem: (index: number) => {
this.getters.inlineExpressionEditorItemInput().type(`{selectall}${index}`);
},
expressionSelectNextItem: () => {
this.getters.inlineExpressionEditorItemNextButton().click();
},
expressionSelectPrevItem: () => {
this.getters.inlineExpressionEditorItemPrevButton().click();
},
};
}

View file

@ -95,3 +95,24 @@ Sizes.args = {
placeholder: 'placeholder...',
controls: false,
};
const ControlsTemplate: StoryFn = (args, { argTypes }) => ({
setup: () => ({ args }),
props: Object.keys(argTypes),
components: {
N8nInputNumber,
},
template:
'<div> <n8n-input-number style="margin-bottom:10px" v-bind="args" v-model="val" @update:modelValue="onUpdateModelValue" /> <n8n-input-number style="margin-bottom:10px" v-bind="args" size="medium" v-model="val" @update:modelValue="onUpdateModelValue" /> <n8n-input-number style="margin-bottom:10px" v-bind="args" size="small" v-model="val" @update:modelValue="onUpdateModelValue" /> <n8n-input-number style="margin-bottom:10px" v-bind="args" v-model="val" size="mini" @update:modelValue="onUpdateModelValue" /> <n8n-input-number controls-position="right" style="margin-bottom:10px" v-bind="args" v-model="val" @update:modelValue="onUpdateModelValue" /> <n8n-input-number controls-position="right" style="margin-bottom:10px" v-bind="args" size="medium" v-model="val" @update:modelValue="onUpdateModelValue" /> <n8n-input-number controls-position="right" style="margin-bottom:10px" v-bind="args" size="small" v-model="val" @update:modelValue="onUpdateModelValue" /> <n8n-input-number controls-position="right" style="margin-bottom:10px" v-bind="args" v-model="val" size="mini" @update:modelValue="onUpdateModelValue" /> </div>',
methods,
data() {
return {
val: '',
};
},
});
export const Controls = ControlsTemplate.bind({});
Controls.args = {
placeholder: 'placeholder...',
};

View file

@ -1,16 +1,16 @@
<script lang="ts">
<script setup lang="ts">
import type { InputSize } from '@/types';
import { ElInputNumber } from 'element-plus';
import { defineComponent } from 'vue';
export default defineComponent({
name: 'N8nInputNumber',
components: {
ElInputNumber,
},
props: {
...ElInputNumber.props,
},
});
type InputNumberProps = {
size?: InputSize;
min?: number;
max?: number;
step?: number;
precision?: number;
};
defineProps<InputNumberProps>();
</script>
<template>

View file

@ -88,6 +88,10 @@ const classes = computed(() => {
color: var(--color-primary);
}
.secondary {
color: var(--color-secondary);
}
.text-dark {
color: var(--color-text-dark);
}

View file

@ -466,6 +466,7 @@ $input-small-height: 30px !default;
$input-mini-font-size: 12px;
/// height||Other|4
$input-mini-height: 26px !default;
$input-number-control-border-radius: 3px;
/* Cascader
-------------------------- */

View file

@ -32,12 +32,16 @@
}
@include mixins.e((increase, decrease)) {
--disabled-color: var(--color-text-light);
position: absolute;
z-index: 1;
top: 1px;
bottom: 1px;
display: flex;
align-items: center;
justify-content: center;
width: var.$input-height;
height: auto;
text-align: center;
background: var.$background-color-base;
color: var(--color-text-dark);
cursor: pointer;
@ -59,13 +63,15 @@
@include mixins.e(increase) {
right: 1px;
border-radius: 0 var(--border-radius-base) var(--border-radius-base) 0;
border-radius: 0 var.$input-number-control-border-radius var.$input-number-control-border-radius
0;
border-left: var(--border-base);
}
@include mixins.e(decrease) {
left: 1px;
border-radius: var(--border-radius-base) 0 0 var(--border-radius-base);
border-radius: var.$input-number-control-border-radius 0 0
var.$input-number-control-border-radius;
border-right: var(--border-base);
}
@ -82,7 +88,7 @@
}
@include mixins.m(medium) {
line-height: #{var.$input-medium-height - 2};
line-height: #{var.$input-medium-height - 4};
@include mixins.e((increase, decrease)) {
width: var.$input-medium-height;
@ -114,7 +120,7 @@
}
@include mixins.m(mini) {
line-height: #{var.$input-mini-height - 2};
line-height: #{var.$input-mini-height - 4};
@include mixins.e((increase, decrease)) {
width: var.$input-mini-height;
@ -134,20 +140,33 @@
@include mixins.when(without-controls) {
.el-input__inner {
text-align: left;
padding-left: 12px;
padding-right: 12px;
padding-left: var(--spacing-2xs);
padding-right: var(--spacing-2xs);
}
}
@include mixins.when(controls-right) {
.el-input__inner {
padding-left: 15px;
padding-right: #{var.$input-height + 10};
padding-left: var(--spacing-2xs);
padding-right: #{var.$input-height + 4};
}
&.el-input-number--medium .el-input__inner {
padding-right: #{var.$input-medium-height + 4};
}
&.el-input-number--small .el-input__inner {
padding-right: #{var.$input-small-height + 4};
}
&.el-input-number--mini .el-input__inner {
padding-left: var(--spacing-4xs);
padding-right: #{var.$input-mini-height + 4};
}
@include mixins.e((increase, decrease)) {
height: auto;
line-height: #{(var.$input-height - 2) * 0.5};
height: calc((100% - 1px) / 2);
bottom: auto;
[class*='el-icon'] {
transform: scale(0.8);
@ -155,7 +174,7 @@
}
@include mixins.e(increase) {
border-radius: 0 var(--border-radius-base) 0 0;
border-radius: 0 var.$input-number-control-border-radius 0 0;
border-bottom: var(--border-base);
}
@ -166,28 +185,7 @@
left: auto;
border-right: none;
border-left: var(--border-base);
border-radius: 0 0 var(--border-radius-base) 0;
}
&[class*='medium'] {
[class*='increase'],
[class*='decrease'] {
line-height: #{(var.$input-medium-height - 2) * 0.5};
}
}
&[class*='small'] {
[class*='increase'],
[class*='decrease'] {
line-height: #{(var.$input-small-height - 2) * 0.5};
}
}
&[class*='mini'] {
[class*='increase'],
[class*='decrease'] {
line-height: #{(var.$input-mini-height - 2) * 0.5};
}
border-radius: 0 0 var.$input-number-control-border-radius 0;
}
}
}

View file

@ -1253,6 +1253,7 @@ export interface NDVState {
focusedInputPath: string;
mappingTelemetry: { [key: string]: string | number | boolean };
hoveringItem: null | TargetItem;
expressionOutputItemIndex: number;
draggable: {
isDragging: boolean;
type: string;
@ -1261,6 +1262,7 @@ export interface NDVState {
activeTarget: { id: string; stickyPosition: null | XYPosition } | null;
};
isMappingOnboarded: boolean;
isTableHoverOnboarded: boolean;
isAutocompleteOnboarded: boolean;
highlightDraggables: boolean;
}

View file

@ -48,9 +48,7 @@ const telemetry = useTelemetry();
const ndvStore = useNDVStore();
const workflowsStore = useWorkflowsStore();
const hoveringItemNumber = computed(() => ndvStore.hoveringItemNumber);
const isDragging = computed(() => ndvStore.isDraggableDragging);
const noInputData = computed(() => ndvStore.hasInputData);
function focus() {
if (inlineInput.value) {
@ -166,9 +164,7 @@ defineExpose({ focus });
:editor-state="editorState"
:segments="segments"
:is-read-only="isReadOnly"
:no-input-data="noInputData"
:visible="isFocused"
:hovering-item-number="hoveringItemNumber"
/>
</div>
</template>

View file

@ -103,6 +103,9 @@ onMounted(() => {
],
}),
});
highlighter.addColor(editor.value as EditorView, resolvedSegments.value);
highlighter.removeColor(editor.value as EditorView, plaintextSegments.value);
});
onBeforeUnmount(() => {

View file

@ -6,20 +6,20 @@ import type { Segment } from '@/types/expressions';
import ExpressionOutput from './ExpressionOutput.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[];
hoveringItemNumber: number;
unresolvedExpression?: string;
editorState?: EditorState;
selection?: SelectionRange;
visible?: boolean;
noInputData?: boolean;
}
withDefaults(defineProps<InlineExpressionEditorOutputProps>(), {
visible: false,
noInputData: false,
editorState: undefined,
selection: undefined,
unresolvedExpression: undefined,
@ -27,19 +27,95 @@ withDefaults(defineProps<InlineExpressionEditorOutputProps>(), {
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;
});
</script>
<template>
<div :class="visible ? $style.dropdown : $style.hidden">
<n8n-text v-if="!noInputData" size="small" compact :class="$style.header">
{{ i18n.baseText('parameterInput.resultForItem') }} {{ hoveringItemNumber }}
</n8n-text>
<div v-if="visible" :class="$style.dropdown" title="">
<div :class="$style.header">
<n8n-text bold size="small" compact>
{{ i18n.baseText('parameterInput.result') }}
</n8n-text>
<div :class="$style.item">
<n8n-text size="small" color="text-base" compact>
{{ i18n.baseText('parameterInput.item') }}
</n8n-text>
<div :class="$style.controls">
<N8nInputNumber
data-test-id="inline-expression-editor-item-input"
size="mini"
:controls="false"
:class="[$style.input, { [$style.hovering]: isHoveringItem }]"
:min="0"
:max="max"
:model-value="itemIndex"
@update:model-value="updateItemIndex"
></N8nInputNumber>
<N8nIconButton
data-test-id="inline-expression-editor-item-prev"
icon="chevron-left"
type="tertiary"
text
size="mini"
:disabled="!canSelectPrevItem"
@click="prevItem"
></N8nIconButton>
<N8nTooltip placement="right" :disabled="hideTableHoverHint">
<template #content>
<div>{{ i18n.baseText('parameterInput.hoverTableItemTip') }}</div>
</template>
<N8nIconButton
data-test-id="inline-expression-editor-item-next"
icon="chevron-right"
type="tertiary"
text
size="mini"
:disabled="!canSelectNextItem"
@click="nextItem"
></N8nIconButton>
</N8nTooltip>
</div>
</div>
</div>
<n8n-text :class="$style.body">
<ExpressionOutput
data-test-id="inline-expression-editor-output"
:segments="segments"
:extensions="theme"
></ExpressionOutput>
>
</ExpressionOutput>
</n8n-text>
<div :class="$style.footer">
<InlineExpressionTip
@ -52,10 +128,6 @@ const theme = outputTheme();
</template>
<style lang="scss" module>
.hidden {
display: none;
}
.dropdown {
display: flex;
flex-direction: column;
@ -73,7 +145,6 @@ const theme = outputTheme();
background-color: var(--color-code-background);
}
.header,
.body {
padding: var(--spacing-3xs);
}
@ -83,12 +154,22 @@ const theme = outputTheme();
}
.header {
display: flex;
justify-content: space-between;
align-items: center;
gap: var(--spacing-2xs);
color: var(--color-text-dark);
font-weight: var(--font-weight-bold);
padding-left: var(--spacing-2xs);
padding: 0 var(--spacing-2xs);
padding-top: var(--spacing-2xs);
}
.item {
display: flex;
align-items: center;
gap: var(--spacing-4xs);
}
.body {
padding-top: 0;
padding-left: var(--spacing-2xs);
@ -98,5 +179,33 @@ const theme = outputTheme();
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);
}
}
}
</style>

View file

@ -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',

View file

@ -7,7 +7,6 @@
:segments="segments"
:is-read-only="isReadOnly"
:visible="hasFocus"
:hovering-item-number="hoveringItemNumber"
/>
</div>
</template>
@ -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,

View file

@ -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');
});
});
});
});

View file

@ -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;',

View file

@ -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<TargetItem | null>(() => {
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<Resolvable[]>(() => {
@ -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 {

View file

@ -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';

View file

@ -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",

View file

@ -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';