mirror of https://github.com/langgenius/dify.git
feat: add FileUploaderField and TextAreaField components; enhance BaseField to support file inputs
This commit is contained in:
parent
55f4177b01
commit
cf73faf174
|
|
@ -110,7 +110,7 @@ const FileUploaderInAttachment = ({
|
|||
)
|
||||
}
|
||||
|
||||
type FileUploaderInAttachmentWrapperProps = {
|
||||
export type FileUploaderInAttachmentWrapperProps = {
|
||||
value?: FileEntity[]
|
||||
onChange: (files: FileEntity[]) => void
|
||||
fileConfig: FileUpload
|
||||
|
|
|
|||
|
|
@ -0,0 +1,40 @@
|
|||
import React from 'react'
|
||||
import { useFieldContext } from '../..'
|
||||
import type { LabelProps } from '../label'
|
||||
import Label from '../label'
|
||||
import cn from '@/utils/classnames'
|
||||
import type { FileUploaderInAttachmentWrapperProps } from '../../../file-uploader/file-uploader-in-attachment'
|
||||
import FileUploaderInAttachmentWrapper from '../../../file-uploader/file-uploader-in-attachment'
|
||||
import type { FileEntity } from '../../../file-uploader/types'
|
||||
|
||||
type FileUploaderFieldProps = {
|
||||
label: string
|
||||
labelOptions?: Omit<LabelProps, 'htmlFor' | 'label'>
|
||||
className?: string
|
||||
} & Omit<FileUploaderInAttachmentWrapperProps, 'value' | 'onChange'>
|
||||
|
||||
const FileUploaderField = ({
|
||||
label,
|
||||
labelOptions,
|
||||
className,
|
||||
...inputProps
|
||||
}: FileUploaderFieldProps) => {
|
||||
const field = useFieldContext<FileEntity[]>()
|
||||
|
||||
return (
|
||||
<div className={cn('flex flex-col gap-y-0.5', className)}>
|
||||
<Label
|
||||
htmlFor={field.name}
|
||||
label={label}
|
||||
{...(labelOptions ?? {})}
|
||||
/>
|
||||
<FileUploaderInAttachmentWrapper
|
||||
value={field.state.value}
|
||||
onChange={value => field.handleChange(value)}
|
||||
{...inputProps}
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default FileUploaderField
|
||||
|
|
@ -0,0 +1,41 @@
|
|||
import React from 'react'
|
||||
import { useFieldContext } from '../..'
|
||||
import type { LabelProps } from '../label'
|
||||
import Label from '../label'
|
||||
import cn from '@/utils/classnames'
|
||||
import type { TextareaProps } from '../../../textarea'
|
||||
import Textarea from '../../../textarea'
|
||||
|
||||
type TextAreaFieldProps = {
|
||||
label: string
|
||||
labelOptions?: Omit<LabelProps, 'htmlFor' | 'label'>
|
||||
className?: string
|
||||
} & Omit<TextareaProps, 'className' | 'onChange' | 'onBlur' | 'value' | 'id'>
|
||||
|
||||
const TextAreaField = ({
|
||||
label,
|
||||
labelOptions,
|
||||
className,
|
||||
...inputProps
|
||||
}: TextAreaFieldProps) => {
|
||||
const field = useFieldContext<string>()
|
||||
|
||||
return (
|
||||
<div className={cn('flex flex-col gap-y-0.5', className)}>
|
||||
<Label
|
||||
htmlFor={field.name}
|
||||
label={label}
|
||||
{...(labelOptions ?? {})}
|
||||
/>
|
||||
<Textarea
|
||||
id={field.name}
|
||||
value={field.state.value}
|
||||
onChange={e => field.handleChange(e.target.value)}
|
||||
onBlur={field.handleBlur}
|
||||
{...inputProps}
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default TextAreaField
|
||||
|
|
@ -29,6 +29,10 @@ const BaseField = ({
|
|||
required,
|
||||
showOptional,
|
||||
popupProps,
|
||||
allowedFileExtensions,
|
||||
allowedFileTypes,
|
||||
allowedFileUploadMethods,
|
||||
maxLength,
|
||||
} = config
|
||||
|
||||
const isAllConditionsMet = useStore(form.store, (state) => {
|
||||
|
|
@ -63,6 +67,25 @@ const BaseField = ({
|
|||
)
|
||||
}
|
||||
|
||||
if (type === BaseFieldType.paragraph) {
|
||||
return (
|
||||
<form.AppField
|
||||
name={variable}
|
||||
children={field => (
|
||||
<field.TextAreaField
|
||||
label={label}
|
||||
labelOptions={{
|
||||
tooltip,
|
||||
isRequired: required,
|
||||
showOptional,
|
||||
}}
|
||||
placeholder={placeholder}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
if (type === BaseFieldType.numberInput) {
|
||||
return (
|
||||
<form.AppField
|
||||
|
|
@ -117,6 +140,53 @@ const BaseField = ({
|
|||
)
|
||||
}
|
||||
|
||||
if (type === BaseFieldType.file) {
|
||||
return (
|
||||
<form.AppField
|
||||
name={variable}
|
||||
children={field => (
|
||||
<field.FileUploaderField
|
||||
label={label}
|
||||
labelOptions={{
|
||||
tooltip,
|
||||
isRequired: required,
|
||||
showOptional,
|
||||
}}
|
||||
fileConfig={{
|
||||
allowed_file_extensions: allowedFileExtensions,
|
||||
allowed_file_types: allowedFileTypes,
|
||||
allowed_file_upload_methods: allowedFileUploadMethods,
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
if (type === BaseFieldType.fileList) {
|
||||
return (
|
||||
<form.AppField
|
||||
name={variable}
|
||||
children={field => (
|
||||
<field.FileUploaderField
|
||||
label={label}
|
||||
labelOptions={{
|
||||
tooltip,
|
||||
isRequired: required,
|
||||
showOptional,
|
||||
}}
|
||||
fileConfig={{
|
||||
allowed_file_extensions: allowedFileExtensions,
|
||||
allowed_file_types: allowedFileTypes,
|
||||
allowed_file_upload_methods: allowedFileUploadMethods,
|
||||
number_limits: maxLength,
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
return <></>
|
||||
},
|
||||
})
|
||||
|
|
|
|||
|
|
@ -1,11 +1,15 @@
|
|||
import type { TransferMethod } from '@/types/app'
|
||||
import type { FormType } from '../..'
|
||||
import type { Option } from '../../../select/pure'
|
||||
|
||||
export enum BaseFieldType {
|
||||
textInput = 'textInput',
|
||||
paragraph = 'paragraph',
|
||||
numberInput = 'numberInput',
|
||||
checkbox = 'checkbox',
|
||||
select = 'select',
|
||||
file = 'file',
|
||||
fileList = 'fileList',
|
||||
}
|
||||
|
||||
export type ShowCondition = {
|
||||
|
|
@ -28,6 +32,12 @@ export type SelectConfiguration = {
|
|||
}
|
||||
}
|
||||
|
||||
export type FileConfiguration = {
|
||||
allowedFileTypes: string[]
|
||||
allowedFileExtensions: string[]
|
||||
allowedFileUploadMethods: TransferMethod[]
|
||||
}
|
||||
|
||||
export type BaseConfiguration = {
|
||||
label: string
|
||||
variable: string // Variable name
|
||||
|
|
@ -38,7 +48,9 @@ export type BaseConfiguration = {
|
|||
showConditions: ShowCondition[] // Show this field only when all conditions are met
|
||||
type: BaseFieldType
|
||||
tooltip?: string // Tooltip for this field
|
||||
} & NumberConfiguration & Partial<SelectConfiguration>
|
||||
} & NumberConfiguration
|
||||
& Partial<SelectConfiguration>
|
||||
& Partial<FileConfiguration>
|
||||
|
||||
export type BaseFormProps = {
|
||||
initialData?: Record<string, any>
|
||||
|
|
|
|||
|
|
@ -11,6 +11,8 @@ import FileTypesField from './components/field/file-types'
|
|||
import UploadMethodField from './components/field/upload-method'
|
||||
import NumberSliderField from './components/field/number-slider'
|
||||
import VariableOrConstantInputField from './components/field/variable-selector'
|
||||
import TextAreaField from './components/field/text-area'
|
||||
import FileUploaderField from './components/field/file-uploader'
|
||||
|
||||
export const { fieldContext, useFieldContext, formContext, useFormContext }
|
||||
= createFormHookContexts()
|
||||
|
|
@ -18,6 +20,7 @@ export const { fieldContext, useFieldContext, formContext, useFormContext }
|
|||
export const { useAppForm, withForm } = createFormHook({
|
||||
fieldComponents: {
|
||||
TextField,
|
||||
TextAreaField,
|
||||
NumberInputField,
|
||||
CheckboxField,
|
||||
SelectField,
|
||||
|
|
@ -28,6 +31,7 @@ export const { useAppForm, withForm } = createFormHook({
|
|||
UploadMethodField,
|
||||
NumberSliderField,
|
||||
VariableOrConstantInputField,
|
||||
FileUploaderField,
|
||||
},
|
||||
formComponents: {
|
||||
Actions,
|
||||
|
|
|
|||
|
|
@ -108,8 +108,6 @@ const InputFieldDialog = ({
|
|||
{
|
||||
datasourceKeys.map((key) => {
|
||||
const inputFields = inputFieldsMap[key] || []
|
||||
if (!inputFields.length)
|
||||
return null
|
||||
return (
|
||||
<FieldList
|
||||
key={key}
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
import { useCallback, useEffect } from 'react'
|
||||
import { useDataSourceOptions } from '../hooks'
|
||||
import { useDatasourceOptions } from '../hooks'
|
||||
import OptionCard from './option-card'
|
||||
import { File, Watercrawl } from '@/app/components/base/icons/src/public/knowledge'
|
||||
import { Notion } from '@/app/components/base/icons/src/public/common'
|
||||
|
|
@ -9,7 +9,7 @@ import { DataSourceProvider } from '@/models/common'
|
|||
import type { Datasource } from '../types'
|
||||
|
||||
type DataSourceOptionsProps = {
|
||||
dataSourceNodeId: string
|
||||
datasourceNodeId: string
|
||||
onSelect: (option: Datasource) => void
|
||||
}
|
||||
|
||||
|
|
@ -22,17 +22,17 @@ const DATA_SOURCE_ICONS = {
|
|||
}
|
||||
|
||||
const DataSourceOptions = ({
|
||||
dataSourceNodeId,
|
||||
datasourceNodeId,
|
||||
onSelect,
|
||||
}: DataSourceOptionsProps) => {
|
||||
const { dataSources, options } = useDataSourceOptions()
|
||||
const { datasources, options } = useDatasourceOptions()
|
||||
|
||||
const handelSelect = useCallback((value: string) => {
|
||||
const selectedOption = dataSources.find(option => option.nodeId === value)
|
||||
const selectedOption = datasources.find(option => option.nodeId === value)
|
||||
if (!selectedOption)
|
||||
return
|
||||
onSelect(selectedOption)
|
||||
}, [dataSources, onSelect])
|
||||
}, [datasources, onSelect])
|
||||
|
||||
useEffect(() => {
|
||||
if (options.length > 0)
|
||||
|
|
@ -46,7 +46,7 @@ const DataSourceOptions = ({
|
|||
<OptionCard
|
||||
key={option.value}
|
||||
label={option.label}
|
||||
selected={dataSourceNodeId === option.value}
|
||||
selected={datasourceNodeId === option.value}
|
||||
Icon={DATA_SOURCE_ICONS[option.type as keyof typeof DATA_SOURCE_ICONS]}
|
||||
onClick={handelSelect.bind(null, option.value)}
|
||||
/>
|
||||
|
|
|
|||
|
|
@ -23,12 +23,12 @@ export const useTestRunSteps = () => {
|
|||
return steps
|
||||
}
|
||||
|
||||
export const useDataSourceOptions = () => {
|
||||
export const useDatasourceOptions = () => {
|
||||
const { t } = useTranslation()
|
||||
const nodes = useNodes<DataSourceNodeType>()
|
||||
const dataSources: Datasource[] = useMemo(() => {
|
||||
const dataSourceNodes = nodes.filter(node => node.data.type === BlockEnum.DataSource)
|
||||
return dataSourceNodes.map((node) => {
|
||||
const datasources: Datasource[] = useMemo(() => {
|
||||
const datasourceNodes = nodes.filter(node => node.data.type === BlockEnum.DataSource)
|
||||
return datasourceNodes.map((node) => {
|
||||
let type: DataSourceType | DataSourceProvider = DataSourceType.FILE
|
||||
switch (node.data.tool_name) {
|
||||
case 'file_upload':
|
||||
|
|
@ -57,7 +57,7 @@ export const useDataSourceOptions = () => {
|
|||
|
||||
const options = useMemo(() => {
|
||||
const options: DataSourceOption[] = []
|
||||
dataSources.forEach((source) => {
|
||||
datasources.forEach((source) => {
|
||||
if (source.type === DataSourceType.FILE) {
|
||||
options.push({
|
||||
label: t('datasetPipeline.testRun.dataSource.localFiles'),
|
||||
|
|
@ -95,6 +95,6 @@ export const useDataSourceOptions = () => {
|
|||
}
|
||||
})
|
||||
return options
|
||||
}, [dataSources, t])
|
||||
return { dataSources, options }
|
||||
}, [datasources, t])
|
||||
return { datasources, options }
|
||||
}
|
||||
|
|
|
|||
|
|
@ -146,7 +146,7 @@ const TestRunPanel = () => {
|
|||
<>
|
||||
<div className='flex flex-col gap-y-4 px-4 py-2'>
|
||||
<DataSourceOptions
|
||||
dataSourceNodeId={datasource?.nodeId || ''}
|
||||
datasourceNodeId={datasource?.nodeId || ''}
|
||||
onSelect={setDatasource}
|
||||
/>
|
||||
{datasource?.type === DataSourceType.FILE && (
|
||||
|
|
|
|||
Loading…
Reference in New Issue