From b8b0422e73c7331060d7d471a70fd17c8add1391 Mon Sep 17 00:00:00 2001 From: CodingOnStar Date: Wed, 25 Mar 2026 15:47:53 +0800 Subject: [PATCH] fix: enhance form validation for file inputs and improve handling of empty array values in variable modal --- .../before-run-form/__tests__/helpers.spec.ts | 10 +++++ .../components/before-run-form/helpers.ts | 11 +++++- .../__tests__/generic-table.spec.tsx | 39 +++++++++++++++++-- .../use-variable-modal-state.spec.ts | 14 +++++++ .../__tests__/variable-modal.helpers.spec.ts | 12 ++++++ .../__tests__/variable-modal.spec.tsx | 6 ++- .../components/use-variable-modal-state.ts | 7 +++- .../components/variable-modal.helpers.ts | 8 +++- 8 files changed, 98 insertions(+), 9 deletions(-) diff --git a/web/app/components/workflow/nodes/_base/components/before-run-form/__tests__/helpers.spec.ts b/web/app/components/workflow/nodes/_base/components/before-run-form/__tests__/helpers.spec.ts index f4d456b6f6..961a56592a 100644 --- a/web/app/components/workflow/nodes/_base/components/before-run-form/__tests__/helpers.spec.ts +++ b/web/app/components/workflow/nodes/_base/components/before-run-form/__tests__/helpers.spec.ts @@ -57,6 +57,16 @@ describe('before-run-form helpers', () => { values: createValues({ query: '' }), })], [{}], t)).toContain('errorMsg.fieldRequired') + expect(getFormErrorMessage([createForm({ + inputs: [createInput({ variable: 'file', label: 'File', type: InputVarType.singleFile, required: true })], + values: createValues({ file: [] }), + })], [{}], t)).toContain('errorMsg.fieldRequired') + + expect(getFormErrorMessage([createForm({ + inputs: [createInput({ variable: 'files', label: 'Files', type: InputVarType.multiFiles, required: true })], + values: createValues({ files: [] }), + })], [{}], t)).toContain('errorMsg.fieldRequired') + expect(getFormErrorMessage([createForm({ inputs: [createInput({ variable: 'file', label: 'File', type: InputVarType.singleFile })], values: createValues({ file: { transferMethod: TransferMethod.local_file } }), diff --git a/web/app/components/workflow/nodes/_base/components/before-run-form/helpers.ts b/web/app/components/workflow/nodes/_base/components/before-run-form/helpers.ts index 3e5cdf9a74..c0e08f64ec 100644 --- a/web/app/components/workflow/nodes/_base/components/before-run-form/helpers.ts +++ b/web/app/components/workflow/nodes/_base/components/before-run-form/helpers.ts @@ -56,7 +56,16 @@ export const getFormErrorMessage = ( const missingRequired = input.required && input.type !== InputVarType.checkbox && !(input.variable in existVarValuesInForm) - && (value === '' || value === undefined || value === null || (input.type === InputVarType.files && Array.isArray(value) && value.length === 0)) + && ( + value === '' || value === undefined || value === null + || ( + (input.type === InputVarType.files + || input.type === InputVarType.multiFiles + || input.type === InputVarType.singleFile) + && Array.isArray(value) + && value.length === 0 + ) + ) if (!errMsg && missingRequired) { errMsg = t('errorMsg.fieldRequired', { ns: 'workflow', field: typeof input.label === 'object' ? input.label.variable : input.label }) diff --git a/web/app/components/workflow/nodes/trigger-webhook/components/__tests__/generic-table.spec.tsx b/web/app/components/workflow/nodes/trigger-webhook/components/__tests__/generic-table.spec.tsx index 4a4d94d3c3..b49c4dd911 100644 --- a/web/app/components/workflow/nodes/trigger-webhook/components/__tests__/generic-table.spec.tsx +++ b/web/app/components/workflow/nodes/trigger-webhook/components/__tests__/generic-table.spec.tsx @@ -3,6 +3,40 @@ import userEvent from '@testing-library/user-event' import { useState } from 'react' import GenericTable from '../generic-table' +vi.mock('@/app/components/base/select', () => ({ + SimpleSelect: ({ + items, + defaultValue, + onSelect, + disabled, + placeholder, + }: { + items: Array<{ name: string, value: string }> + defaultValue?: string + onSelect: (item: { name: string, value: string }) => void + disabled?: boolean + placeholder?: string + }) => ( + + ), +})) + const columns = [ { key: 'name', @@ -144,12 +178,11 @@ describe('GenericTable', () => { , ) - await user.click(screen.getByRole('button', { name: 'Choose method' })) - await user.click(await screen.findByText('POST')) + await user.selectOptions(screen.getAllByRole('combobox', { name: 'Choose method' })[0], 'post') await waitFor(() => { expect(onChange).toHaveBeenCalledWith([{ method: 'post', preview: '' }]) - expect(screen.getByRole('button', { name: 'POST' })).toBeInTheDocument() + expect(screen.getAllByRole('combobox', { name: 'Choose method' })[0]).toHaveValue('post') }) onChange.mockClear() diff --git a/web/app/components/workflow/panel/chat-variable-panel/components/__tests__/use-variable-modal-state.spec.ts b/web/app/components/workflow/panel/chat-variable-panel/components/__tests__/use-variable-modal-state.spec.ts index 47aeb57ae7..0b9da42961 100644 --- a/web/app/components/workflow/panel/chat-variable-panel/components/__tests__/use-variable-modal-state.spec.ts +++ b/web/app/components/workflow/panel/chat-variable-panel/components/__tests__/use-variable-modal-state.spec.ts @@ -158,6 +158,20 @@ describe('useVariableModalState', () => { expect(result.current.editorContent).toBe(JSON.stringify(['True', 'False'])) }) + it('should preserve zero values when switching number arrays into json mode', () => { + const { result } = renderHook(() => useVariableModalState(createOptions())) + + act(() => { + result.current.handleTypeChange(ChatVarType.ArrayNumber) + result.current.setValue([0, 2, undefined]) + result.current.handleEditorChange(true) + }) + + expect(result.current.editInJSON).toBe(true) + expect(result.current.value).toEqual([0, 2]) + expect(result.current.editorContent).toBe(JSON.stringify([0, 2])) + }) + it('should notify and stop saving when object keys are invalid', () => { const notify = vi.fn() const onSave = vi.fn() diff --git a/web/app/components/workflow/panel/chat-variable-panel/components/__tests__/variable-modal.helpers.spec.ts b/web/app/components/workflow/panel/chat-variable-panel/components/__tests__/variable-modal.helpers.spec.ts index 2caaf9b90d..1c88d3a63b 100644 --- a/web/app/components/workflow/panel/chat-variable-panel/components/__tests__/variable-modal.helpers.spec.ts +++ b/web/app/components/workflow/panel/chat-variable-panel/components/__tests__/variable-modal.helpers.spec.ts @@ -59,6 +59,13 @@ describe('variable-modal helpers', () => { value: ['a', '', 'b'], })).toEqual(['a', 'b']) + expect(formatChatVariableValue({ + editInJSON: false, + objectValue: [], + type: ChatVarType.ArrayNumber, + value: [0, 1, undefined, null, ''] as unknown as Array, + })).toEqual([0, 1]) + expect(formatChatVariableValue({ editInJSON: false, objectValue: [], @@ -99,6 +106,11 @@ describe('variable-modal helpers', () => { type: ChatVarType.ArrayBoolean, })).toEqual([true, false, true, false]) + expect(() => parseEditorContent({ + content: '{"enabled":true}', + type: ChatVarType.ArrayBoolean, + })).toThrow('JSON array') + expect(parseEditorContent({ content: '{"enabled":true}', type: ChatVarType.Object, diff --git a/web/app/components/workflow/panel/chat-variable-panel/components/__tests__/variable-modal.spec.tsx b/web/app/components/workflow/panel/chat-variable-panel/components/__tests__/variable-modal.spec.tsx index e61a6bd085..319e3803f4 100644 --- a/web/app/components/workflow/panel/chat-variable-panel/components/__tests__/variable-modal.spec.tsx +++ b/web/app/components/workflow/panel/chat-variable-panel/components/__tests__/variable-modal.spec.tsx @@ -100,8 +100,10 @@ describe('variable-modal', () => { expect(screen.getByDisplayValue('secret')).toBeInTheDocument() expect(screen.getByDisplayValue('30')).toBeInTheDocument() + const timeoutInput = screen.getByDisplayValue('30') as HTMLInputElement await user.clear(screen.getByDisplayValue('secret')) - await user.type(screen.getByDisplayValue('30'), '5') + await user.clear(timeoutInput) + await user.type(timeoutInput, '5') await user.click(screen.getByText('common.operation.save')) expect(onSave).toHaveBeenCalledWith({ @@ -110,7 +112,7 @@ describe('variable-modal', () => { value_type: ChatVarType.Object, value: { apiKey: null, - timeout: 305, + timeout: 5, }, description: 'settings', }) diff --git a/web/app/components/workflow/panel/chat-variable-panel/components/use-variable-modal-state.ts b/web/app/components/workflow/panel/chat-variable-panel/components/use-variable-modal-state.ts index 648fdc8eaf..07619029a3 100644 --- a/web/app/components/workflow/panel/chat-variable-panel/components/use-variable-modal-state.ts +++ b/web/app/components/workflow/panel/chat-variable-panel/components/use-variable-modal-state.ts @@ -133,8 +133,11 @@ export const useVariableModalState = ({ if (prev.type === ChatVarType.ArrayString || prev.type === ChatVarType.ArrayNumber) { if (nextEditInJSON) { - const nextValue = (Array.isArray(prev.value) && prev.value.length && prev.value.filter(Boolean).length) - ? prev.value.filter(Boolean) + const compactValues = Array.isArray(prev.value) + ? prev.value.filter(item => item !== null && item !== undefined && item !== '') + : [] + const nextValue = compactValues.length + ? compactValues : undefined nextState.value = nextValue if (!prev.editorContent) diff --git a/web/app/components/workflow/panel/chat-variable-panel/components/variable-modal.helpers.ts b/web/app/components/workflow/panel/chat-variable-panel/components/variable-modal.helpers.ts index 07369ed1fe..999dc4e2c9 100644 --- a/web/app/components/workflow/panel/chat-variable-panel/components/variable-modal.helpers.ts +++ b/web/app/components/workflow/panel/chat-variable-panel/components/variable-modal.helpers.ts @@ -88,6 +88,9 @@ export const formatChatVariableValue = ({ type: ChatVarType value: unknown }) => { + const compactArrayValue = (items: unknown[]) => + items.filter(item => item !== null && item !== undefined && item !== '') + switch (type) { case ChatVarTypeEnum.String: return value || '' @@ -100,7 +103,7 @@ export const formatChatVariableValue = ({ case ChatVarTypeEnum.ArrayString: case ChatVarTypeEnum.ArrayNumber: case ChatVarTypeEnum.ArrayObject: - return Array.isArray(value) ? value.filter(Boolean) : [] + return Array.isArray(value) ? compactArrayValue(value) : [] case ChatVarTypeEnum.ArrayBoolean: return value || [] } @@ -151,6 +154,9 @@ export const parseEditorContent = ({ if (type !== ChatVarTypeEnum.ArrayBoolean) return parsed + if (!Array.isArray(parsed)) + throw new TypeError('ArrayBoolean editor content must be a JSON array') + return parsed .map((item: string | boolean) => { if (item === 'True' || item === 'true' || item === true)