mirror of
https://github.com/n8n-io/n8n.git
synced 2024-11-09 22:24:05 -08:00
feat(editor): Add item selector to expression output (#9281)
This commit is contained in:
parent
1c1e4443f4
commit
dc5994b185
|
@ -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();
|
||||
|
||||
|
|
|
@ -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');
|
||||
});
|
||||
});
|
||||
|
|
|
@ -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();
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
|
|
|
@ -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...',
|
||||
};
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -88,6 +88,10 @@ const classes = computed(() => {
|
|||
color: var(--color-primary);
|
||||
}
|
||||
|
||||
.secondary {
|
||||
color: var(--color-secondary);
|
||||
}
|
||||
|
||||
.text-dark {
|
||||
color: var(--color-text-dark);
|
||||
}
|
||||
|
|
|
@ -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
|
||||
-------------------------- */
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -103,6 +103,9 @@ onMounted(() => {
|
|||
],
|
||||
}),
|
||||
});
|
||||
|
||||
highlighter.addColor(editor.value as EditorView, resolvedSegments.value);
|
||||
highlighter.removeColor(editor.value as EditorView, plaintextSegments.value);
|
||||
});
|
||||
|
||||
onBeforeUnmount(() => {
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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',
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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');
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -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;',
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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';
|
||||
|
|
|
@ -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",
|
||||
|
|
|
@ -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';
|
||||
|
|
Loading…
Reference in a new issue