mirror of
https://github.com/langgenius/dify.git
synced 2026-05-10 05:56:31 +08:00
Extract shared human input field renderer
This commit is contained in:
parent
c2fd595a82
commit
8d3ddee7d3
@ -0,0 +1,136 @@
|
||||
import type { FileEntity } from '@/app/components/base/file-uploader/types'
|
||||
import { fireEvent, render, screen } from '@testing-library/react'
|
||||
import userEvent from '@testing-library/user-event'
|
||||
import { InputVarType, SupportUploadFileTypes } from '@/app/components/workflow/types'
|
||||
import { TransferMethod } from '@/types/app'
|
||||
import HumanInputFieldRenderer from '../field-renderer'
|
||||
|
||||
vi.mock('@/app/components/base/textarea', () => ({
|
||||
__esModule: true,
|
||||
default: ({ value, onChange }: { value: string, onChange: (event: { target: { value: string } }) => void }) => (
|
||||
<textarea
|
||||
data-testid="content-item-textarea"
|
||||
value={value}
|
||||
onChange={event => onChange({ target: { value: event.target.value } })}
|
||||
/>
|
||||
),
|
||||
}))
|
||||
|
||||
vi.mock('@langgenius/dify-ui/select', () => ({
|
||||
Select: ({ children, onValueChange }: { children: React.ReactNode, onValueChange: (value: string) => void }) => (
|
||||
<div data-testid="content-item-select-root" onClick={() => onValueChange('alice')}>{children}</div>
|
||||
),
|
||||
SelectTrigger: ({ children }: { children: React.ReactNode }) => <button type="button" data-testid="content-item-select">{children}</button>,
|
||||
SelectContent: ({ children }: { children: React.ReactNode }) => <div>{children}</div>,
|
||||
SelectItem: ({ children }: { children: React.ReactNode }) => <div>{children}</div>,
|
||||
SelectItemText: ({ children }: { children: React.ReactNode }) => <span>{children}</span>,
|
||||
SelectItemIndicator: () => <span>selected</span>,
|
||||
}))
|
||||
|
||||
vi.mock('@/app/components/base/file-uploader', () => ({
|
||||
FileUploaderInAttachmentWrapper: ({ value, onChange, fileConfig }: {
|
||||
value?: FileEntity[]
|
||||
onChange: (files: FileEntity[]) => void
|
||||
fileConfig: { number_limits?: number }
|
||||
}) => (
|
||||
<button
|
||||
type="button"
|
||||
data-testid={`content-item-file-${fileConfig.number_limits ?? 0}`}
|
||||
onClick={() => onChange([{ id: 'file-1', name: 'report.pdf', size: 1, type: 'document', progress: 100, transferMethod: TransferMethod.local_file, supportFileType: 'document' }])}
|
||||
>
|
||||
{(value || []).map(file => file.name).join(',')}
|
||||
</button>
|
||||
),
|
||||
}))
|
||||
|
||||
describe('HumanInputFieldRenderer', () => {
|
||||
it('renders paragraph input and emits string changes', async () => {
|
||||
const onChange = vi.fn()
|
||||
|
||||
render(
|
||||
<HumanInputFieldRenderer
|
||||
field={{
|
||||
type: InputVarType.paragraph,
|
||||
output_variable_name: 'summary',
|
||||
default: { type: 'constant', selector: [], value: '' },
|
||||
}}
|
||||
value="hello"
|
||||
onChange={onChange}
|
||||
/>,
|
||||
)
|
||||
|
||||
fireEvent.change(screen.getByTestId('content-item-textarea'), {
|
||||
target: { value: 'hello world' },
|
||||
})
|
||||
|
||||
expect(onChange).toHaveBeenLastCalledWith('hello world')
|
||||
})
|
||||
|
||||
it('renders select input and emits selected values', async () => {
|
||||
const user = userEvent.setup()
|
||||
const onChange = vi.fn()
|
||||
|
||||
render(
|
||||
<HumanInputFieldRenderer
|
||||
field={{
|
||||
type: InputVarType.select,
|
||||
output_variable_name: 'reviewer',
|
||||
option_source: { type: 'constant', selector: [], value: ['alice', 'bob'] },
|
||||
}}
|
||||
value=""
|
||||
onChange={onChange}
|
||||
/>,
|
||||
)
|
||||
|
||||
await user.click(screen.getByTestId('content-item-select-root'))
|
||||
|
||||
expect(onChange).toHaveBeenCalledWith('alice')
|
||||
})
|
||||
|
||||
it('renders single-file input and emits one file', async () => {
|
||||
const user = userEvent.setup()
|
||||
const onChange = vi.fn()
|
||||
|
||||
render(
|
||||
<HumanInputFieldRenderer
|
||||
field={{
|
||||
type: InputVarType.singleFile,
|
||||
output_variable_name: 'attachment',
|
||||
allowed_file_extensions: ['.pdf'],
|
||||
allowed_file_types: [SupportUploadFileTypes.document],
|
||||
allowed_file_upload_methods: [TransferMethod.local_file],
|
||||
}}
|
||||
value={null}
|
||||
onChange={onChange}
|
||||
/>,
|
||||
)
|
||||
|
||||
await user.click(screen.getByTestId('content-item-file-1'))
|
||||
|
||||
expect(onChange).toHaveBeenCalledWith(expect.objectContaining({ name: 'report.pdf' }))
|
||||
})
|
||||
|
||||
it('renders file-list input and emits file arrays with max count', async () => {
|
||||
const user = userEvent.setup()
|
||||
const onChange = vi.fn()
|
||||
|
||||
render(
|
||||
<HumanInputFieldRenderer
|
||||
field={{
|
||||
type: InputVarType.multiFiles,
|
||||
output_variable_name: 'attachments',
|
||||
allowed_file_extensions: ['.pdf'],
|
||||
allowed_file_types: [SupportUploadFileTypes.document],
|
||||
allowed_file_upload_methods: [TransferMethod.local_file],
|
||||
max_upload_count: 3,
|
||||
}}
|
||||
value={[]}
|
||||
onChange={onChange}
|
||||
/>,
|
||||
)
|
||||
|
||||
await user.click(screen.getByTestId('content-item-file-3'))
|
||||
|
||||
expect(onChange).toHaveBeenCalledWith([expect.objectContaining({ name: 'report.pdf' })])
|
||||
})
|
||||
})
|
||||
@ -0,0 +1,112 @@
|
||||
import type { FileEntity } from '@/app/components/base/file-uploader/types'
|
||||
import type { FormInputItem } from '@/app/components/workflow/nodes/human-input/types'
|
||||
import {
|
||||
Select,
|
||||
SelectContent,
|
||||
SelectItem,
|
||||
SelectItemIndicator,
|
||||
SelectItemText,
|
||||
SelectTrigger,
|
||||
} from '@langgenius/dify-ui/select'
|
||||
import * as React from 'react'
|
||||
import { FileUploaderInAttachmentWrapper } from '@/app/components/base/file-uploader'
|
||||
import Textarea from '@/app/components/base/textarea'
|
||||
import {
|
||||
isFileFormInput,
|
||||
isFileListFormInput,
|
||||
isParagraphFormInput,
|
||||
isSelectFormInput,
|
||||
} from '@/app/components/workflow/nodes/human-input/types'
|
||||
|
||||
export type HumanInputFieldValue = string | FileEntity | FileEntity[] | null
|
||||
|
||||
type Props = {
|
||||
field: FormInputItem
|
||||
value?: HumanInputFieldValue
|
||||
onChange: (value: HumanInputFieldValue) => void
|
||||
}
|
||||
|
||||
const HumanInputFieldRenderer = ({
|
||||
field,
|
||||
value,
|
||||
onChange,
|
||||
}: Props) => {
|
||||
if (isParagraphFormInput(field)) {
|
||||
return (
|
||||
<Textarea
|
||||
className="h-[104px] sm:text-xs"
|
||||
value={typeof value === 'string' ? value : ''}
|
||||
onChange={e => onChange(e.target.value)}
|
||||
data-testid="content-item-textarea"
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
if (isSelectFormInput(field)) {
|
||||
const options = field.option_source.value.map(option => ({
|
||||
name: option,
|
||||
value: option,
|
||||
}))
|
||||
|
||||
return (
|
||||
<Select
|
||||
value={typeof value === 'string' ? value : ''}
|
||||
onValueChange={(nextValue) => {
|
||||
if (nextValue == null)
|
||||
return
|
||||
onChange(nextValue)
|
||||
}}
|
||||
>
|
||||
<SelectTrigger size="large" className="w-full" aria-label={field.output_variable_name}>
|
||||
{typeof value === 'string' ? value : ''}
|
||||
</SelectTrigger>
|
||||
<SelectContent listClassName="max-h-[140px] overflow-y-auto">
|
||||
{options.map(option => (
|
||||
<SelectItem key={option.value} value={option.value}>
|
||||
<SelectItemText>{option.name}</SelectItemText>
|
||||
<SelectItemIndicator />
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
)
|
||||
}
|
||||
|
||||
if (isFileFormInput(field)) {
|
||||
const singleFileValue = value && !Array.isArray(value) && typeof value !== 'string'
|
||||
? [value]
|
||||
: []
|
||||
|
||||
return (
|
||||
<FileUploaderInAttachmentWrapper
|
||||
value={singleFileValue}
|
||||
onChange={files => onChange(files[0] || null)}
|
||||
fileConfig={{
|
||||
allowed_file_types: field.allowed_file_types,
|
||||
allowed_file_extensions: field.allowed_file_extensions,
|
||||
allowed_file_upload_methods: field.allowed_file_upload_methods,
|
||||
number_limits: 1,
|
||||
}}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
if (isFileListFormInput(field)) {
|
||||
return (
|
||||
<FileUploaderInAttachmentWrapper
|
||||
value={Array.isArray(value) ? value : []}
|
||||
onChange={files => onChange(files)}
|
||||
fileConfig={{
|
||||
allowed_file_types: field.allowed_file_types,
|
||||
allowed_file_extensions: field.allowed_file_extensions,
|
||||
allowed_file_upload_methods: field.allowed_file_upload_methods,
|
||||
number_limits: field.max_upload_count || 5,
|
||||
}}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
return null
|
||||
}
|
||||
|
||||
export default React.memo(HumanInputFieldRenderer)
|
||||
Loading…
Reference in New Issue
Block a user