diff --git a/web/app/components/base/prompt-editor/plugins/hitl-input-block/__tests__/input-field.spec.tsx b/web/app/components/base/prompt-editor/plugins/hitl-input-block/__tests__/input-field.spec.tsx index e97fc19f71..c6905da9f7 100644 --- a/web/app/components/base/prompt-editor/plugins/hitl-input-block/__tests__/input-field.spec.tsx +++ b/web/app/components/base/prompt-editor/plugins/hitl-input-block/__tests__/input-field.spec.tsx @@ -11,6 +11,7 @@ type VarReferencePickerProps = { } let lastVarReferencePickerProps: VarReferencePickerProps | undefined +let fileUploadSettingMaxLength: number | undefined = 4 vi.mock('@/app/components/workflow/nodes/_base/components/variable/var-reference-picker', () => ({ default: (props: VarReferencePickerProps) => { @@ -66,7 +67,7 @@ vi.mock('@/app/components/workflow/nodes/_base/components/file-upload-setting', allowed_file_extensions: ['.pdf'], allowed_file_types: [SupportUploadFileTypes.document], allowed_file_upload_methods: [TransferMethod.local_file], - max_length: 4, + max_length: fileUploadSettingMaxLength, })} > file-upload-setting @@ -89,6 +90,7 @@ describe('InputField', () => { beforeEach(() => { vi.clearAllMocks() lastVarReferencePickerProps = undefined + fileUploadSettingMaxLength = 4 }) it('should keep the header and actions visible while the field content scrolls internally', () => { @@ -627,6 +629,32 @@ describe('InputField', () => { }) }) + it('should normalize empty file-list upload limit on save', async () => { + const user = userEvent.setup() + const onChange = vi.fn() + fileUploadSettingMaxLength = Number.NaN + + render( + , + ) + + await user.click(screen.getByRole('button', { name: 'select-file-list' })) + await user.click(screen.getByRole('button', { name: 'file-upload-setting' })) + await user.click(screen.getByRole('button', { name: /workflow\.nodes\.humanInput\.insertInputField\.insert/i })) + + expect(onChange).toHaveBeenCalledTimes(1) + expect(onChange.mock.calls[0]![0]).toEqual(expect.objectContaining({ + type: InputVarType.multiFiles, + number_limits: 1, + })) + }) + it('should clear paragraph default state when switching to file-list', async () => { const user = userEvent.setup() const onChange = vi.fn() diff --git a/web/app/components/base/prompt-editor/plugins/hitl-input-block/input-field.tsx b/web/app/components/base/prompt-editor/plugins/hitl-input-block/input-field.tsx index e5393ee387..6d7ad432db 100644 --- a/web/app/components/base/prompt-editor/plugins/hitl-input-block/input-field.tsx +++ b/web/app/components/base/prompt-editor/plugins/hitl-input-block/input-field.tsx @@ -97,6 +97,16 @@ const InputField: React.FC = ({ const handleSave = useCallback(() => { if (!nameValid) return + if (isFileListFormInput(tempPayload)) { + const value = tempPayload.number_limits ?? 5 + if (!Number.isFinite(value)) { + onChange({ + ...tempPayload, + number_limits: 1, + }) + return + } + } onChange(tempPayload) }, [nameValid, onChange, tempPayload]) const handleTypeChange = useCallback((item: TypeSelectItem) => { diff --git a/web/app/components/workflow/nodes/_base/components/__tests__/file-support.spec.tsx b/web/app/components/workflow/nodes/_base/components/__tests__/file-support.spec.tsx index 1a3e004c7e..c330e0caed 100644 --- a/web/app/components/workflow/nodes/_base/components/__tests__/file-support.spec.tsx +++ b/web/app/components/workflow/nodes/_base/components/__tests__/file-support.spec.tsx @@ -1,6 +1,7 @@ import type { UploadFileSetting } from '@/app/components/workflow/types' import { fireEvent, render, screen } from '@testing-library/react' import userEvent from '@testing-library/user-event' +import { useState } from 'react' import { useFileSizeLimit } from '@/app/components/base/file-uploader/hooks' import { SupportUploadFileTypes } from '@/app/components/workflow/types' import { useFileUploadConfig } from '@/service/use-common' @@ -120,6 +121,35 @@ describe('File upload support components', () => { })) }) + it('should keep upload limits within the configured range', () => { + const StatefulFileUploadSetting = () => { + const [payload, setPayload] = useState(createPayload()) + + return ( + + ) + } + + render() + + const input = screen.getByRole('spinbutton') + + fireEvent.change(input, { target: { value: '20' } }) + expect(input).toHaveValue(10) + + fireEvent.change(input, { target: { value: '0' } }) + expect(input).toHaveValue(1) + + fireEvent.change(input, { target: { value: '' } }) + expect(input).toHaveValue(null) + fireEvent.blur(input) + expect(input).toHaveValue(1) + }) + it('should toggle built-in and custom file type selections', async () => { const user = userEvent.setup() const onChange = vi.fn() diff --git a/web/app/components/workflow/nodes/_base/components/file-upload-setting.tsx b/web/app/components/workflow/nodes/_base/components/file-upload-setting.tsx index 3a9af99538..77e7de851e 100644 --- a/web/app/components/workflow/nodes/_base/components/file-upload-setting.tsx +++ b/web/app/components/workflow/nodes/_base/components/file-upload-setting.tsx @@ -89,11 +89,14 @@ const FileUploadSetting: FC = ({ }, [onChange, payload]) const handleMaxUploadNumLimitChange = useCallback((value: number) => { + const normalizedValue = Number.isFinite(value) + ? Math.min(Math.max(value, 1), maxFileUploadLimit) + : value const newPayload = produce(payload, (draft) => { - draft.max_length = value + draft.max_length = normalizedValue }) onChange(newPayload) - }, [onChange, payload]) + }, [maxFileUploadLimit, onChange, payload]) return (
@@ -163,6 +166,7 @@ const FileUploadSetting: FC = ({