From 60f577fd11fb0de50ff349968570e090e275d962 Mon Sep 17 00:00:00 2001 From: JzoNg Date: Fri, 24 Apr 2026 10:56:19 +0800 Subject: [PATCH] fix(web): step run of file uploader --- .../use-single-run-form-params.spec.ts | 73 ++++++++++++++++++- .../hooks/use-single-run-form-params.ts | 49 +++++++++++-- 2 files changed, 113 insertions(+), 9 deletions(-) diff --git a/web/app/components/workflow/nodes/human-input/hooks/__tests__/use-single-run-form-params.spec.ts b/web/app/components/workflow/nodes/human-input/hooks/__tests__/use-single-run-form-params.spec.ts index ccd7ae83a0..57dd1aea76 100644 --- a/web/app/components/workflow/nodes/human-input/hooks/__tests__/use-single-run-form-params.spec.ts +++ b/web/app/components/workflow/nodes/human-input/hooks/__tests__/use-single-run-form-params.spec.ts @@ -3,7 +3,7 @@ import type { FileEntity } from '@/app/components/base/file-uploader/types' import type { InputVar } from '@/app/components/workflow/types' import type { HumanInputFormData } from '@/types/workflow' import { act, renderHook } from '@testing-library/react' -import { BlockEnum, InputVarType } from '@/app/components/workflow/types' +import { BlockEnum, InputVarType, SupportUploadFileTypes } from '@/app/components/workflow/types' import { AppModeEnum, TransferMethod } from '@/types/app' import useSingleRunFormParams from '../use-single-run-form-params' @@ -123,6 +123,18 @@ describe('human-input/hooks/use-single-run-form-params', () => { progress: 100, transferMethod: TransferMethod.local_file, supportFileType: 'document', + uploadedId: 'upload-file-1', + } + + const remoteFile: FileEntity = { + id: 'file-2', + name: 'reference.pdf', + size: 256, + type: 'application/pdf', + progress: 100, + transferMethod: TransferMethod.remote_url, + supportFileType: 'document', + url: 'https://example.com/reference.pdf', } it('should build a single before-run form, filter output vars, and expose dependent vars', () => { @@ -159,6 +171,37 @@ describe('human-input/hooks/use-single-run-form-params', () => { }) it('should fetch and submit generated forms in workflow mode while keeping required inputs', async () => { + const formDataWithFiles = { + ...mockFormData, + inputs: [ + { + type: InputVarType.paragraph, + output_variable_name: 'answer', + default: { + type: 'constant', + selector: [], + value: '', + }, + }, + { + type: InputVarType.singleFile, + output_variable_name: 'attachment', + allowed_file_extensions: ['.pdf'], + allowed_file_types: [SupportUploadFileTypes.document], + allowed_file_upload_methods: [TransferMethod.local_file, TransferMethod.remote_url], + }, + { + type: InputVarType.multiFiles, + output_variable_name: 'references', + allowed_file_extensions: ['.pdf'], + allowed_file_types: [SupportUploadFileTypes.document], + allowed_file_upload_methods: [TransferMethod.local_file, TransferMethod.remote_url], + max_upload_count: 3, + }, + ], + } satisfies HumanInputFormData + mockFetchHumanInputNodeStepRunForm.mockResolvedValue(formDataWithFiles) + const { result } = renderHook(() => useSingleRunFormParams({ id: 'node-1', payload: currentInputs, @@ -181,11 +224,11 @@ describe('human-input/hooks/use-single-run-form-params', () => { inputs: { topic: 'AI' }, }, ) - expect(result.current.formData).toEqual(mockFormData) + expect(result.current.formData).toEqual(formDataWithFiles) await act(async () => { await result.current.handleSubmitHumanInputForm({ - inputs: { answer: 'approved', attachment: [uploadedFile] }, + inputs: { answer: 'approved', attachment: uploadedFile, references: [uploadedFile, remoteFile] }, form_inputs: { ignored: 'value' }, action: 'approve', }) @@ -195,7 +238,29 @@ describe('human-input/hooks/use-single-run-form-params', () => { '/apps/app-1/workflows/draft/human-input/nodes/node-1/form', { inputs: { topic: 'AI' }, - form_inputs: { answer: 'approved', attachment: [uploadedFile] }, + form_inputs: { + answer: 'approved', + attachment: { + type: 'document', + transfer_method: TransferMethod.local_file, + url: '', + upload_file_id: 'upload-file-1', + }, + references: [ + { + type: 'document', + transfer_method: TransferMethod.local_file, + url: '', + upload_file_id: 'upload-file-1', + }, + { + type: 'document', + transfer_method: TransferMethod.remote_url, + url: 'https://example.com/reference.pdf', + upload_file_id: '', + }, + ], + }, action: 'approve', }, ) diff --git a/web/app/components/workflow/nodes/human-input/hooks/use-single-run-form-params.ts b/web/app/components/workflow/nodes/human-input/hooks/use-single-run-form-params.ts index 7f0ea9b6b9..62024c85e7 100644 --- a/web/app/components/workflow/nodes/human-input/hooks/use-single-run-form-params.ts +++ b/web/app/components/workflow/nodes/human-input/hooks/use-single-run-form-params.ts @@ -1,15 +1,17 @@ import type { HumanInputNodeType } from '../types' import type { HumanInputFieldValue } from '@/app/components/base/chat/chat/answer/human-input-content/field-renderer' +import type { FileEntity } from '@/app/components/base/file-uploader/types' import type { Props as FormProps } from '@/app/components/workflow/nodes/_base/components/before-run-form/form' import type { InputVar } from '@/app/components/workflow/types' import type { HumanInputFormData } from '@/types/workflow' import { useCallback, useMemo, useState } from 'react' import { useTranslation } from 'react-i18next' import { useStore as useAppStore } from '@/app/components/app/store' +import { getProcessedFiles } from '@/app/components/base/file-uploader/utils' import { fetchHumanInputNodeStepRunForm, submitHumanInputNodeStepRunForm } from '@/service/workflow' import { AppModeEnum } from '@/types/app' import useNodeCrud from '../../_base/hooks/use-node-crud' -import { isParagraphFormInput } from '../types' +import { isFileFormInput, isFileListFormInput, isParagraphFormInput } from '../types' import { isOutput } from '../utils' const i18nPrefix = 'nodes.humanInput' @@ -21,6 +23,41 @@ type Params = { getInputVars: (textList: string[]) => InputVar[] setRunInputData: (data: Record) => void } + +const getProcessedHumanInputFormInputs = ( + formInputs: HumanInputNodeType['inputs'], + values: Record | undefined, +) => { + if (!values) + return undefined + + const processedInputs: Record = { ...values } + + formInputs.forEach((input) => { + const value = values[input.output_variable_name] + + if (isFileListFormInput(input)) { + processedInputs[input.output_variable_name] = Array.isArray(value) + ? getProcessedFiles(value) + : [] + return + } + + if (isFileFormInput(input)) { + if (Array.isArray(value)) { + processedInputs[input.output_variable_name] = getProcessedFiles(value)[0] + return + } + + processedInputs[input.output_variable_name] = value && typeof value !== 'string' + ? getProcessedFiles([value as FileEntity])[0] + : undefined + } + }) + + return processedInputs +} + const useSingleRunFormParams = ({ id, payload, @@ -94,17 +131,19 @@ const useSingleRunFormParams = ({ return data }, [fetchURL]) - const handleSubmitHumanInputForm = useCallback(async (formData: { + const handleSubmitHumanInputForm = useCallback(async (submission: { inputs: Record | undefined form_inputs: Record | undefined action: string }) => { + const formInputs = formData?.inputs?.length ? formData.inputs : inputs.inputs + await submitHumanInputNodeStepRunForm(fetchURL, { inputs: requiredInputs, - form_inputs: formData.inputs, - action: formData.action, + form_inputs: getProcessedHumanInputFormInputs(formInputs, submission.inputs), + action: submission.action, }) - }, [fetchURL, requiredInputs]) + }, [fetchURL, formData?.inputs, inputs.inputs, requiredInputs]) const handleShowGeneratedForm = async (formValue: Record) => { setShowGeneratedForm(true)