fix(web): step run of file uploader

This commit is contained in:
JzoNg 2026-04-24 10:56:19 +08:00
parent 46747993d4
commit 60f577fd11
2 changed files with 113 additions and 9 deletions

View File

@ -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',
},
)

View File

@ -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<string, string>) => void
}
const getProcessedHumanInputFormInputs = (
formInputs: HumanInputNodeType['inputs'],
values: Record<string, HumanInputFieldValue> | undefined,
) => {
if (!values)
return undefined
const processedInputs: Record<string, unknown> = { ...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<string, HumanInputFieldValue> | undefined
form_inputs: Record<string, HumanInputFieldValue> | 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<string, string>) => {
setShowGeneratedForm(true)