mirror of
https://github.com/n8n-io/n8n.git
synced 2026-10-12 02:19:43 +00:00
fix(editor): Hide setup parameter issue icons until user interacts with input (#28010)
Co-authored-by: Claude Opus 4.6 (1M context) <[email protected]>
This commit is contained in:
co-authored by
Claude Opus 4.6
parent
094a5b403e
commit
00b0558c2b
+160
@@ -0,0 +1,160 @@
|
||||
import { createComponentRenderer } from '@/__tests__/render';
|
||||
import { createTestNode } from '@/__tests__/mocks';
|
||||
import { mockedStore } from '@/__tests__/utils';
|
||||
import { createTestingPinia } from '@pinia/testing';
|
||||
import { fireEvent } from '@testing-library/vue';
|
||||
import SetupCardBody from './SetupCardBody.vue';
|
||||
import type { NodeSetupState } from '@/features/setupPanel/setupPanel.types';
|
||||
import { useNodeTypesStore } from '@/app/stores/nodeTypes.store';
|
||||
import type { INodeUi } from '@/Interface';
|
||||
import { jsonParse } from 'n8n-workflow';
|
||||
|
||||
vi.mock('@/features/ndv/parameters/components/ParameterInputList.vue', () => ({
|
||||
default: {
|
||||
template: `<div data-test-id="parameter-input-list" :data-hidden-issues="JSON.stringify(hiddenIssuesInputs)">
|
||||
<button data-test-id="change-param1" @click="$emit('valueChanged', { name: 'param1', value: 'v' })">Change param1</button>
|
||||
<button data-test-id="change-param2" @click="$emit('valueChanged', { name: 'param2', value: 'v' })">Change param2</button>
|
||||
<button data-test-id="blur-param1" @click="$emit('parameterBlur', 'param1')">Blur param1</button>
|
||||
<button data-test-id="blur-param2" @click="$emit('parameterBlur', 'param2')">Blur param2</button>
|
||||
</div>`,
|
||||
props: [
|
||||
'parameters',
|
||||
'nodeValues',
|
||||
'node',
|
||||
'hideDelete',
|
||||
'hiddenIssuesInputs',
|
||||
'path',
|
||||
'optionsOverrides',
|
||||
'removeFirstParameterMargin',
|
||||
'removeLastParameterMargin',
|
||||
],
|
||||
emits: ['valueChanged', 'parameterBlur'],
|
||||
},
|
||||
}));
|
||||
|
||||
const { mockUpdateNodeProperties } = vi.hoisted(() => ({
|
||||
mockUpdateNodeProperties: vi.fn(),
|
||||
}));
|
||||
|
||||
vi.mock('@/app/composables/useWorkflowState', () => ({
|
||||
injectWorkflowState: vi.fn(() => ({
|
||||
updateNodeProperties: mockUpdateNodeProperties,
|
||||
})),
|
||||
}));
|
||||
|
||||
const renderComponent = createComponentRenderer(SetupCardBody);
|
||||
|
||||
const NODE_PROPERTIES = [
|
||||
{
|
||||
name: 'param1',
|
||||
displayName: 'Param 1',
|
||||
type: 'string' as const,
|
||||
required: true,
|
||||
default: '',
|
||||
},
|
||||
{
|
||||
name: 'param2',
|
||||
displayName: 'Param 2',
|
||||
type: 'string' as const,
|
||||
required: true,
|
||||
default: '',
|
||||
},
|
||||
];
|
||||
|
||||
const createState = (overrides: Partial<NodeSetupState> = {}): NodeSetupState => ({
|
||||
node: createTestNode({
|
||||
name: 'TestNode',
|
||||
type: 'n8n-nodes-base.openAi',
|
||||
parameters: {},
|
||||
}) as INodeUi,
|
||||
parameterIssues: { param1: ['Required'], param2: ['Required'] },
|
||||
isTrigger: false,
|
||||
isComplete: false,
|
||||
...overrides,
|
||||
});
|
||||
|
||||
function getHiddenIssues(container: Element): string[] {
|
||||
const el = container.querySelector('[data-test-id="parameter-input-list"]');
|
||||
return jsonParse<string[]>(el?.getAttribute('data-hidden-issues') ?? '[]', { fallbackValue: [] });
|
||||
}
|
||||
|
||||
describe('SetupCardBody', () => {
|
||||
let nodeTypesStore: ReturnType<typeof mockedStore<typeof useNodeTypesStore>>;
|
||||
|
||||
beforeEach(() => {
|
||||
mockUpdateNodeProperties.mockClear();
|
||||
createTestingPinia();
|
||||
nodeTypesStore = mockedStore(useNodeTypesStore);
|
||||
nodeTypesStore.getNodeType = vi.fn().mockReturnValue({ properties: NODE_PROPERTIES });
|
||||
});
|
||||
|
||||
describe('hidden issues', () => {
|
||||
it('should hide all parameter issues initially', () => {
|
||||
const { container } = renderComponent({
|
||||
props: { state: createState() },
|
||||
});
|
||||
|
||||
expect(getHiddenIssues(container)).toEqual(expect.arrayContaining(['param1', 'param2']));
|
||||
});
|
||||
|
||||
it('should reveal parameter issues when a value is changed', async () => {
|
||||
const { container, getByTestId } = renderComponent({
|
||||
props: { state: createState() },
|
||||
});
|
||||
|
||||
await fireEvent.click(getByTestId('change-param1'));
|
||||
|
||||
const hidden = getHiddenIssues(container);
|
||||
expect(hidden).not.toContain('param1');
|
||||
expect(hidden).toContain('param2');
|
||||
});
|
||||
|
||||
it('should reveal parameter issues on blur', async () => {
|
||||
const { container, getByTestId } = renderComponent({
|
||||
props: { state: createState() },
|
||||
});
|
||||
|
||||
await fireEvent.click(getByTestId('blur-param2'));
|
||||
|
||||
const hidden = getHiddenIssues(container);
|
||||
expect(hidden).toContain('param1');
|
||||
expect(hidden).not.toContain('param2');
|
||||
});
|
||||
|
||||
it('should keep revealed state when new parameters appear', async () => {
|
||||
const { container, getByTestId, rerender } = renderComponent({
|
||||
props: { state: createState() },
|
||||
});
|
||||
|
||||
// Reveal param1
|
||||
await fireEvent.click(getByTestId('change-param1'));
|
||||
expect(getHiddenIssues(container)).not.toContain('param1');
|
||||
|
||||
// Add a new parameter via updated node type properties
|
||||
nodeTypesStore.getNodeType = vi.fn().mockReturnValue({
|
||||
properties: [
|
||||
...NODE_PROPERTIES,
|
||||
{ name: 'param3', displayName: 'Param 3', type: 'string', required: true, default: '' },
|
||||
],
|
||||
});
|
||||
|
||||
await rerender({
|
||||
state: createState({
|
||||
parameterIssues: {
|
||||
param1: ['Required'],
|
||||
param2: ['Required'],
|
||||
param3: ['Required'],
|
||||
},
|
||||
}),
|
||||
});
|
||||
|
||||
const hidden = getHiddenIssues(container);
|
||||
// param1 was revealed and should stay revealed
|
||||
expect(hidden).not.toContain('param1');
|
||||
// param2 was never interacted with, stays hidden
|
||||
expect(hidden).toContain('param2');
|
||||
// param3 is new and should start hidden
|
||||
expect(hidden).toContain('param3');
|
||||
});
|
||||
});
|
||||
});
|
||||
+28
-1
@@ -1,5 +1,5 @@
|
||||
<script setup lang="ts">
|
||||
import { computed, watch } from 'vue';
|
||||
import { computed, ref, watch } from 'vue';
|
||||
import { useI18n, type BaseTextKey } from '@n8n/i18n';
|
||||
import { N8nLink, N8nText, N8nTooltip } from '@n8n/design-system';
|
||||
import { type INodeProperties, NodeHelpers } from 'n8n-workflow';
|
||||
@@ -117,6 +117,29 @@ const openNdv = () => {
|
||||
ndvStore.setActiveNodeName(props.state.node.name, 'other');
|
||||
};
|
||||
|
||||
// Hide parameter issues until the user interacts with a parameter.
|
||||
const hideIssuesMap = ref(new Map<string, boolean>());
|
||||
|
||||
watch(
|
||||
simpleParameters,
|
||||
(params) => {
|
||||
for (const p of params) {
|
||||
if (!hideIssuesMap.value.has(p.name)) {
|
||||
hideIssuesMap.value.set(p.name, true);
|
||||
}
|
||||
}
|
||||
},
|
||||
{ immediate: true },
|
||||
);
|
||||
|
||||
const hiddenIssuesInputs = computed(() =>
|
||||
[...hideIssuesMap.value.entries()].filter(([, hidden]) => hidden).map(([name]) => name),
|
||||
);
|
||||
|
||||
const revealParameterIssues = (parameterName: string) => {
|
||||
hideIssuesMap.value.set(parameterName, false);
|
||||
};
|
||||
|
||||
const onCredentialSelected = (updateInfo: INodeUpdatePropertiesInformation) => {
|
||||
if (!props.state.credentialType) return;
|
||||
emit('interacted');
|
||||
@@ -145,6 +168,8 @@ const onValueChanged = (parameterData: IUpdateInformation) => {
|
||||
? parameterData.name.replace(/^parameters\./, '')
|
||||
: parameterData.name;
|
||||
|
||||
revealParameterIssues(paramName);
|
||||
|
||||
workflowState.updateNodeProperties({
|
||||
name: props.state.node.name,
|
||||
properties: {
|
||||
@@ -205,9 +230,11 @@ const onValueChanged = (parameterData: IUpdateInformation) => {
|
||||
:remove-last-parameter-margin="true"
|
||||
:node="state.node"
|
||||
:hide-delete="true"
|
||||
:hidden-issues-inputs="hiddenIssuesInputs"
|
||||
:path="isWizard ? 'parameters' : undefined"
|
||||
:options-overrides="{ hideExpressionSelector: true, hideFocusPanelButton: true }"
|
||||
@value-changed="onValueChanged"
|
||||
@parameter-blur="revealParameterIssues"
|
||||
/>
|
||||
|
||||
<N8nLink
|
||||
|
||||
Reference in New Issue
Block a user