mirror of
https://github.com/n8n-io/n8n.git
synced 2025-03-05 20:50:17 -08:00
feat(editor): Show fixed collection parameter issues in UI (#12899)
This commit is contained in:
parent
152310b7a0
commit
12d686ce52
|
@ -4,21 +4,14 @@ import type { ILocalLoadOptionsFunctions, ResourceMapperFields } from 'n8n-workf
|
||||||
export async function loadSubWorkflowInputs(
|
export async function loadSubWorkflowInputs(
|
||||||
this: ILocalLoadOptionsFunctions,
|
this: ILocalLoadOptionsFunctions,
|
||||||
): Promise<ResourceMapperFields> {
|
): Promise<ResourceMapperFields> {
|
||||||
const { fields, dataMode, subworkflowInfo } = await loadWorkflowInputMappings.bind(this)();
|
const { fields, subworkflowInfo } = await loadWorkflowInputMappings.bind(this)();
|
||||||
let emptyFieldsNotice: string | undefined;
|
let emptyFieldsNotice: string | undefined;
|
||||||
if (fields.length === 0) {
|
if (fields.length === 0) {
|
||||||
const subworkflowLink = subworkflowInfo?.id
|
const subworkflowLink = subworkflowInfo?.id
|
||||||
? `<a href="/workflow/${subworkflowInfo?.id}" target="_blank">sub-workflow’s trigger</a>`
|
? `<a href="/workflow/${subworkflowInfo?.id}" target="_blank">sub-workflow’s trigger</a>`
|
||||||
: 'sub-workflow’s trigger';
|
: 'sub-workflow’s trigger';
|
||||||
|
|
||||||
switch (dataMode) {
|
emptyFieldsNotice = `This sub-workflow will not receive any input when called by your AI node. Define your expected input in the ${subworkflowLink}.`;
|
||||||
case 'passthrough':
|
|
||||||
emptyFieldsNotice = `This sub-workflow will consume all input data passed to it. Define specific expected input in the ${subworkflowLink}.`;
|
|
||||||
break;
|
|
||||||
default:
|
|
||||||
emptyFieldsNotice = `This sub-workflow will not receive any input when called by your AI node. Define your expected input in the ${subworkflowLink}.`;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
return { fields, emptyFieldsNotice };
|
return { fields, emptyFieldsNotice };
|
||||||
}
|
}
|
||||||
|
|
|
@ -35,20 +35,29 @@ exports[`components > N8nCheckbox > should render with both child and label 1`]
|
||||||
class="n8n-input-label inputLabel heading medium"
|
class="n8n-input-label inputLabel heading medium"
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
class="title"
|
class="main-content"
|
||||||
>
|
>
|
||||||
<span
|
<div
|
||||||
class="n8n-text size-medium regular"
|
class="title"
|
||||||
>
|
>
|
||||||
|
<span
|
||||||
Checkbox
|
class="n8n-text size-medium regular"
|
||||||
<!--v-if-->
|
>
|
||||||
|
|
||||||
</span>
|
Checkbox
|
||||||
|
<!--v-if-->
|
||||||
|
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<!--v-if-->
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
class="trailing-content"
|
||||||
|
>
|
||||||
|
<!--v-if-->
|
||||||
|
<!--v-if-->
|
||||||
|
<!--v-if-->
|
||||||
</div>
|
</div>
|
||||||
<!--v-if-->
|
|
||||||
<!--v-if-->
|
|
||||||
<!--v-if-->
|
|
||||||
</label>
|
</label>
|
||||||
|
|
||||||
|
|
||||||
|
@ -126,20 +135,29 @@ exports[`components > N8nCheckbox > should render with label 1`] = `
|
||||||
class="n8n-input-label inputLabel heading medium"
|
class="n8n-input-label inputLabel heading medium"
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
class="title"
|
class="main-content"
|
||||||
>
|
>
|
||||||
<span
|
<div
|
||||||
class="n8n-text size-medium regular"
|
class="title"
|
||||||
>
|
>
|
||||||
|
<span
|
||||||
Checkbox
|
class="n8n-text size-medium regular"
|
||||||
<!--v-if-->
|
>
|
||||||
|
|
||||||
</span>
|
Checkbox
|
||||||
|
<!--v-if-->
|
||||||
|
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<!--v-if-->
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
class="trailing-content"
|
||||||
|
>
|
||||||
|
<!--v-if-->
|
||||||
|
<!--v-if-->
|
||||||
|
<!--v-if-->
|
||||||
</div>
|
</div>
|
||||||
<!--v-if-->
|
|
||||||
<!--v-if-->
|
|
||||||
<!--v-if-->
|
|
||||||
</label>
|
</label>
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -38,26 +38,35 @@ exports[`FormBox > should render the component 1`] = `
|
||||||
for="name"
|
for="name"
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
class="title"
|
class="main-content"
|
||||||
>
|
>
|
||||||
<span
|
<div
|
||||||
class="n8n-text size-small bold"
|
class="title"
|
||||||
>
|
>
|
||||||
|
|
||||||
Name
|
|
||||||
<span
|
<span
|
||||||
class="n8n-text primary size-small bold"
|
class="n8n-text size-small bold"
|
||||||
>
|
>
|
||||||
|
|
||||||
*
|
Name
|
||||||
|
<span
|
||||||
|
class="n8n-text primary size-small bold"
|
||||||
|
>
|
||||||
|
|
||||||
|
*
|
||||||
|
|
||||||
|
</span>
|
||||||
|
|
||||||
</span>
|
</span>
|
||||||
|
</div>
|
||||||
</span>
|
<!--v-if-->
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
class="trailing-content"
|
||||||
|
>
|
||||||
|
<!--v-if-->
|
||||||
|
<!--v-if-->
|
||||||
|
<!--v-if-->
|
||||||
</div>
|
</div>
|
||||||
<!--v-if-->
|
|
||||||
<!--v-if-->
|
|
||||||
<!--v-if-->
|
|
||||||
</label>
|
</label>
|
||||||
|
|
||||||
<div
|
<div
|
||||||
|
@ -110,26 +119,35 @@ exports[`FormBox > should render the component 1`] = `
|
||||||
for="email"
|
for="email"
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
class="title"
|
class="main-content"
|
||||||
>
|
>
|
||||||
<span
|
<div
|
||||||
class="n8n-text size-medium bold"
|
class="title"
|
||||||
>
|
>
|
||||||
|
|
||||||
Email
|
|
||||||
<span
|
<span
|
||||||
class="n8n-text primary size-medium bold"
|
class="n8n-text size-medium bold"
|
||||||
>
|
>
|
||||||
|
|
||||||
*
|
Email
|
||||||
|
<span
|
||||||
|
class="n8n-text primary size-medium bold"
|
||||||
|
>
|
||||||
|
|
||||||
|
*
|
||||||
|
|
||||||
|
</span>
|
||||||
|
|
||||||
</span>
|
</span>
|
||||||
|
</div>
|
||||||
</span>
|
<!--v-if-->
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
class="trailing-content"
|
||||||
|
>
|
||||||
|
<!--v-if-->
|
||||||
|
<!--v-if-->
|
||||||
|
<!--v-if-->
|
||||||
</div>
|
</div>
|
||||||
<!--v-if-->
|
|
||||||
<!--v-if-->
|
|
||||||
<!--v-if-->
|
|
||||||
</label>
|
</label>
|
||||||
|
|
||||||
<div
|
<div
|
||||||
|
@ -182,26 +200,35 @@ exports[`FormBox > should render the component 1`] = `
|
||||||
for="password"
|
for="password"
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
class="title"
|
class="main-content"
|
||||||
>
|
>
|
||||||
<span
|
<div
|
||||||
class="n8n-text size-medium bold"
|
class="title"
|
||||||
>
|
>
|
||||||
|
|
||||||
Password
|
|
||||||
<span
|
<span
|
||||||
class="n8n-text primary size-medium bold"
|
class="n8n-text size-medium bold"
|
||||||
>
|
>
|
||||||
|
|
||||||
*
|
Password
|
||||||
|
<span
|
||||||
|
class="n8n-text primary size-medium bold"
|
||||||
|
>
|
||||||
|
|
||||||
|
*
|
||||||
|
|
||||||
|
</span>
|
||||||
|
|
||||||
</span>
|
</span>
|
||||||
|
</div>
|
||||||
</span>
|
<!--v-if-->
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
class="trailing-content"
|
||||||
|
>
|
||||||
|
<!--v-if-->
|
||||||
|
<!--v-if-->
|
||||||
|
<!--v-if-->
|
||||||
</div>
|
</div>
|
||||||
<!--v-if-->
|
|
||||||
<!--v-if-->
|
|
||||||
<!--v-if-->
|
|
||||||
</label>
|
</label>
|
||||||
|
|
||||||
<div
|
<div
|
||||||
|
|
|
@ -53,41 +53,52 @@ const addTargetBlank = (html: string) =>
|
||||||
[$style.overflow]: !!$slots.options,
|
[$style.overflow]: !!$slots.options,
|
||||||
}"
|
}"
|
||||||
>
|
>
|
||||||
<div v-if="label" :class="$style.title">
|
<div :class="$style['main-content']">
|
||||||
<N8nText
|
<div v-if="label" :class="$style.title">
|
||||||
:bold="bold"
|
<N8nText
|
||||||
:size="size"
|
:bold="bold"
|
||||||
:compact="compact"
|
:size="size"
|
||||||
:color="color"
|
:compact="compact"
|
||||||
:class="{
|
:color="color"
|
||||||
[$style.textEllipses]: showOptions,
|
:class="{
|
||||||
}"
|
[$style.textEllipses]: showOptions,
|
||||||
|
}"
|
||||||
|
>
|
||||||
|
{{ label }}
|
||||||
|
<N8nText v-if="required" color="primary" :bold="bold" :size="size">*</N8nText>
|
||||||
|
</N8nText>
|
||||||
|
</div>
|
||||||
|
<span
|
||||||
|
v-if="tooltipText && label"
|
||||||
|
:class="[$style.infoIcon, showTooltip ? $style.visible : $style.hidden]"
|
||||||
>
|
>
|
||||||
{{ label }}
|
<N8nTooltip placement="top" :popper-class="$style.tooltipPopper" :show-after="300">
|
||||||
<N8nText v-if="required" color="primary" :bold="bold" :size="size">*</N8nText>
|
<N8nIcon icon="question-circle" size="small" />
|
||||||
</N8nText>
|
<template #content>
|
||||||
|
<div v-n8n-html="addTargetBlank(tooltipText)" />
|
||||||
|
</template>
|
||||||
|
</N8nTooltip>
|
||||||
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<span
|
<div :class="$style['trailing-content']">
|
||||||
v-if="tooltipText && label"
|
<div
|
||||||
:class="[$style.infoIcon, showTooltip ? $style.visible : $style.hidden]"
|
v-if="$slots.options && label"
|
||||||
>
|
:class="{ [$style.overlay]: true, [$style.visible]: showOptions }"
|
||||||
<N8nTooltip placement="top" :popper-class="$style.tooltipPopper" :show-after="300">
|
/>
|
||||||
<N8nIcon icon="question-circle" size="small" />
|
<div
|
||||||
<template #content>
|
v-if="$slots.options"
|
||||||
<div v-n8n-html="addTargetBlank(tooltipText)" />
|
:class="{ [$style.options]: true, [$style.visible]: showOptions }"
|
||||||
</template>
|
:data-test-id="`${inputName}-parameter-input-options-container`"
|
||||||
</N8nTooltip>
|
>
|
||||||
</span>
|
<slot name="options" />
|
||||||
<div
|
</div>
|
||||||
v-if="$slots.options && label"
|
<div
|
||||||
:class="{ [$style.overlay]: true, [$style.visible]: showOptions }"
|
v-if="$slots.issues"
|
||||||
/>
|
:class="$style.issues"
|
||||||
<div
|
:data-test-id="`${inputName}-parameter-input-issues-container`"
|
||||||
v-if="$slots.options"
|
>
|
||||||
:class="{ [$style.options]: true, [$style.visible]: showOptions }"
|
<slot name="issues" />
|
||||||
:data-test-id="`${inputName}-parameter-input-options-container`"
|
</div>
|
||||||
>
|
|
||||||
<slot name="options" />
|
|
||||||
</div>
|
</div>
|
||||||
</label>
|
</label>
|
||||||
<slot />
|
<slot />
|
||||||
|
@ -98,20 +109,40 @@ const addTargetBlank = (html: string) =>
|
||||||
.container {
|
.container {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
|
|
||||||
|
label {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.main-content {
|
||||||
|
display: flex;
|
||||||
|
&:hover {
|
||||||
|
.infoIcon {
|
||||||
|
opacity: 1;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
color: var(--color-text-base);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.trailing-content {
|
||||||
|
display: flex;
|
||||||
|
gap: var(--spacing-3xs);
|
||||||
|
|
||||||
|
* {
|
||||||
|
align-self: center;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
.inputLabel {
|
.inputLabel {
|
||||||
display: block;
|
display: block;
|
||||||
}
|
}
|
||||||
.container:hover,
|
.container:hover,
|
||||||
.inputLabel:hover {
|
.inputLabel:hover {
|
||||||
.infoIcon {
|
|
||||||
opacity: 1;
|
|
||||||
|
|
||||||
&:hover {
|
|
||||||
color: var(--color-text-base);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
.options {
|
.options {
|
||||||
opacity: 1;
|
opacity: 1;
|
||||||
transition: opacity 100ms ease-in; // transition on hover in
|
transition: opacity 100ms ease-in; // transition on hover in
|
||||||
|
@ -150,10 +181,13 @@ const addTargetBlank = (html: string) =>
|
||||||
.options {
|
.options {
|
||||||
opacity: 0;
|
opacity: 0;
|
||||||
transition: opacity 250ms cubic-bezier(0.98, -0.06, 0.49, -0.2); // transition on hover out
|
transition: opacity 250ms cubic-bezier(0.98, -0.06, 0.49, -0.2); // transition on hover out
|
||||||
|
display: flex;
|
||||||
|
align-self: center;
|
||||||
|
}
|
||||||
|
|
||||||
> * {
|
.issues {
|
||||||
float: right;
|
display: flex;
|
||||||
}
|
align-self: center;
|
||||||
}
|
}
|
||||||
|
|
||||||
.overlay {
|
.overlay {
|
||||||
|
|
|
@ -10,20 +10,29 @@ exports[`component > Text overflow behavior > displays ellipsis with options 1`]
|
||||||
class="n8n-input-label inputLabel heading medium"
|
class="n8n-input-label inputLabel heading medium"
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
class="title"
|
class="main-content"
|
||||||
>
|
>
|
||||||
<span
|
<div
|
||||||
class="n8n-text size-medium bold textEllipses textEllipses"
|
class="title"
|
||||||
>
|
>
|
||||||
|
<span
|
||||||
a label
|
class="n8n-text size-medium bold textEllipses textEllipses"
|
||||||
<!--v-if-->
|
>
|
||||||
|
|
||||||
</span>
|
a label
|
||||||
|
<!--v-if-->
|
||||||
|
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<!--v-if-->
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
class="trailing-content"
|
||||||
|
>
|
||||||
|
<!--v-if-->
|
||||||
|
<!--v-if-->
|
||||||
|
<!--v-if-->
|
||||||
</div>
|
</div>
|
||||||
<!--v-if-->
|
|
||||||
<!--v-if-->
|
|
||||||
<!--v-if-->
|
|
||||||
</label>
|
</label>
|
||||||
|
|
||||||
|
|
||||||
|
@ -41,20 +50,29 @@ exports[`component > Text overflow behavior > displays full text without options
|
||||||
class="n8n-input-label inputLabel heading medium"
|
class="n8n-input-label inputLabel heading medium"
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
class="title"
|
class="main-content"
|
||||||
>
|
>
|
||||||
<span
|
<div
|
||||||
class="n8n-text size-medium bold"
|
class="title"
|
||||||
>
|
>
|
||||||
|
<span
|
||||||
a label
|
class="n8n-text size-medium bold"
|
||||||
<!--v-if-->
|
>
|
||||||
|
|
||||||
</span>
|
a label
|
||||||
|
<!--v-if-->
|
||||||
|
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<!--v-if-->
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
class="trailing-content"
|
||||||
|
>
|
||||||
|
<!--v-if-->
|
||||||
|
<!--v-if-->
|
||||||
|
<!--v-if-->
|
||||||
</div>
|
</div>
|
||||||
<!--v-if-->
|
|
||||||
<!--v-if-->
|
|
||||||
<!--v-if-->
|
|
||||||
</label>
|
</label>
|
||||||
|
|
||||||
|
|
||||||
|
|
|
@ -0,0 +1,70 @@
|
||||||
|
import type { INodeUi } from '@/Interface';
|
||||||
|
import type { INodeParameters, INodeProperties } from 'n8n-workflow';
|
||||||
|
|
||||||
|
export const TEST_PARAMETERS: INodeProperties[] = [
|
||||||
|
{
|
||||||
|
displayName: 'Test Fixed Collection',
|
||||||
|
name: 'fixedCollectionTest',
|
||||||
|
placeholder: 'Test',
|
||||||
|
type: 'fixedCollection',
|
||||||
|
description:
|
||||||
|
'Test fixed collection description. This is a long description that should be wrapped.',
|
||||||
|
typeOptions: { multipleValues: true, sortable: true, minRequiredFields: 1 },
|
||||||
|
displayOptions: {
|
||||||
|
show: { '@version': [{ _cnd: { gte: 1.1 } }] },
|
||||||
|
},
|
||||||
|
default: {},
|
||||||
|
options: [
|
||||||
|
{
|
||||||
|
name: 'values',
|
||||||
|
displayName: 'Values',
|
||||||
|
values: [
|
||||||
|
{
|
||||||
|
displayName: 'Name',
|
||||||
|
name: 'name',
|
||||||
|
type: 'string',
|
||||||
|
default: '',
|
||||||
|
placeholder: 'e.g. fieldName',
|
||||||
|
description: 'A name of the field in the collection',
|
||||||
|
required: true,
|
||||||
|
noDataExpression: true,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
export const FIXED_COLLECTION_PARAMETERS: INodeProperties[] = TEST_PARAMETERS.filter(
|
||||||
|
(p) => p.type === 'fixedCollection',
|
||||||
|
);
|
||||||
|
|
||||||
|
export const TEST_NODE_VALUES: INodeParameters = {
|
||||||
|
color: '#ff0000',
|
||||||
|
alwaysOutputData: false,
|
||||||
|
executeOnce: false,
|
||||||
|
notesInFlow: false,
|
||||||
|
onError: 'stopWorkflow',
|
||||||
|
retryOnFail: false,
|
||||||
|
maxTries: 3,
|
||||||
|
waitBetweenTries: 1000,
|
||||||
|
notes: '',
|
||||||
|
parameters: { fixedCollectionTest: {} },
|
||||||
|
};
|
||||||
|
|
||||||
|
export const TEST_NODE_NO_ISSUES: INodeUi = {
|
||||||
|
id: 'test-123',
|
||||||
|
parameters: { fixedCollectionTest: { values: [{ name: 'firstName' }] } },
|
||||||
|
typeVersion: 1.1,
|
||||||
|
name: 'Test Node',
|
||||||
|
type: 'n8n-nodes-base.executeWorkflowTrigger',
|
||||||
|
position: [260, 340],
|
||||||
|
};
|
||||||
|
|
||||||
|
export const TEST_ISSUE = 'At least 1 field is required.';
|
||||||
|
|
||||||
|
export const TEST_NODE_WITH_ISSUES: INodeUi = {
|
||||||
|
...TEST_NODE_NO_ISSUES,
|
||||||
|
parameters: { fixedCollectionTest: {} },
|
||||||
|
issues: { parameters: { fixedCollectionTest: [TEST_ISSUE] } },
|
||||||
|
};
|
101
packages/editor-ui/src/components/ParameterInputList.test.ts
Normal file
101
packages/editor-ui/src/components/ParameterInputList.test.ts
Normal file
|
@ -0,0 +1,101 @@
|
||||||
|
import { createComponentRenderer } from '@/__tests__/render';
|
||||||
|
import ParameterInputList from './ParameterInputList.vue';
|
||||||
|
import { createTestingPinia } from '@pinia/testing';
|
||||||
|
import { mockedStore } from '@/__tests__/utils';
|
||||||
|
import { useNDVStore } from '@/stores/ndv.store';
|
||||||
|
import {
|
||||||
|
TEST_NODE_NO_ISSUES,
|
||||||
|
TEST_PARAMETERS,
|
||||||
|
TEST_NODE_VALUES,
|
||||||
|
TEST_NODE_WITH_ISSUES,
|
||||||
|
FIXED_COLLECTION_PARAMETERS,
|
||||||
|
TEST_ISSUE,
|
||||||
|
} from './ParameterInputList.test.constants';
|
||||||
|
|
||||||
|
vi.mock('vue-router', async () => {
|
||||||
|
const actual = await vi.importActual('vue-router');
|
||||||
|
const params = {};
|
||||||
|
const location = {};
|
||||||
|
return {
|
||||||
|
...actual,
|
||||||
|
useRouter: () => ({
|
||||||
|
push: vi.fn(),
|
||||||
|
}),
|
||||||
|
useRoute: () => ({
|
||||||
|
params,
|
||||||
|
location,
|
||||||
|
}),
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
let ndvStore: ReturnType<typeof mockedStore<typeof useNDVStore>>;
|
||||||
|
|
||||||
|
const renderComponent = createComponentRenderer(ParameterInputList, {
|
||||||
|
props: {
|
||||||
|
hideDelete: true,
|
||||||
|
indent: true,
|
||||||
|
isReadOnly: false,
|
||||||
|
},
|
||||||
|
global: {
|
||||||
|
stubs: {
|
||||||
|
ParameterInputFull: { template: '<div data-test-id="parameter-input"></div>' },
|
||||||
|
Suspense: { template: '<div data-test-id="suspense-stub"><slot></slot></div>' },
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('ParameterInputList', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
createTestingPinia();
|
||||||
|
ndvStore = mockedStore(useNDVStore);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('renders', () => {
|
||||||
|
ndvStore.activeNode = TEST_NODE_NO_ISSUES;
|
||||||
|
expect(() =>
|
||||||
|
renderComponent({
|
||||||
|
props: {
|
||||||
|
parameters: TEST_PARAMETERS,
|
||||||
|
nodeValues: TEST_NODE_VALUES,
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
).not.toThrow();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('renders fixed collection inputs correctly', () => {
|
||||||
|
ndvStore.activeNode = TEST_NODE_NO_ISSUES;
|
||||||
|
const { getAllByTestId, getByText } = renderComponent({
|
||||||
|
props: {
|
||||||
|
parameters: TEST_PARAMETERS,
|
||||||
|
nodeValues: TEST_NODE_VALUES,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
// Should render labels for all parameters
|
||||||
|
TEST_PARAMETERS.forEach((parameter) => {
|
||||||
|
expect(getByText(parameter.displayName)).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
// Should render input placeholders for all fixed collection parameters
|
||||||
|
expect(getAllByTestId('suspense-stub')).toHaveLength(FIXED_COLLECTION_PARAMETERS.length);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('renders fixed collection inputs correctly with issues', () => {
|
||||||
|
ndvStore.activeNode = TEST_NODE_WITH_ISSUES;
|
||||||
|
const { getByText, getByTestId } = renderComponent({
|
||||||
|
props: {
|
||||||
|
parameters: TEST_PARAMETERS,
|
||||||
|
nodeValues: TEST_NODE_VALUES,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
// Should render labels for all parameters
|
||||||
|
TEST_PARAMETERS.forEach((parameter) => {
|
||||||
|
expect(getByText(parameter.displayName)).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
// Should render error message for fixed collection parameter
|
||||||
|
expect(
|
||||||
|
getByTestId(`${FIXED_COLLECTION_PARAMETERS[0].name}-parameter-input-issues-container`),
|
||||||
|
).toBeInTheDocument();
|
||||||
|
expect(getByText(TEST_ISSUE)).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
});
|
|
@ -5,7 +5,7 @@ import type {
|
||||||
NodeParameterValue,
|
NodeParameterValue,
|
||||||
NodeParameterValueType,
|
NodeParameterValueType,
|
||||||
} from 'n8n-workflow';
|
} from 'n8n-workflow';
|
||||||
import { deepCopy, ADD_FORM_NOTICE } from 'n8n-workflow';
|
import { deepCopy, ADD_FORM_NOTICE, NodeHelpers } from 'n8n-workflow';
|
||||||
import { computed, defineAsyncComponent, onErrorCaptured, ref, watch } from 'vue';
|
import { computed, defineAsyncComponent, onErrorCaptured, ref, watch } from 'vue';
|
||||||
|
|
||||||
import type { IUpdateInformation } from '@/Interface';
|
import type { IUpdateInformation } from '@/Interface';
|
||||||
|
@ -45,6 +45,9 @@ const LazyCollectionParameter = defineAsyncComponent(
|
||||||
async () => await import('./CollectionParameter.vue'),
|
async () => await import('./CollectionParameter.vue'),
|
||||||
);
|
);
|
||||||
|
|
||||||
|
// Parameter issues are displayed within the inputs themselves, but some parameters need to show them in the label UI
|
||||||
|
const showIssuesInLabelFor = ['fixedCollection'];
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
nodeValues: INodeParameters;
|
nodeValues: INodeParameters;
|
||||||
parameters: INodeProperties[];
|
parameters: INodeProperties[];
|
||||||
|
@ -432,6 +435,15 @@ function onNoticeAction(action: string) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function getParameterIssues(parameter: INodeProperties): string[] {
|
||||||
|
if (!node.value || !showIssuesInLabelFor.includes(parameter.type)) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
const issues = NodeHelpers.getParameterIssues(parameter, node.value.parameters, '', node.value);
|
||||||
|
|
||||||
|
return issues.parameters?.[parameter.name] ?? [];
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Handles default node button parameter type actions
|
* Handles default node button parameter type actions
|
||||||
* @param parameter
|
* @param parameter
|
||||||
|
@ -536,8 +548,26 @@ function getParameterValue<T extends NodeParameterValueType = NodeParameterValue
|
||||||
:tooltip-text="i18n.nodeText().inputLabelDescription(parameter, path)"
|
:tooltip-text="i18n.nodeText().inputLabelDescription(parameter, path)"
|
||||||
size="small"
|
size="small"
|
||||||
:underline="true"
|
:underline="true"
|
||||||
|
:input-name="parameter.name"
|
||||||
color="text-dark"
|
color="text-dark"
|
||||||
/>
|
>
|
||||||
|
<template
|
||||||
|
v-if="
|
||||||
|
showIssuesInLabelFor.includes(parameter.type) &&
|
||||||
|
getParameterIssues(parameter).length > 0
|
||||||
|
"
|
||||||
|
#issues
|
||||||
|
>
|
||||||
|
<N8nTooltip>
|
||||||
|
<template #content>
|
||||||
|
<span v-for="(issue, i) in getParameterIssues(parameter)" :key="i">{{
|
||||||
|
issue
|
||||||
|
}}</span>
|
||||||
|
</template>
|
||||||
|
<N8nIcon icon="exclamation-triangle" size="small" color="danger" />
|
||||||
|
</N8nTooltip>
|
||||||
|
</template>
|
||||||
|
</N8nInputLabel>
|
||||||
<Suspense v-if="!asyncLoadingError">
|
<Suspense v-if="!asyncLoadingError">
|
||||||
<template #default>
|
<template #default>
|
||||||
<LazyCollectionParameter
|
<LazyCollectionParameter
|
||||||
|
|
|
@ -310,15 +310,6 @@ defineExpose({
|
||||||
color="text-dark"
|
color="text-dark"
|
||||||
>
|
>
|
||||||
<template #options>
|
<template #options>
|
||||||
<ParameterOptions
|
|
||||||
:parameter="parameter"
|
|
||||||
:custom-actions="parameterActions"
|
|
||||||
:loading="props.refreshInProgress"
|
|
||||||
:loading-message="fetchingFieldsLabel"
|
|
||||||
:is-read-only="isReadOnly"
|
|
||||||
:value="props.paramValue"
|
|
||||||
@update:model-value="onParameterActionSelected"
|
|
||||||
/>
|
|
||||||
<div v-if="props.isDataStale && !props.refreshInProgress" :class="$style.staleDataWarning">
|
<div v-if="props.isDataStale && !props.refreshInProgress" :class="$style.staleDataWarning">
|
||||||
<N8nTooltip>
|
<N8nTooltip>
|
||||||
<template #content>
|
<template #content>
|
||||||
|
@ -340,6 +331,15 @@ defineExpose({
|
||||||
@click="onParameterActionSelected('refreshFieldList')"
|
@click="onParameterActionSelected('refreshFieldList')"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
<ParameterOptions
|
||||||
|
:parameter="parameter"
|
||||||
|
:custom-actions="parameterActions"
|
||||||
|
:loading="props.refreshInProgress"
|
||||||
|
:loading-message="fetchingFieldsLabel"
|
||||||
|
:is-read-only="isReadOnly"
|
||||||
|
:value="props.paramValue"
|
||||||
|
@update:model-value="onParameterActionSelected"
|
||||||
|
/>
|
||||||
</template>
|
</template>
|
||||||
</N8nInputLabel>
|
</N8nInputLabel>
|
||||||
<div v-if="orderedFields.length === 0" class="mt-3xs mb-xs">
|
<div v-if="orderedFields.length === 0" class="mt-3xs mb-xs">
|
||||||
|
|
|
@ -3,10 +3,15 @@
|
||||||
exports[`MultipleParameter > should render correctly 1`] = `
|
exports[`MultipleParameter > should render correctly 1`] = `
|
||||||
"<div data-v-a47e4507="" class="duplicate-parameter">
|
"<div data-v-a47e4507="" class="duplicate-parameter">
|
||||||
<div data-v-a47e4507="" class="container" data-test-id="input-label"><label class="n8n-input-label inputLabel heading underline small">
|
<div data-v-a47e4507="" class="container" data-test-id="input-label"><label class="n8n-input-label inputLabel heading underline small">
|
||||||
<div class="title"><span class="n8n-text text-dark size-small bold">Additional Fields <!--v-if--></span></div>
|
<div class="main-content">
|
||||||
<!--v-if-->
|
<div class="title"><span class="n8n-text text-dark size-small bold">Additional Fields <!--v-if--></span></div>
|
||||||
<!--v-if-->
|
<!--v-if-->
|
||||||
<!--v-if-->
|
</div>
|
||||||
|
<div class="trailing-content">
|
||||||
|
<!--v-if-->
|
||||||
|
<!--v-if-->
|
||||||
|
<!--v-if-->
|
||||||
|
</div>
|
||||||
</label></div>
|
</label></div>
|
||||||
<div data-v-a47e4507="" class="add-item-wrapper">
|
<div data-v-a47e4507="" class="add-item-wrapper">
|
||||||
<div data-v-a47e4507="" class="no-items-exist"><span data-v-a47e4507="" class="n8n-text size-small regular">Currently no items exist</span></div><button data-v-a47e4507="" class="button button tertiary medium block" aria-live="polite">
|
<div data-v-a47e4507="" class="no-items-exist"><span data-v-a47e4507="" class="n8n-text size-small regular">Currently no items exist</span></div><button data-v-a47e4507="" class="button button tertiary medium block" aria-live="polite">
|
||||||
|
|
Loading…
Reference in a new issue