diff --git a/packages/@n8n/nodes-langchain/nodes/tools/ToolWorkflow/v2/methods/localResourceMapping.ts b/packages/@n8n/nodes-langchain/nodes/tools/ToolWorkflow/v2/methods/localResourceMapping.ts index 598b3250ea..2ca2b534e5 100644 --- a/packages/@n8n/nodes-langchain/nodes/tools/ToolWorkflow/v2/methods/localResourceMapping.ts +++ b/packages/@n8n/nodes-langchain/nodes/tools/ToolWorkflow/v2/methods/localResourceMapping.ts @@ -4,21 +4,14 @@ import type { ILocalLoadOptionsFunctions, ResourceMapperFields } from 'n8n-workf export async function loadSubWorkflowInputs( this: ILocalLoadOptionsFunctions, ): Promise { - const { fields, dataMode, subworkflowInfo } = await loadWorkflowInputMappings.bind(this)(); + const { fields, subworkflowInfo } = await loadWorkflowInputMappings.bind(this)(); let emptyFieldsNotice: string | undefined; if (fields.length === 0) { const subworkflowLink = subworkflowInfo?.id ? `sub-workflow’s trigger` : 'sub-workflow’s trigger'; - switch (dataMode) { - 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; - } + emptyFieldsNotice = `This sub-workflow will not receive any input when called by your AI node. Define your expected input in the ${subworkflowLink}.`; } return { fields, emptyFieldsNotice }; } diff --git a/packages/design-system/src/components/N8nCheckbox/__snapshots__/Checkbox.test.ts.snap b/packages/design-system/src/components/N8nCheckbox/__snapshots__/Checkbox.test.ts.snap index c6dd12ec37..fa1e46c889 100644 --- a/packages/design-system/src/components/N8nCheckbox/__snapshots__/Checkbox.test.ts.snap +++ b/packages/design-system/src/components/N8nCheckbox/__snapshots__/Checkbox.test.ts.snap @@ -35,20 +35,29 @@ exports[`components > N8nCheckbox > should render with both child and label 1`] class="n8n-input-label inputLabel heading medium" >
- - - Checkbox - - - + + + Checkbox + + + +
+ + +
+ + +
- - - @@ -126,20 +135,29 @@ exports[`components > N8nCheckbox > should render with label 1`] = ` class="n8n-input-label inputLabel heading medium" >
- - - Checkbox - - - + + + Checkbox + + + +
+ + +
+ + +
- - - diff --git a/packages/design-system/src/components/N8nFormBox/__snapshots__/FormBox.test.ts.snap b/packages/design-system/src/components/N8nFormBox/__snapshots__/FormBox.test.ts.snap index 8138b44b8c..7123cc6c5a 100644 --- a/packages/design-system/src/components/N8nFormBox/__snapshots__/FormBox.test.ts.snap +++ b/packages/design-system/src/components/N8nFormBox/__snapshots__/FormBox.test.ts.snap @@ -38,26 +38,35 @@ exports[`FormBox > should render the component 1`] = ` for="name" >
- - - Name - * + Name + + + * + + - - +
+ + +
+ + +
- - -
should render the component 1`] = ` for="email" >
- - - Email - * + Email + + + * + + - - +
+ +
+
+ + +
- - -
should render the component 1`] = ` for="password" >
- - - Password - * + Password + + + * + + - - +
+ +
+
+ + +
- - -
[$style.overflow]: !!$slots.options, }" > -
- +
+ + {{ label }} + * + +
+ - {{ label }} - * -
+ + + + +
- - - - - - -
-
- +
+
+
+ +
+
+ +
@@ -98,20 +109,40 @@ const addTargetBlank = (html: string) => .container { display: flex; 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 { display: block; } .container:hover, .inputLabel:hover { - .infoIcon { - opacity: 1; - - &:hover { - color: var(--color-text-base); - } - } - .options { opacity: 1; transition: opacity 100ms ease-in; // transition on hover in @@ -150,10 +181,13 @@ const addTargetBlank = (html: string) => .options { opacity: 0; transition: opacity 250ms cubic-bezier(0.98, -0.06, 0.49, -0.2); // transition on hover out + display: flex; + align-self: center; +} - > * { - float: right; - } +.issues { + display: flex; + align-self: center; } .overlay { diff --git a/packages/design-system/src/components/N8nInputLabel/__snapshots__/InputLabel.test.ts.snap b/packages/design-system/src/components/N8nInputLabel/__snapshots__/InputLabel.test.ts.snap index b4b61558a4..5335044bf5 100644 --- a/packages/design-system/src/components/N8nInputLabel/__snapshots__/InputLabel.test.ts.snap +++ b/packages/design-system/src/components/N8nInputLabel/__snapshots__/InputLabel.test.ts.snap @@ -10,20 +10,29 @@ exports[`component > Text overflow behavior > displays ellipsis with options 1`] class="n8n-input-label inputLabel heading medium" >
- - - a label - - - + + + a label + + + +
+ +
+
+ + +
- - - @@ -41,20 +50,29 @@ exports[`component > Text overflow behavior > displays full text without options class="n8n-input-label inputLabel heading medium" >
- - - a label - - - + + + a label + + + +
+ +
+
+ + +
- - - diff --git a/packages/editor-ui/src/components/ParameterInputList.test.constants.ts b/packages/editor-ui/src/components/ParameterInputList.test.constants.ts new file mode 100644 index 0000000000..4e564aa90d --- /dev/null +++ b/packages/editor-ui/src/components/ParameterInputList.test.constants.ts @@ -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] } }, +}; diff --git a/packages/editor-ui/src/components/ParameterInputList.test.ts b/packages/editor-ui/src/components/ParameterInputList.test.ts new file mode 100644 index 0000000000..aa5f6a941f --- /dev/null +++ b/packages/editor-ui/src/components/ParameterInputList.test.ts @@ -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>; + +const renderComponent = createComponentRenderer(ParameterInputList, { + props: { + hideDelete: true, + indent: true, + isReadOnly: false, + }, + global: { + stubs: { + ParameterInputFull: { template: '
' }, + Suspense: { template: '
' }, + }, + }, +}); + +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(); + }); +}); diff --git a/packages/editor-ui/src/components/ParameterInputList.vue b/packages/editor-ui/src/components/ParameterInputList.vue index 11fe4e1f42..ebc1839fea 100644 --- a/packages/editor-ui/src/components/ParameterInputList.vue +++ b/packages/editor-ui/src/components/ParameterInputList.vue @@ -5,7 +5,7 @@ import type { NodeParameterValue, NodeParameterValueType, } 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 type { IUpdateInformation } from '@/Interface'; @@ -45,6 +45,9 @@ const LazyCollectionParameter = defineAsyncComponent( 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 = { nodeValues: INodeParameters; 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 * @param parameter @@ -536,8 +548,26 @@ function getParameterValue + > + +