diff --git a/web/app/components/app/text-generate/item/index.tsx b/web/app/components/app/text-generate/item/index.tsx index caa2f624c8..e383bbd902 100644 --- a/web/app/components/app/text-generate/item/index.tsx +++ b/web/app/components/app/text-generate/item/index.tsx @@ -1,6 +1,6 @@ 'use client' import type { FC } from 'react' -import type { HumanInputFieldValue } from '@/app/components/base/chat/chat/answer/human-input-content/field-renderer' +import type { HumanInputFormSubmitData } from '@/app/components/base/chat/chat/answer/human-input-content/type' import type { FeedbackType } from '@/app/components/base/chat/chat/type' import type { WorkflowProcess } from '@/app/components/base/chat/types' import type { SiteInfo } from '@/models/share' @@ -179,7 +179,7 @@ const GenerationItem: FC = ({ // eslint-disable-next-line react/set-state-in-effect setCurrentTab(getDefaultGenerationTab(workflowProcessData)) }, [workflowProcessData]) - const handleSubmitHumanInputForm = useCallback(async (formToken: string, formData: { inputs: Record, action: string }) => { + const handleSubmitHumanInputForm = useCallback(async (formToken: string, formData: HumanInputFormSubmitData) => { if (appSourceType === AppSourceType.installedApp) await submitHumanInputFormService(formToken, formData) else diff --git a/web/app/components/app/text-generate/item/workflow-body.tsx b/web/app/components/app/text-generate/item/workflow-body.tsx index d8cde0306e..f194c00092 100644 --- a/web/app/components/app/text-generate/item/workflow-body.tsx +++ b/web/app/components/app/text-generate/item/workflow-body.tsx @@ -1,6 +1,6 @@ 'use client' import type { FC } from 'react' -import type { HumanInputFieldValue } from '@/app/components/base/chat/chat/answer/human-input-content/field-renderer' +import type { HumanInputFormSubmitData } from '@/app/components/base/chat/chat/answer/human-input-content/type' import type { WorkflowProcess } from '@/app/components/base/chat/types' import type { SiteInfo } from '@/models/share' import { cn } from '@langgenius/dify-ui/cn' @@ -18,7 +18,7 @@ type WorkflowBodyProps = { depth: number hideProcessDetail?: boolean isError: boolean - onSubmitHumanInputForm: (formToken: string, formData: { inputs: Record, action: string }) => Promise + onSubmitHumanInputForm: (formToken: string, formData: HumanInputFormSubmitData) => Promise onSwitchTab: (tab: string) => void showResultTabs: boolean siteInfo: SiteInfo | null diff --git a/web/app/components/base/chat/chat/answer/human-input-content/__tests__/human-input-form.spec.tsx b/web/app/components/base/chat/chat/answer/human-input-content/__tests__/human-input-form.spec.tsx index 8feb97eaac..40bb12808f 100644 --- a/web/app/components/base/chat/chat/answer/human-input-content/__tests__/human-input-form.spec.tsx +++ b/web/app/components/base/chat/chat/answer/human-input-content/__tests__/human-input-form.spec.tsx @@ -1,11 +1,10 @@ -import type { FileEntity } from '@/app/components/base/file-uploader/types' import type { FormInputItem } from '@/app/components/workflow/nodes/human-input/types' import type { HumanInputFormData } from '@/types/workflow' import { act, render, screen } from '@testing-library/react' import userEvent from '@testing-library/user-event' import { describe, expect, it, vi } from 'vitest' import { UserActionButtonType } from '@/app/components/workflow/nodes/human-input/types' -import { InputVarType } from '@/app/components/workflow/types' +import { InputVarType, SupportUploadFileTypes } from '@/app/components/workflow/types' import { TransferMethod } from '@/types/app' import HumanInputForm from '../human-input-form' @@ -14,6 +13,36 @@ vi.mock('../content-item', () => ({
{content} + + + +
), })) @@ -93,7 +137,7 @@ describe('HumanInputForm', () => { }) }) - it('should submit non-string field values without coercion', async () => { + it('should submit file field values using the backend payload shape', async () => { const user = userEvent.setup() const mockOnSubmit = vi.fn().mockResolvedValue(undefined) const formDataWithFileList: HumanInputFormData = { @@ -108,9 +152,9 @@ describe('HumanInputForm', () => { { type: InputVarType.multiFiles, output_variable_name: 'field3', - allowed_file_extensions: [], - allowed_file_types: [], - allowed_file_upload_methods: [], + allowed_file_extensions: ['.png'], + allowed_file_types: [SupportUploadFileTypes.image], + allowed_file_upload_methods: [TransferMethod.local_file], max_upload_count: 5, }, ] as FormInputItem[], @@ -127,14 +171,84 @@ describe('HumanInputForm', () => { inputs: { field1: 'new value', field3: [{ - id: 'file-1', - name: 'avatar.png', - size: 128, - type: 'image/png', - progress: 100, - transferMethod: TransferMethod.local_file, - supportFileType: 'image', - } satisfies FileEntity], + type: 'image', + transfer_method: TransferMethod.local_file, + url: '', + upload_file_id: 'upload-file-1', + }], + }, + }) + }) + + it('should disable buttons until select, file, and file list inputs have uploaded values', async () => { + const user = userEvent.setup() + const mockOnSubmit = vi.fn().mockResolvedValue(undefined) + const formDataWithRequiredInteractiveFields: HumanInputFormData = { + ...mockFormData, + form_content: '{{#$output.field2#}} {{#$output.field3#}} {{#$output.field4#}}', + inputs: [ + { + type: InputVarType.select, + output_variable_name: 'field2', + option_source: { + type: 'constant', + value: ['approved'], + selector: [], + }, + }, + { + type: InputVarType.multiFiles, + output_variable_name: 'field3', + allowed_file_extensions: ['.png'], + allowed_file_types: [SupportUploadFileTypes.image], + allowed_file_upload_methods: [TransferMethod.local_file], + max_upload_count: 5, + }, + { + type: InputVarType.singleFile, + output_variable_name: 'field4', + allowed_file_extensions: ['.png'], + allowed_file_types: [SupportUploadFileTypes.image], + allowed_file_upload_methods: [TransferMethod.local_file], + }, + ] as FormInputItem[], + } + + render() + + const submitButton = screen.getByRole('button', { name: 'Submit' }) + expect(submitButton).toBeDisabled() + + await user.click(screen.getAllByTestId('update-select')[0]!) + await user.click(screen.getAllByTestId('update-pending-single-file')[0]!) + await user.click(screen.getAllByTestId('update-input-file')[0]!) + expect(submitButton).toBeDisabled() + + await user.click(screen.getAllByTestId('update-single-file')[0]!) + await user.click(screen.getAllByTestId('update-pending-input-file')[0]!) + expect(submitButton).toBeDisabled() + + await user.click(screen.getAllByTestId('update-input-file')[0]!) + expect(submitButton).toBeEnabled() + + await user.click(submitButton) + + expect(mockOnSubmit).toHaveBeenCalledWith('token_123', { + action: 'action_1', + inputs: { + field2: 'approved', + field3: [{ + type: 'image', + transfer_method: TransferMethod.local_file, + url: '', + upload_file_id: 'upload-file-1', + }], + field4: { + type: 'image', + transfer_method: TransferMethod.local_file, + url: '', + upload_file_id: 'upload-file-2', + }, }, }) }) diff --git a/web/app/components/base/chat/chat/answer/human-input-content/human-input-form.tsx b/web/app/components/base/chat/chat/answer/human-input-content/human-input-form.tsx index 8c98131d77..04bd3b66b9 100644 --- a/web/app/components/base/chat/chat/answer/human-input-content/human-input-form.tsx +++ b/web/app/components/base/chat/chat/answer/human-input-content/human-input-form.tsx @@ -7,7 +7,7 @@ import { Button } from '@langgenius/dify-ui/button' import * as React from 'react' import { useCallback, useState } from 'react' import ContentItem from './content-item' -import { getButtonStyle, initializeInputs, splitByOutputVar } from './utils' +import { getButtonStyle, getProcessedHumanInputFormInputs, hasInvalidSelectOrFileInput, initializeInputs, splitByOutputVar } from './utils' const HumanInputForm = ({ formData, @@ -28,10 +28,15 @@ const HumanInputForm = ({ const submit = async (formToken: string, actionID: string, inputs: Record) => { setIsSubmitting(true) - await onSubmit?.(formToken, { inputs, action: actionID }) + await onSubmit?.(formToken, { + inputs: getProcessedHumanInputFormInputs(formData.inputs, inputs) || {}, + action: actionID, + }) setIsSubmitting(false) } + const isActionDisabled = isSubmitting || hasInvalidSelectOrFileInput(formData.inputs, inputs) + return ( <> {contentList.map((content, index) => ( @@ -47,7 +52,7 @@ const HumanInputForm = ({ {formData.actions.map((action: UserAction) => (