mirror of
https://github.com/langgenius/dify.git
synced 2026-05-09 21:28:25 +08:00
fix(web): human input step run preview restriction
This commit is contained in:
parent
60f577fd11
commit
a8e663863d
@ -12,9 +12,41 @@ vi.mock('@/app/components/base/chat/chat/answer/human-input-content/content-item
|
||||
default: ({ content, onInputChange }: { content: string, onInputChange: (name: string, value: unknown) => void }) => (
|
||||
<div data-testid="single-run-content-item">
|
||||
{content}
|
||||
<button type="button" onClick={() => onInputChange('decision', 'approve')}>
|
||||
update-decision
|
||||
</button>
|
||||
<button type="button" onClick={() => onInputChange('summary', 'updated summary')}>
|
||||
update-summary
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => onInputChange('attachment', {
|
||||
id: 'file-0',
|
||||
name: 'main.pdf',
|
||||
size: 64,
|
||||
type: 'document',
|
||||
progress: 100,
|
||||
transferMethod: TransferMethod.local_file,
|
||||
supportFileType: 'document',
|
||||
uploadedId: 'upload-file-0',
|
||||
})}
|
||||
>
|
||||
update-attachment
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => onInputChange('attachment', {
|
||||
id: 'file-0',
|
||||
name: 'main.pdf',
|
||||
size: 64,
|
||||
type: 'document',
|
||||
progress: 50,
|
||||
transferMethod: TransferMethod.local_file,
|
||||
supportFileType: 'document',
|
||||
})}
|
||||
>
|
||||
update-pending-attachment
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => onInputChange('attachments', [{
|
||||
@ -25,10 +57,25 @@ vi.mock('@/app/components/base/chat/chat/answer/human-input-content/content-item
|
||||
progress: 100,
|
||||
transferMethod: TransferMethod.local_file,
|
||||
supportFileType: 'document',
|
||||
uploadedId: 'upload-file-1',
|
||||
}])}
|
||||
>
|
||||
update-attachments
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => onInputChange('attachments', [{
|
||||
id: 'file-1',
|
||||
name: 'review.pdf',
|
||||
size: 128,
|
||||
type: 'document',
|
||||
progress: 50,
|
||||
transferMethod: TransferMethod.local_file,
|
||||
supportFileType: 'document',
|
||||
}])}
|
||||
>
|
||||
update-pending-attachments
|
||||
</button>
|
||||
</div>
|
||||
),
|
||||
}))
|
||||
@ -38,8 +85,17 @@ describe('SingleRunForm', () => {
|
||||
form_id: 'form-1',
|
||||
node_id: 'node-1',
|
||||
node_title: 'Human Input',
|
||||
form_content: '{{#$output.summary#}} {{#$output.attachments#}}',
|
||||
form_content: '{{#$output.decision#}} {{#$output.summary#}} {{#$output.attachment#}} {{#$output.attachments#}}',
|
||||
inputs: [
|
||||
{
|
||||
type: InputVarType.select,
|
||||
output_variable_name: 'decision',
|
||||
option_source: {
|
||||
type: 'constant',
|
||||
value: ['approve', 'reject'],
|
||||
selector: [],
|
||||
},
|
||||
},
|
||||
{
|
||||
type: InputVarType.paragraph,
|
||||
output_variable_name: 'summary',
|
||||
@ -49,6 +105,13 @@ describe('SingleRunForm', () => {
|
||||
selector: [],
|
||||
},
|
||||
},
|
||||
{
|
||||
type: InputVarType.singleFile,
|
||||
output_variable_name: 'attachment',
|
||||
allowed_file_extensions: ['.pdf'],
|
||||
allowed_file_types: [SupportUploadFileTypes.document],
|
||||
allowed_file_upload_methods: [TransferMethod.local_file],
|
||||
},
|
||||
{
|
||||
type: InputVarType.multiFiles,
|
||||
output_variable_name: 'attachments',
|
||||
@ -84,13 +147,26 @@ describe('SingleRunForm', () => {
|
||||
)
|
||||
|
||||
await user.click(screen.getAllByRole('button', { name: 'update-summary' })[0]!)
|
||||
await user.click(screen.getAllByRole('button', { name: 'update-decision' })[0]!)
|
||||
await user.click(screen.getAllByRole('button', { name: 'update-attachment' })[0]!)
|
||||
await user.click(screen.getAllByRole('button', { name: 'update-attachments' })[0]!)
|
||||
await user.click(screen.getByRole('button', { name: 'Approve' }))
|
||||
|
||||
expect(onSubmit).toHaveBeenCalledWith({
|
||||
action: 'approve',
|
||||
inputs: {
|
||||
decision: 'approve',
|
||||
summary: 'updated summary',
|
||||
attachment: {
|
||||
id: 'file-0',
|
||||
name: 'main.pdf',
|
||||
size: 64,
|
||||
type: 'document',
|
||||
progress: 100,
|
||||
transferMethod: TransferMethod.local_file,
|
||||
supportFileType: 'document',
|
||||
uploadedId: 'upload-file-0',
|
||||
} satisfies FileEntity,
|
||||
attachments: [{
|
||||
id: 'file-1',
|
||||
name: 'review.pdf',
|
||||
@ -99,8 +175,65 @@ describe('SingleRunForm', () => {
|
||||
progress: 100,
|
||||
transferMethod: TransferMethod.local_file,
|
||||
supportFileType: 'document',
|
||||
uploadedId: 'upload-file-1',
|
||||
} satisfies FileEntity],
|
||||
},
|
||||
})
|
||||
})
|
||||
|
||||
it('disables action buttons until select, file, and file list fields have values', async () => {
|
||||
const user = userEvent.setup()
|
||||
const onSubmit = vi.fn().mockResolvedValue(undefined)
|
||||
|
||||
render(
|
||||
<SingleRunForm
|
||||
nodeName="Human Input"
|
||||
data={formData}
|
||||
onSubmit={onSubmit}
|
||||
/>,
|
||||
)
|
||||
|
||||
const actionButton = screen.getByRole('button', { name: 'Approve' })
|
||||
expect(actionButton).toBeDisabled()
|
||||
|
||||
await user.click(screen.getAllByRole('button', { name: 'update-decision' })[0]!)
|
||||
expect(actionButton).toBeDisabled()
|
||||
|
||||
await user.click(screen.getAllByRole('button', { name: 'update-attachment' })[0]!)
|
||||
expect(actionButton).toBeDisabled()
|
||||
|
||||
await user.click(screen.getAllByRole('button', { name: 'update-attachments' })[0]!)
|
||||
expect(actionButton).toBeEnabled()
|
||||
|
||||
await user.click(actionButton)
|
||||
|
||||
expect(onSubmit).toHaveBeenCalledTimes(1)
|
||||
})
|
||||
|
||||
it('keeps action buttons disabled while selected files are still uploading', async () => {
|
||||
const user = userEvent.setup()
|
||||
const onSubmit = vi.fn().mockResolvedValue(undefined)
|
||||
|
||||
render(
|
||||
<SingleRunForm
|
||||
nodeName="Human Input"
|
||||
data={formData}
|
||||
onSubmit={onSubmit}
|
||||
/>,
|
||||
)
|
||||
|
||||
const actionButton = screen.getByRole('button', { name: 'Approve' })
|
||||
|
||||
await user.click(screen.getAllByRole('button', { name: 'update-decision' })[0]!)
|
||||
await user.click(screen.getAllByRole('button', { name: 'update-pending-attachment' })[0]!)
|
||||
await user.click(screen.getAllByRole('button', { name: 'update-attachments' })[0]!)
|
||||
expect(actionButton).toBeDisabled()
|
||||
|
||||
await user.click(screen.getAllByRole('button', { name: 'update-attachment' })[0]!)
|
||||
await user.click(screen.getAllByRole('button', { name: 'update-pending-attachments' })[0]!)
|
||||
expect(actionButton).toBeDisabled()
|
||||
|
||||
await user.click(screen.getAllByRole('button', { name: 'update-attachments' })[0]!)
|
||||
expect(actionButton).toBeEnabled()
|
||||
})
|
||||
})
|
||||
|
||||
@ -1,6 +1,7 @@
|
||||
'use client'
|
||||
import type { ButtonProps } from '@langgenius/dify-ui/button'
|
||||
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 { UserAction } from '@/app/components/workflow/nodes/human-input/types'
|
||||
import type { HumanInputFormData } from '@/types/workflow'
|
||||
import { Button } from '@langgenius/dify-ui/button'
|
||||
@ -11,6 +12,8 @@ import { useState } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import ContentItem from '@/app/components/base/chat/chat/answer/human-input-content/content-item'
|
||||
import { getButtonStyle, initializeInputs, splitByOutputVar } from '@/app/components/base/chat/chat/answer/human-input-content/utils'
|
||||
import { fileIsUploaded } from '@/app/components/base/file-uploader/utils'
|
||||
import { isFileFormInput, isFileListFormInput, isSelectFormInput } from '@/app/components/workflow/nodes/human-input/types'
|
||||
|
||||
type Props = {
|
||||
nodeName: string
|
||||
@ -20,6 +23,19 @@ type Props = {
|
||||
onSubmit?: ({ inputs, action }: { inputs: Record<string, HumanInputFieldValue>, action: string }) => Promise<void>
|
||||
}
|
||||
|
||||
const isUploadedFile = (value: HumanInputFieldValue | undefined) => {
|
||||
return !!value
|
||||
&& !Array.isArray(value)
|
||||
&& typeof value !== 'string'
|
||||
&& !!fileIsUploaded(value as FileEntity)
|
||||
}
|
||||
|
||||
const hasUploadedFiles = (value: HumanInputFieldValue | undefined) => {
|
||||
return Array.isArray(value)
|
||||
&& value.length > 0
|
||||
&& value.every(file => !!fileIsUploaded(file))
|
||||
}
|
||||
|
||||
const FormContent = ({
|
||||
nodeName,
|
||||
data,
|
||||
@ -40,6 +56,21 @@ const FormContent = ({
|
||||
}))
|
||||
}
|
||||
|
||||
const hasEmptySelectOrFileInput = data.inputs.some((input) => {
|
||||
const value = inputs[input.output_variable_name]
|
||||
|
||||
if (isSelectFormInput(input))
|
||||
return typeof value !== 'string' || value.length === 0
|
||||
|
||||
if (isFileFormInput(input))
|
||||
return Array.isArray(value) ? !hasUploadedFiles(value) : !isUploadedFile(value)
|
||||
|
||||
if (isFileListFormInput(input))
|
||||
return !hasUploadedFiles(value)
|
||||
|
||||
return false
|
||||
})
|
||||
|
||||
const submit = async (actionID: string) => {
|
||||
setIsSubmitting(true)
|
||||
await onSubmit?.({ inputs, action: actionID })
|
||||
@ -72,7 +103,7 @@ const FormContent = ({
|
||||
{data.actions.map((action: UserAction) => (
|
||||
<Button
|
||||
key={action.id}
|
||||
disabled={isSubmitting}
|
||||
disabled={isSubmitting || hasEmptySelectOrFileInput}
|
||||
variant={getButtonStyle(action.button_style) as ButtonProps['variant']}
|
||||
onClick={() => submit(action.id)}
|
||||
>
|
||||
|
||||
Loading…
Reference in New Issue
Block a user