mirror of
https://github.com/langgenius/dify.git
synced 2025-09-25 17:15:28 +00:00
Refactor input field form components and schema
This commit is contained in:
parent
8367ae85de
commit
fd8ee9f53e
@ -12,14 +12,14 @@ import Option from './option'
|
||||
|
||||
type InputTypeSelectFieldProps = {
|
||||
label: string
|
||||
labeOptions?: Omit<LabelProps, 'htmlFor' | 'label'>
|
||||
labelOptions?: Omit<LabelProps, 'htmlFor' | 'label'>
|
||||
supportFile: boolean
|
||||
className?: string
|
||||
} & Omit<CustomSelectProps<FileTypeSelectOption>, 'options' | 'value' | 'onChange' | 'CustomTrigger' | 'CustomOption'>
|
||||
|
||||
const InputTypeSelectField = ({
|
||||
label,
|
||||
labeOptions,
|
||||
labelOptions,
|
||||
supportFile,
|
||||
className,
|
||||
...customSelectProps
|
||||
@ -39,7 +39,7 @@ const InputTypeSelectField = ({
|
||||
<Label
|
||||
htmlFor={field.name}
|
||||
label={label}
|
||||
{...(labeOptions ?? {})}
|
||||
{...(labelOptions ?? {})}
|
||||
/>
|
||||
<CustomSelect<FileTypeSelectOption>
|
||||
value={field.state.value}
|
||||
|
@ -13,12 +13,8 @@ const BaseField = <T,>({
|
||||
config,
|
||||
}: BaseFieldProps<T>) => withForm({
|
||||
defaultValues: initialData,
|
||||
props: {
|
||||
config,
|
||||
},
|
||||
render: function Render({
|
||||
form,
|
||||
config,
|
||||
}) {
|
||||
const { type, label, placeholder, variable, tooltip, showConditions, max, min, options, required, showOptional } = config
|
||||
|
||||
|
@ -48,7 +48,7 @@ const BaseForm = <T,>({
|
||||
initialData,
|
||||
config,
|
||||
})
|
||||
return <FieldComponent key={index} form={baseForm} config={config} />
|
||||
return <FieldComponent key={index} form={baseForm} />
|
||||
})}
|
||||
</div>
|
||||
<baseForm.AppForm>
|
||||
|
@ -20,7 +20,7 @@ export type NumberConfiguration = {
|
||||
}
|
||||
|
||||
export type SelectConfiguration = {
|
||||
options?: Option[] // Options for select field
|
||||
options: Option[] // Options for select field
|
||||
}
|
||||
|
||||
export type BaseConfiguration<T> = {
|
||||
@ -33,7 +33,7 @@ export type BaseConfiguration<T> = {
|
||||
showConditions: ShowCondition<T>[] // Show this field only when all conditions are met
|
||||
type: BaseFieldType
|
||||
tooltip?: string // Tooltip for this field
|
||||
} & NumberConfiguration & SelectConfiguration
|
||||
} & NumberConfiguration & Partial<SelectConfiguration>
|
||||
|
||||
export type BaseFormProps<T> = {
|
||||
initialData?: T
|
||||
|
@ -0,0 +1,221 @@
|
||||
import React, { useMemo } from 'react'
|
||||
import { type InputFieldConfiguration, InputFieldType } from './types'
|
||||
import { withForm } from '../..'
|
||||
import { useStore } from '@tanstack/react-form'
|
||||
|
||||
type InputFieldProps<T> = {
|
||||
initialData?: T
|
||||
config: InputFieldConfiguration<T>
|
||||
}
|
||||
|
||||
const InputField = <T,>({
|
||||
initialData,
|
||||
config,
|
||||
}: InputFieldProps<T>) => withForm({
|
||||
defaultValues: initialData,
|
||||
render: function Render({
|
||||
form,
|
||||
}) {
|
||||
const {
|
||||
type,
|
||||
label,
|
||||
placeholder,
|
||||
variable,
|
||||
tooltip,
|
||||
showConditions,
|
||||
max,
|
||||
min,
|
||||
required,
|
||||
showOptional,
|
||||
supportFile,
|
||||
description,
|
||||
options,
|
||||
listeners,
|
||||
} = config
|
||||
|
||||
const fieldValues = useStore(form.store, state => state.values)
|
||||
|
||||
const isAllConditionsMet = useMemo(() => {
|
||||
if (!showConditions.length) return true
|
||||
return showConditions.every((condition) => {
|
||||
const { variable, value } = condition
|
||||
const fieldValue = fieldValues[variable as keyof typeof fieldValues]
|
||||
return fieldValue === value
|
||||
})
|
||||
}, [fieldValues, showConditions])
|
||||
|
||||
if (!isAllConditionsMet)
|
||||
return <></>
|
||||
|
||||
if (type === InputFieldType.textInput) {
|
||||
return (
|
||||
<form.AppField
|
||||
name={variable}
|
||||
children={field => (
|
||||
<field.TextField
|
||||
label={label}
|
||||
labelOptions={{
|
||||
tooltip,
|
||||
isRequired: required,
|
||||
showOptional,
|
||||
}}
|
||||
placeholder={placeholder}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
if (type === InputFieldType.numberInput) {
|
||||
return (
|
||||
<form.AppField
|
||||
name={variable}
|
||||
children={field => (
|
||||
<field.NumberInputField
|
||||
label={label}
|
||||
labelOptions={{
|
||||
tooltip,
|
||||
isRequired: required,
|
||||
showOptional,
|
||||
}}
|
||||
placeholder={placeholder}
|
||||
max={max}
|
||||
min={min}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
if (type === InputFieldType.numberSlider) {
|
||||
return (
|
||||
<form.AppField
|
||||
name={variable}
|
||||
children={field => (
|
||||
<field.NumberSliderField
|
||||
label={label}
|
||||
labelOptions={{
|
||||
tooltip,
|
||||
isRequired: required,
|
||||
showOptional,
|
||||
}}
|
||||
description={description}
|
||||
max={max}
|
||||
min={min}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
if (type === InputFieldType.checkbox) {
|
||||
return (
|
||||
<form.AppField
|
||||
name={variable}
|
||||
children={field => (
|
||||
<field.CheckboxField
|
||||
label={label}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
if (type === InputFieldType.select) {
|
||||
return (
|
||||
<form.AppField
|
||||
name={variable}
|
||||
children={field => (
|
||||
<field.SelectField
|
||||
label={label}
|
||||
labelOptions={{
|
||||
tooltip,
|
||||
isRequired: required,
|
||||
showOptional,
|
||||
}}
|
||||
options={options!}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
if (type === InputFieldType.inputTypeSelect) {
|
||||
return (
|
||||
<form.AppField
|
||||
name={variable}
|
||||
listeners={listeners}
|
||||
children={field => (
|
||||
<field.InputTypeSelectField
|
||||
label={label}
|
||||
labelOptions={{
|
||||
tooltip,
|
||||
isRequired: required,
|
||||
showOptional,
|
||||
}}
|
||||
supportFile={!!supportFile}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
if (type === InputFieldType.uploadMethod) {
|
||||
return (
|
||||
<form.AppField
|
||||
name={variable}
|
||||
children={field => (
|
||||
<field.UploadMethodField
|
||||
label={label}
|
||||
labelOptions={{
|
||||
tooltip,
|
||||
isRequired: required,
|
||||
showOptional,
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
if (type === InputFieldType.fileTypes) {
|
||||
return (
|
||||
<form.AppField
|
||||
name={variable}
|
||||
children={field => (
|
||||
<field.FileTypesField
|
||||
label={label}
|
||||
labelOptions={{
|
||||
tooltip,
|
||||
isRequired: required,
|
||||
showOptional,
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
if (type === InputFieldType.options) {
|
||||
return (
|
||||
<form.AppField
|
||||
name={variable}
|
||||
children={field => (
|
||||
<field.OptionsField
|
||||
label={label}
|
||||
labelOptions={{
|
||||
tooltip,
|
||||
isRequired: required,
|
||||
showOptional,
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
return <></>
|
||||
},
|
||||
})
|
||||
|
||||
export default InputField
|
@ -1,103 +0,0 @@
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { InputType } from '../types'
|
||||
import { InputVarType } from '@/app/components/workflow/types'
|
||||
import { useMemo } from 'react'
|
||||
import {
|
||||
RiAlignLeft,
|
||||
RiCheckboxLine,
|
||||
RiFileCopy2Line,
|
||||
RiFileTextLine,
|
||||
RiHashtag,
|
||||
RiListCheck3,
|
||||
RiTextSnippet,
|
||||
} from '@remixicon/react'
|
||||
|
||||
const i18nFileTypeMap: Record<string, string> = {
|
||||
'file': 'single-file',
|
||||
'file-list': 'multi-files',
|
||||
}
|
||||
|
||||
const INPUT_TYPE_ICON = {
|
||||
[InputVarType.textInput]: RiTextSnippet,
|
||||
[InputVarType.paragraph]: RiAlignLeft,
|
||||
[InputVarType.number]: RiHashtag,
|
||||
[InputVarType.select]: RiListCheck3,
|
||||
[InputVarType.checkbox]: RiCheckboxLine,
|
||||
[InputVarType.singleFile]: RiFileTextLine,
|
||||
[InputVarType.multiFiles]: RiFileCopy2Line,
|
||||
}
|
||||
|
||||
const DATA_TYPE = {
|
||||
[InputVarType.textInput]: 'string',
|
||||
[InputVarType.paragraph]: 'string',
|
||||
[InputVarType.number]: 'number',
|
||||
[InputVarType.select]: 'string',
|
||||
[InputVarType.checkbox]: 'boolean',
|
||||
[InputVarType.singleFile]: 'file',
|
||||
[InputVarType.multiFiles]: 'array[file]',
|
||||
}
|
||||
|
||||
export const useInputTypeOptions = (supportFile: boolean) => {
|
||||
const { t } = useTranslation()
|
||||
const options = supportFile ? InputType.options : InputType.exclude(['file', 'file-list']).options
|
||||
|
||||
return options.map((value) => {
|
||||
return {
|
||||
value,
|
||||
label: t(`appDebug.variableConfig.${i18nFileTypeMap[value] || value}`),
|
||||
Icon: INPUT_TYPE_ICON[value],
|
||||
type: DATA_TYPE[value],
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
export const useHiddenFieldNames = (type: InputVarType) => {
|
||||
const { t } = useTranslation()
|
||||
const hiddenFieldNames = useMemo(() => {
|
||||
let fieldNames = []
|
||||
switch (type) {
|
||||
case InputVarType.textInput:
|
||||
case InputVarType.paragraph:
|
||||
fieldNames = [
|
||||
t('appDebug.variableConfig.defaultValue'),
|
||||
t('appDebug.variableConfig.placeholder'),
|
||||
t('appDebug.variableConfig.tooltips'),
|
||||
]
|
||||
break
|
||||
case InputVarType.number:
|
||||
fieldNames = [
|
||||
t('appDebug.variableConfig.defaultValue'),
|
||||
t('appDebug.variableConfig.unit'),
|
||||
t('appDebug.variableConfig.placeholder'),
|
||||
t('appDebug.variableConfig.tooltips'),
|
||||
]
|
||||
break
|
||||
case InputVarType.select:
|
||||
fieldNames = [
|
||||
t('appDebug.variableConfig.defaultValue'),
|
||||
t('appDebug.variableConfig.tooltips'),
|
||||
]
|
||||
break
|
||||
case InputVarType.singleFile:
|
||||
fieldNames = [
|
||||
t('appDebug.variableConfig.uploadMethod'),
|
||||
t('appDebug.variableConfig.tooltips'),
|
||||
]
|
||||
break
|
||||
case InputVarType.multiFiles:
|
||||
fieldNames = [
|
||||
t('appDebug.variableConfig.uploadMethod'),
|
||||
t('appDebug.variableConfig.maxNumberOfUploads'),
|
||||
t('appDebug.variableConfig.tooltips'),
|
||||
]
|
||||
break
|
||||
default:
|
||||
fieldNames = [
|
||||
t('appDebug.variableConfig.tooltips'),
|
||||
]
|
||||
}
|
||||
return fieldNames.map(name => name.toLowerCase()).join(', ')
|
||||
}, [type, t])
|
||||
|
||||
return hiddenFieldNames
|
||||
}
|
@ -1,83 +0,0 @@
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { withForm } from '../../..'
|
||||
import { type InputVar, SupportUploadFileTypes } from '@/app/components/workflow/types'
|
||||
import { getNewVarInWorkflow } from '@/utils/var'
|
||||
import { useField } from '@tanstack/react-form'
|
||||
import Label from '../../../components/label'
|
||||
import FileTypeItem from '@/app/components/workflow/nodes/_base/components/file-type-item'
|
||||
import { useCallback, useMemo } from 'react'
|
||||
|
||||
type FileTypesFieldsProps = {
|
||||
initialData?: InputVar
|
||||
}
|
||||
|
||||
const UseFileTypesFields = ({
|
||||
initialData,
|
||||
}: FileTypesFieldsProps) => {
|
||||
const FileTypesFields = useMemo(() => {
|
||||
return withForm({
|
||||
defaultValues: initialData || getNewVarInWorkflow(''),
|
||||
render: function Render({
|
||||
form,
|
||||
}) {
|
||||
const { t } = useTranslation()
|
||||
const allowFileTypesField = useField({ form, name: 'allowed_file_types' })
|
||||
const allowFileExtensionsField = useField({ form, name: 'allowed_file_extensions' })
|
||||
const { value: allowed_file_types = [] } = allowFileTypesField.state
|
||||
const { value: allowed_file_extensions = [] } = allowFileExtensionsField.state
|
||||
|
||||
const handleSupportFileTypeChange = useCallback((type: SupportUploadFileTypes) => {
|
||||
let newAllowFileTypes = [...allowed_file_types]
|
||||
if (type === SupportUploadFileTypes.custom) {
|
||||
if (!newAllowFileTypes.includes(SupportUploadFileTypes.custom))
|
||||
newAllowFileTypes = [SupportUploadFileTypes.custom]
|
||||
else
|
||||
newAllowFileTypes = newAllowFileTypes.filter(v => v !== type)
|
||||
}
|
||||
else {
|
||||
newAllowFileTypes = newAllowFileTypes.filter(v => v !== SupportUploadFileTypes.custom)
|
||||
if (newAllowFileTypes.includes(type))
|
||||
newAllowFileTypes = newAllowFileTypes.filter(v => v !== type)
|
||||
else
|
||||
newAllowFileTypes.push(type)
|
||||
}
|
||||
allowFileTypesField.handleChange(newAllowFileTypes)
|
||||
}, [allowFileTypesField, allowed_file_types])
|
||||
|
||||
const handleCustomFileTypesChange = useCallback((customFileTypes: string[]) => {
|
||||
allowFileExtensionsField.handleChange(customFileTypes)
|
||||
}, [allowFileExtensionsField])
|
||||
|
||||
return (
|
||||
<div className='flex flex-col gap-y-0.5'>
|
||||
<Label
|
||||
htmlFor='allowed_file_types'
|
||||
label={t('appDebug.variableConfig.file.supportFileTypes')}
|
||||
/>
|
||||
{
|
||||
[SupportUploadFileTypes.document, SupportUploadFileTypes.image, SupportUploadFileTypes.audio, SupportUploadFileTypes.video].map((type: SupportUploadFileTypes) => (
|
||||
<FileTypeItem
|
||||
key={type}
|
||||
type={type as SupportUploadFileTypes.image | SupportUploadFileTypes.document | SupportUploadFileTypes.audio | SupportUploadFileTypes.video}
|
||||
selected={allowed_file_types.includes(type)}
|
||||
onToggle={handleSupportFileTypeChange}
|
||||
/>
|
||||
))
|
||||
}
|
||||
<FileTypeItem
|
||||
type={SupportUploadFileTypes.custom}
|
||||
selected={allowed_file_types.includes(SupportUploadFileTypes.custom)}
|
||||
onToggle={handleSupportFileTypeChange}
|
||||
customFileTypes={allowed_file_extensions}
|
||||
onCustomFileTypesChange={handleCustomFileTypesChange}
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
},
|
||||
})
|
||||
}, [initialData])
|
||||
|
||||
return FileTypesFields
|
||||
}
|
||||
|
||||
export default UseFileTypesFields
|
@ -1,75 +0,0 @@
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { withForm } from '../../..'
|
||||
import type { InputVar } from '@/app/components/workflow/types'
|
||||
import { getNewVarInWorkflow } from '@/utils/var'
|
||||
import { useField } from '@tanstack/react-form'
|
||||
import Label from '../../../components/label'
|
||||
import { useCallback, useMemo } from 'react'
|
||||
import { useFileSizeLimit } from '@/app/components/base/file-uploader/hooks'
|
||||
import { formatFileSize } from '@/utils/format'
|
||||
import InputNumberWithSlider from '@/app/components/workflow/nodes/_base/components/input-number-with-slider'
|
||||
import { useFileUploadConfig } from '@/service/use-common'
|
||||
|
||||
type MaxNumberOfUploadsFieldProps = {
|
||||
initialData?: InputVar
|
||||
}
|
||||
|
||||
const UseMaxNumberOfUploadsField = ({
|
||||
initialData,
|
||||
}: MaxNumberOfUploadsFieldProps) => {
|
||||
const MaxNumberOfUploadsField = useMemo(() => {
|
||||
return withForm({
|
||||
defaultValues: initialData || getNewVarInWorkflow(''),
|
||||
render: function Render({
|
||||
form,
|
||||
}) {
|
||||
const { t } = useTranslation()
|
||||
const maxNumberOfUploadsField = useField({ form, name: 'max_length' })
|
||||
const { value: max_length = 0 } = maxNumberOfUploadsField.state
|
||||
|
||||
const { data: fileUploadConfigResponse } = useFileUploadConfig()
|
||||
const {
|
||||
imgSizeLimit,
|
||||
docSizeLimit,
|
||||
audioSizeLimit,
|
||||
videoSizeLimit,
|
||||
maxFileUploadLimit,
|
||||
} = useFileSizeLimit(fileUploadConfigResponse)
|
||||
|
||||
const handleMaxUploadNumLimitChange = useCallback((value: number) => {
|
||||
maxNumberOfUploadsField.handleChange(value)
|
||||
}, [maxNumberOfUploadsField])
|
||||
|
||||
return (
|
||||
<div className='flex flex-col gap-y-0.5'>
|
||||
<Label
|
||||
htmlFor='allowed_file_types'
|
||||
label={t('appDebug.variableConfig.maxNumberOfUploads')}
|
||||
/>
|
||||
<div>
|
||||
<div className='body-xs-regular mb-1.5 text-text-tertiary'>
|
||||
{t('appDebug.variableConfig.maxNumberTip', {
|
||||
imgLimit: formatFileSize(imgSizeLimit),
|
||||
docLimit: formatFileSize(docSizeLimit),
|
||||
audioLimit: formatFileSize(audioSizeLimit),
|
||||
videoLimit: formatFileSize(videoSizeLimit),
|
||||
})}
|
||||
</div>
|
||||
|
||||
<InputNumberWithSlider
|
||||
value={max_length}
|
||||
min={1}
|
||||
max={maxFileUploadLimit}
|
||||
onChange={handleMaxUploadNumLimitChange}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
},
|
||||
})
|
||||
}, [initialData])
|
||||
|
||||
return MaxNumberOfUploadsField
|
||||
}
|
||||
|
||||
export default UseMaxNumberOfUploadsField
|
@ -1,64 +0,0 @@
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { withForm } from '../../..'
|
||||
import type { InputVar } from '@/app/components/workflow/types'
|
||||
import { getNewVarInWorkflow } from '@/utils/var'
|
||||
import { useField } from '@tanstack/react-form'
|
||||
import Label from '../../../components/label'
|
||||
import { useCallback, useMemo } from 'react'
|
||||
import OptionCard from '@/app/components/workflow/nodes/_base/components/option-card'
|
||||
import { TransferMethod } from '@/types/app'
|
||||
|
||||
type UploadMethodFieldProps = {
|
||||
initialData?: InputVar
|
||||
}
|
||||
|
||||
const UseUploadMethodField = ({
|
||||
initialData,
|
||||
}: UploadMethodFieldProps) => {
|
||||
const UploadMethodField = useMemo(() => {
|
||||
return withForm({
|
||||
defaultValues: initialData || getNewVarInWorkflow(''),
|
||||
render: function Render({
|
||||
form,
|
||||
}) {
|
||||
const { t } = useTranslation()
|
||||
const allowFileUploadMethodField = useField({ form, name: 'allowed_file_upload_methods' })
|
||||
const { value: allowed_file_upload_methods = [] } = allowFileUploadMethodField.state
|
||||
|
||||
const handleUploadMethodChange = useCallback((method: TransferMethod) => {
|
||||
allowFileUploadMethodField.handleChange(method === TransferMethod.all ? [TransferMethod.local_file, TransferMethod.remote_url] : [method])
|
||||
}, [allowFileUploadMethodField])
|
||||
|
||||
return (
|
||||
<div className='flex flex-col gap-y-0.5'>
|
||||
<Label
|
||||
htmlFor='allowed_file_types'
|
||||
label={t('appDebug.variableConfig.uploadFileTypes')}
|
||||
/>
|
||||
<div className='grid grid-cols-3 gap-2'>
|
||||
<OptionCard
|
||||
title={t('appDebug.variableConfig.localUpload')}
|
||||
selected={allowed_file_upload_methods.length === 1 && allowed_file_upload_methods.includes(TransferMethod.local_file)}
|
||||
onSelect={handleUploadMethodChange.bind(null, TransferMethod.local_file)}
|
||||
/>
|
||||
<OptionCard
|
||||
title="URL"
|
||||
selected={allowed_file_upload_methods.length === 1 && allowed_file_upload_methods.includes(TransferMethod.remote_url)}
|
||||
onSelect={handleUploadMethodChange.bind(null, TransferMethod.remote_url)}
|
||||
/>
|
||||
<OptionCard
|
||||
title={t('appDebug.variableConfig.both')}
|
||||
selected={allowed_file_upload_methods.includes(TransferMethod.local_file) && allowed_file_upload_methods.includes(TransferMethod.remote_url)}
|
||||
onSelect={handleUploadMethodChange.bind(null, TransferMethod.all)}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
},
|
||||
})
|
||||
}, [initialData])
|
||||
|
||||
return UploadMethodField
|
||||
}
|
||||
|
||||
export default UseUploadMethodField
|
@ -1,328 +0,0 @@
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { useAppForm } from '../..'
|
||||
import { type FileTypeSelectOption, type InputFieldFormProps, TEXT_MAX_LENGTH, createInputFieldSchema } from './types'
|
||||
import { getNewVarInWorkflow } from '@/utils/var'
|
||||
import { useHiddenFieldNames, useInputTypeOptions } from './hooks'
|
||||
import Divider from '../../../divider'
|
||||
import { useCallback, useMemo, useState } from 'react'
|
||||
import { useStore } from '@tanstack/react-form'
|
||||
import { ChangeType, InputVarType } from '@/app/components/workflow/types'
|
||||
import ShowAllSettings from './show-all-settings'
|
||||
import Button from '../../../button'
|
||||
import UseFileTypesFields from './hooks/use-file-types-fields'
|
||||
import UseUploadMethodField from './hooks/use-upload-method-field'
|
||||
import UseMaxNumberOfUploadsField from './hooks/use-max-number-of-uploads-filed'
|
||||
import { DEFAULT_FILE_UPLOAD_SETTING } from '@/app/components/workflow/constants'
|
||||
import { DEFAULT_VALUE_MAX_LEN } from '@/config'
|
||||
import { RiArrowDownSLine } from '@remixicon/react'
|
||||
import cn from '@/utils/classnames'
|
||||
import Badge from '../../../badge'
|
||||
import Toast from '../../../toast'
|
||||
|
||||
const InputFieldForm = ({
|
||||
initialData,
|
||||
supportFile = false,
|
||||
onCancel,
|
||||
onSubmit,
|
||||
}: InputFieldFormProps) => {
|
||||
const { t } = useTranslation()
|
||||
const form = useAppForm({
|
||||
defaultValues: initialData || getNewVarInWorkflow(''),
|
||||
validators: {
|
||||
onSubmit: ({ value }) => {
|
||||
const { type } = value
|
||||
const schema = createInputFieldSchema(type, t)
|
||||
const result = schema.safeParse(value)
|
||||
if (!result.success) {
|
||||
const issues = result.error.issues
|
||||
const firstIssue = issues[0].message
|
||||
Toast.notify({
|
||||
type: 'error',
|
||||
message: firstIssue,
|
||||
})
|
||||
return firstIssue
|
||||
}
|
||||
return undefined
|
||||
},
|
||||
},
|
||||
onSubmit: ({ value }) => {
|
||||
const moreInfo = value.variable === initialData?.variable
|
||||
? undefined
|
||||
: {
|
||||
type: ChangeType.changeVarName,
|
||||
payload: { beforeKey: initialData?.variable || '', afterKey: value.variable },
|
||||
}
|
||||
onSubmit(value, moreInfo)
|
||||
},
|
||||
})
|
||||
|
||||
const [showAllSettings, setShowAllSettings] = useState(false)
|
||||
const type = useStore(form.store, state => state.values.type)
|
||||
const options = useStore(form.store, state => state.values.options)
|
||||
const hiddenFieldNames = useHiddenFieldNames(type)
|
||||
const inputTypes = useInputTypeOptions(supportFile)
|
||||
|
||||
const FileTypesFields = UseFileTypesFields({ initialData })
|
||||
const UploadMethodField = UseUploadMethodField({ initialData })
|
||||
const MaxNumberOfUploads = UseMaxNumberOfUploadsField({ initialData })
|
||||
|
||||
const isTextInput = [InputVarType.textInput, InputVarType.paragraph].includes(type)
|
||||
const isNumberInput = type === InputVarType.number
|
||||
const isSelectInput = type === InputVarType.select
|
||||
const isSingleFile = type === InputVarType.singleFile
|
||||
const isMultipleFile = type === InputVarType.multiFiles
|
||||
|
||||
const defaultSelectOptions = useMemo(() => {
|
||||
if (isSelectInput && options) {
|
||||
const defaultOptions = [
|
||||
{
|
||||
value: '',
|
||||
label: t('appDebug.variableConfig.noDefaultSelected'),
|
||||
},
|
||||
]
|
||||
const otherOptions = options.map((option: string) => ({
|
||||
value: option,
|
||||
label: option,
|
||||
}))
|
||||
return [...defaultOptions, ...otherOptions]
|
||||
}
|
||||
return []
|
||||
}, [isSelectInput, options, t])
|
||||
|
||||
const handleTypeChange = useCallback((type: string) => {
|
||||
if ([InputVarType.singleFile, InputVarType.multiFiles].includes(type as InputVarType)) {
|
||||
(Object.keys(DEFAULT_FILE_UPLOAD_SETTING)).forEach((key) => {
|
||||
if (key !== 'max_length')
|
||||
form.setFieldValue(key as keyof typeof form.options.defaultValues, (DEFAULT_FILE_UPLOAD_SETTING as any)[key])
|
||||
})
|
||||
if (type === InputVarType.multiFiles)
|
||||
form.setFieldValue('max_length', DEFAULT_FILE_UPLOAD_SETTING.max_length)
|
||||
}
|
||||
if (type === InputVarType.paragraph)
|
||||
form.setFieldValue('max_length', DEFAULT_VALUE_MAX_LEN)
|
||||
}, [form])
|
||||
|
||||
const handleShowAllSettings = useCallback(() => {
|
||||
setShowAllSettings(true)
|
||||
}, [])
|
||||
|
||||
return (
|
||||
<form
|
||||
className='w-full'
|
||||
onSubmit={(e) => {
|
||||
e.preventDefault()
|
||||
e.stopPropagation()
|
||||
form.handleSubmit()
|
||||
}}
|
||||
>
|
||||
<div className='flex flex-col gap-4 px-4 py-2'>
|
||||
<form.AppField
|
||||
name='type'
|
||||
children={field => (
|
||||
<field.CustomSelectField<FileTypeSelectOption>
|
||||
label={t('appDebug.variableConfig.fieldType')}
|
||||
options={inputTypes}
|
||||
onChange={handleTypeChange}
|
||||
triggerProps={{
|
||||
className: 'gap-x-0.5',
|
||||
}}
|
||||
popupProps={{
|
||||
className: 'w-[368px]',
|
||||
wrapperClassName: 'z-40',
|
||||
itemClassName: 'gap-x-1',
|
||||
}}
|
||||
CustomTrigger={(option, open) => {
|
||||
return (
|
||||
<>
|
||||
{option ? (
|
||||
<>
|
||||
<option.Icon className='h-4 w-4 shrink-0 text-text-tertiary' />
|
||||
<span className='grow p-1'>{option.label}</span>
|
||||
<div className='pr-0.5'>
|
||||
<Badge text={option.type} uppercase={false} />
|
||||
</div>
|
||||
</>
|
||||
) : (
|
||||
<span className='grow p-1'>{t('common.placeholder.select')}</span>
|
||||
)}
|
||||
<RiArrowDownSLine
|
||||
className={cn(
|
||||
'h-4 w-4 shrink-0 text-text-quaternary group-hover:text-text-secondary',
|
||||
open && 'text-text-secondary',
|
||||
)}
|
||||
/>
|
||||
</>
|
||||
)
|
||||
}}
|
||||
CustomOption={(option) => {
|
||||
return (
|
||||
<>
|
||||
<option.Icon className='h-4 w-4 shrink-0 text-text-tertiary' />
|
||||
<span className='grow px-1'>{option.label}</span>
|
||||
<Badge text={option.type} uppercase={false} />
|
||||
</>
|
||||
)
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
<form.AppField
|
||||
name='variable'
|
||||
children={field => (
|
||||
<field.TextField
|
||||
label={t('appDebug.variableConfig.varName')}
|
||||
placeholder={t('appDebug.variableConfig.inputPlaceholder')!}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
<form.AppField
|
||||
name='label'
|
||||
children={field => (
|
||||
<field.TextField
|
||||
label={t('appDebug.variableConfig.labelName')}
|
||||
placeholder={t('appDebug.variableConfig.inputPlaceholder')!}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
{isTextInput && (
|
||||
<form.AppField
|
||||
name='max_length'
|
||||
children={field => (
|
||||
<field.NumberInputField
|
||||
label={t('appDebug.variableConfig.maxLength')}
|
||||
max={TEXT_MAX_LENGTH}
|
||||
min={1}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
)}
|
||||
{isSelectInput && (
|
||||
<form.AppField
|
||||
name='options'
|
||||
listeners={{
|
||||
onChange: () => {
|
||||
form.setFieldValue('default', '')
|
||||
},
|
||||
}}
|
||||
children={field => (
|
||||
<field.OptionsField
|
||||
label={t('appDebug.variableConfig.options')}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
)}
|
||||
{(isSingleFile || isMultipleFile) && (
|
||||
<FileTypesFields form={form} />
|
||||
)}
|
||||
<form.AppField
|
||||
name='required'
|
||||
children={field => (
|
||||
<field.CheckboxField
|
||||
label={t('appDebug.variableConfig.required')}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
<Divider type='horizontal' />
|
||||
{!showAllSettings && (
|
||||
<ShowAllSettings
|
||||
handleShowAllSettings={handleShowAllSettings}
|
||||
description={hiddenFieldNames}
|
||||
/>
|
||||
)}
|
||||
{showAllSettings && (
|
||||
<>
|
||||
{isTextInput && (
|
||||
<form.AppField
|
||||
name='default'
|
||||
children={field => (
|
||||
<field.TextField
|
||||
label={t('appDebug.variableConfig.defaultValue')}
|
||||
placeholder={t('appDebug.variableConfig.defaultValuePlaceholder')!}
|
||||
showOptional
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
)}
|
||||
{isNumberInput && (
|
||||
<form.AppField
|
||||
name='default'
|
||||
children={field => (
|
||||
<field.NumberInputField
|
||||
label={t('appDebug.variableConfig.defaultValue')}
|
||||
placeholder={t('appDebug.variableConfig.defaultValuePlaceholder')!}
|
||||
showOptional
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
)}
|
||||
{isSelectInput && (
|
||||
<form.AppField
|
||||
name='default'
|
||||
children={field => (
|
||||
<field.SelectField
|
||||
label={t('appDebug.variableConfig.startSelectedOption')}
|
||||
options={defaultSelectOptions}
|
||||
showOptional
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
)}
|
||||
{(isTextInput || isNumberInput) && (
|
||||
<form.AppField
|
||||
name='placeholder'
|
||||
children={field => (
|
||||
<field.TextField
|
||||
label={t('appDebug.variableConfig.placeholder')}
|
||||
placeholder={t('appDebug.variableConfig.placeholderPlaceholder')!}
|
||||
showOptional
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
)}
|
||||
{isNumberInput && (
|
||||
<form.AppField
|
||||
name='unit'
|
||||
children={field => (
|
||||
<field.TextField
|
||||
label={t('appDebug.variableConfig.unit')}
|
||||
placeholder={t('appDebug.variableConfig.unitPlaceholder')!}
|
||||
showOptional
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
)}
|
||||
{(isSingleFile || isMultipleFile) && (
|
||||
<UploadMethodField form={form} />
|
||||
)}
|
||||
{isMultipleFile && (
|
||||
<MaxNumberOfUploads form={form} />
|
||||
)}
|
||||
<form.AppField
|
||||
name='hint'
|
||||
children={(field) => {
|
||||
return (
|
||||
<field.TextField
|
||||
label={t('appDebug.variableConfig.tooltips')}
|
||||
placeholder={t('appDebug.variableConfig.tooltipsPlaceholder')!}
|
||||
showOptional
|
||||
/>
|
||||
)
|
||||
}
|
||||
}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
<div className='flex items-center justify-end gap-x-2 p-4 pt-2'>
|
||||
<Button variant='secondary' onClick={onCancel}>
|
||||
{t('common.operation.cancel')}
|
||||
</Button>
|
||||
<form.AppForm>
|
||||
<form.Actions />
|
||||
</form.AppForm>
|
||||
</div>
|
||||
</form>
|
||||
)
|
||||
}
|
||||
|
||||
export default InputFieldForm
|
@ -1,113 +1,39 @@
|
||||
import { DEFAULT_FILE_UPLOAD_SETTING } from '@/app/components/workflow/constants'
|
||||
import type { MoreInfo } from '@/app/components/workflow/types'
|
||||
import { type InputVar, InputVarType } from '@/app/components/workflow/types'
|
||||
import { MAX_VAR_KEY_LENGTH } from '@/config'
|
||||
import type { RemixiconComponentType } from '@remixicon/react'
|
||||
import type { TFunction } from 'i18next'
|
||||
import { z } from 'zod'
|
||||
import type { DeepKeys, FieldListeners } from '@tanstack/react-form'
|
||||
import type { NumberConfiguration, SelectConfiguration, ShowCondition } from '../base/types'
|
||||
|
||||
export const TEXT_MAX_LENGTH = 256
|
||||
|
||||
export const InputType = z.enum([
|
||||
'text-input',
|
||||
'paragraph',
|
||||
'number',
|
||||
'select',
|
||||
'checkbox',
|
||||
'file',
|
||||
'file-list',
|
||||
])
|
||||
|
||||
const TransferMethod = z.enum([
|
||||
'all',
|
||||
'local_file',
|
||||
'remote_url',
|
||||
])
|
||||
|
||||
const SupportedFileTypes = z.enum([
|
||||
'image',
|
||||
'document',
|
||||
'video',
|
||||
'audio',
|
||||
'custom',
|
||||
])
|
||||
|
||||
export const createInputFieldSchema = (type: InputVarType, t: TFunction) => {
|
||||
const commonSchema = z.object({
|
||||
type: InputType,
|
||||
variable: z.string({
|
||||
invalid_type_error: t('appDebug.varKeyError.notValid', { key: t('appDebug.variableConfig.varName') }),
|
||||
}).nonempty({
|
||||
message: t('appDebug.varKeyError.canNoBeEmpty', { key: t('appDebug.variableConfig.varName') }),
|
||||
}).max(MAX_VAR_KEY_LENGTH, {
|
||||
message: t('appDebug.varKeyError.tooLong', { key: t('appDebug.variableConfig.varName') }),
|
||||
}).regex(/^(?!\d)\w+/, {
|
||||
message: t('appDebug.varKeyError.notStartWithNumber', { key: t('appDebug.variableConfig.varName') }),
|
||||
}),
|
||||
label: z.string().nonempty({
|
||||
message: t('appDebug.variableConfig.errorMsg.labelNameRequired'),
|
||||
}),
|
||||
required: z.boolean(),
|
||||
hint: z.string().optional(),
|
||||
})
|
||||
if (type === InputVarType.textInput || type === InputVarType.paragraph) {
|
||||
return z.object({
|
||||
max_length: z.number().min(1).max(TEXT_MAX_LENGTH),
|
||||
default: z.string().optional(),
|
||||
}).merge(commonSchema).passthrough()
|
||||
}
|
||||
if (type === InputVarType.number) {
|
||||
return z.object({
|
||||
default: z.number().optional(),
|
||||
unit: z.string().optional(),
|
||||
placeholder: z.string().optional(),
|
||||
}).merge(commonSchema).passthrough()
|
||||
}
|
||||
if (type === InputVarType.select) {
|
||||
return z.object({
|
||||
options: z.array(z.string()).nonempty({
|
||||
message: t('appDebug.variableConfig.errorMsg.atLeastOneOption'),
|
||||
}).refine(
|
||||
arr => new Set(arr).size === arr.length,
|
||||
{
|
||||
message: t('appDebug.variableConfig.errorMsg.optionRepeat'),
|
||||
},
|
||||
),
|
||||
default: z.string().optional(),
|
||||
}).merge(commonSchema).passthrough()
|
||||
}
|
||||
if (type === InputVarType.singleFile) {
|
||||
return z.object({
|
||||
allowed_file_types: z.array(SupportedFileTypes),
|
||||
allowed_file_extensions: z.string().optional(),
|
||||
allowed_file_upload_methods: z.array(TransferMethod),
|
||||
}).merge(commonSchema).passthrough()
|
||||
}
|
||||
if (type === InputVarType.multiFiles) {
|
||||
return z.object({
|
||||
allowed_file_types: z.array(SupportedFileTypes),
|
||||
allowed_file_extensions: z.array(z.string()).optional(),
|
||||
allowed_file_upload_methods: z.array(TransferMethod),
|
||||
max_length: z.number().min(1).max(DEFAULT_FILE_UPLOAD_SETTING.max_length),
|
||||
}).merge(commonSchema).passthrough()
|
||||
}
|
||||
return commonSchema.passthrough()
|
||||
export enum InputFieldType {
|
||||
textInput = 'textInput',
|
||||
numberInput = 'numberInput',
|
||||
numberSlider = 'numberSlider',
|
||||
checkbox = 'checkbox',
|
||||
options = 'options',
|
||||
select = 'select',
|
||||
inputTypeSelect = 'inputTypeSelect',
|
||||
uploadMethod = 'uploadMethod',
|
||||
fileTypes = 'fileTypes',
|
||||
}
|
||||
|
||||
export type InputFieldFormProps = {
|
||||
initialData?: InputVar
|
||||
supportFile?: boolean
|
||||
onCancel: () => void
|
||||
onSubmit: (value: InputVar, moreInfo?: MoreInfo) => void
|
||||
export type InputTypeSelectConfiguration = {
|
||||
supportFile: boolean
|
||||
}
|
||||
|
||||
export type TextFieldsProps = {
|
||||
initialData?: InputVar
|
||||
export type NumberSliderConfiguration = {
|
||||
description: string
|
||||
max?: number
|
||||
min?: number
|
||||
}
|
||||
|
||||
export type FileTypeSelectOption = {
|
||||
value: string
|
||||
export type InputFieldConfiguration<T> = {
|
||||
label: string
|
||||
Icon: RemixiconComponentType
|
||||
type: string
|
||||
}
|
||||
variable: DeepKeys<T> // Variable name
|
||||
maxLength?: number // Max length for text input
|
||||
placeholder?: string
|
||||
required: boolean
|
||||
showOptional?: boolean // show optional label
|
||||
showConditions: ShowCondition<T>[] // Show this field only when all conditions are met
|
||||
type: InputFieldType
|
||||
tooltip?: string // Tooltip for this field
|
||||
listeners?: FieldListeners<T, DeepKeys<T>> // Listener for this field
|
||||
} & NumberConfiguration & Partial<InputTypeSelectConfiguration>
|
||||
& Partial<NumberSliderConfiguration>
|
||||
& Partial<SelectConfiguration>
|
||||
|
@ -0,0 +1,65 @@
|
||||
import type { ZodSchema, ZodString } from 'zod'
|
||||
import { z } from 'zod'
|
||||
import { type InputFieldConfiguration, InputFieldType } from './types'
|
||||
|
||||
export const generateZodSchema = <T>(fields: InputFieldConfiguration<T>[]) => {
|
||||
const shape: Record<string, ZodSchema> = {}
|
||||
|
||||
fields.forEach((field) => {
|
||||
let zodType
|
||||
|
||||
switch (field.type) {
|
||||
case InputFieldType.textInput:
|
||||
zodType = z.string()
|
||||
break
|
||||
case InputFieldType.numberInput:
|
||||
zodType = z.number()
|
||||
break
|
||||
case InputFieldType.numberSlider:
|
||||
zodType = z.number()
|
||||
break
|
||||
case InputFieldType.checkbox:
|
||||
zodType = z.boolean()
|
||||
break
|
||||
case InputFieldType.fileTypes:
|
||||
zodType = z.array(z.string())
|
||||
break
|
||||
case InputFieldType.inputTypeSelect:
|
||||
zodType = z.string()
|
||||
break
|
||||
case InputFieldType.uploadMethod:
|
||||
zodType = z.array(z.string())
|
||||
break
|
||||
default:
|
||||
zodType = z.any()
|
||||
break
|
||||
}
|
||||
|
||||
if (field.required) {
|
||||
if ([InputFieldType.textInput].includes(field.type))
|
||||
zodType = (zodType as ZodString).nonempty(`${field.label} is required`)
|
||||
}
|
||||
else {
|
||||
zodType = zodType.optional()
|
||||
}
|
||||
|
||||
if (field.maxLength) {
|
||||
if ([InputFieldType.textInput].includes(field.type))
|
||||
zodType = (zodType as ZodString).max(field.maxLength, `${field.label} exceeds max length of ${field.maxLength}`)
|
||||
}
|
||||
|
||||
if (field.min) {
|
||||
if ([InputFieldType.numberInput].includes(field.type))
|
||||
zodType = (zodType as ZodString).min(field.min, `${field.label} must be at least ${field.min}`)
|
||||
}
|
||||
|
||||
if (field.max) {
|
||||
if ([InputFieldType.numberInput].includes(field.type))
|
||||
zodType = (zodType as ZodString).max(field.max, `${field.label} exceeds max value of ${field.max}`)
|
||||
}
|
||||
|
||||
shape[field.variable] = zodType
|
||||
})
|
||||
|
||||
return z.object(shape)
|
||||
}
|
@ -11,8 +11,6 @@ import FileTypesField from './components/field/file-types'
|
||||
import UploadMethodField from './components/field/upload-method'
|
||||
import NumberSliderField from './components/field/number-slider'
|
||||
|
||||
export type FormType = ReturnType<typeof useFormContext>
|
||||
|
||||
export const { fieldContext, useFieldContext, formContext, useFormContext }
|
||||
= createFormHookContexts()
|
||||
|
||||
@ -35,3 +33,5 @@ export const { useAppForm, withForm } = createFormHook({
|
||||
fieldContext,
|
||||
formContext,
|
||||
})
|
||||
|
||||
export type FormType = ReturnType<typeof useFormContext>
|
||||
|
@ -0,0 +1,303 @@
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { InputVarType } from '@/app/components/workflow/types'
|
||||
import { useCallback, useMemo } from 'react'
|
||||
import type { InputFieldConfiguration } from '@/app/components/base/form/form-scenarios/input-field/types'
|
||||
import { InputFieldType } from '@/app/components/base/form/form-scenarios/input-field/types'
|
||||
import type { DeepKeys } from '@tanstack/react-form'
|
||||
import { useFileUploadConfig } from '@/service/use-common'
|
||||
import { useFileSizeLimit } from '@/app/components/base/file-uploader/hooks'
|
||||
import { formatFileSize } from '@/utils/format'
|
||||
import { DEFAULT_FILE_UPLOAD_SETTING } from '@/app/components/workflow/constants'
|
||||
import { DEFAULT_VALUE_MAX_LEN } from '@/config'
|
||||
import type { FormData } from './types'
|
||||
import { TEXT_MAX_LENGTH } from './schema'
|
||||
|
||||
export const useHiddenFieldNames = (type: InputVarType) => {
|
||||
const { t } = useTranslation()
|
||||
const hiddenFieldNames = useMemo(() => {
|
||||
let fieldNames = []
|
||||
switch (type) {
|
||||
case InputVarType.textInput:
|
||||
case InputVarType.paragraph:
|
||||
fieldNames = [
|
||||
t('appDebug.variableConfig.defaultValue'),
|
||||
t('appDebug.variableConfig.placeholder'),
|
||||
t('appDebug.variableConfig.tooltips'),
|
||||
]
|
||||
break
|
||||
case InputVarType.number:
|
||||
fieldNames = [
|
||||
t('appDebug.variableConfig.defaultValue'),
|
||||
t('appDebug.variableConfig.unit'),
|
||||
t('appDebug.variableConfig.placeholder'),
|
||||
t('appDebug.variableConfig.tooltips'),
|
||||
]
|
||||
break
|
||||
case InputVarType.select:
|
||||
fieldNames = [
|
||||
t('appDebug.variableConfig.defaultValue'),
|
||||
t('appDebug.variableConfig.tooltips'),
|
||||
]
|
||||
break
|
||||
case InputVarType.singleFile:
|
||||
fieldNames = [
|
||||
t('appDebug.variableConfig.uploadMethod'),
|
||||
t('appDebug.variableConfig.tooltips'),
|
||||
]
|
||||
break
|
||||
case InputVarType.multiFiles:
|
||||
fieldNames = [
|
||||
t('appDebug.variableConfig.uploadMethod'),
|
||||
t('appDebug.variableConfig.maxNumberOfUploads'),
|
||||
t('appDebug.variableConfig.tooltips'),
|
||||
]
|
||||
break
|
||||
default:
|
||||
fieldNames = [
|
||||
t('appDebug.variableConfig.tooltips'),
|
||||
]
|
||||
}
|
||||
return fieldNames.map(name => name.toLowerCase()).join(', ')
|
||||
}, [type, t])
|
||||
|
||||
return hiddenFieldNames
|
||||
}
|
||||
|
||||
export const useConfigurations = (props: {
|
||||
type: string,
|
||||
options: string[] | undefined,
|
||||
setFieldValue: (fieldName: DeepKeys<FormData>, value: any) => void,
|
||||
supportFile: boolean
|
||||
}) => {
|
||||
const { t } = useTranslation()
|
||||
const { type, options, setFieldValue, supportFile } = props
|
||||
|
||||
const { data: fileUploadConfigResponse } = useFileUploadConfig()
|
||||
const {
|
||||
imgSizeLimit,
|
||||
docSizeLimit,
|
||||
audioSizeLimit,
|
||||
videoSizeLimit,
|
||||
} = useFileSizeLimit(fileUploadConfigResponse)
|
||||
|
||||
const isSelectInput = type === InputVarType.select
|
||||
|
||||
const defaultSelectOptions = useMemo(() => {
|
||||
if (isSelectInput && options) {
|
||||
const defaultOptions = [
|
||||
{
|
||||
value: '',
|
||||
label: t('appDebug.variableConfig.noDefaultSelected'),
|
||||
},
|
||||
]
|
||||
const otherOptions = options.map((option: string) => ({
|
||||
value: option,
|
||||
label: option,
|
||||
}))
|
||||
return [...defaultOptions, ...otherOptions]
|
||||
}
|
||||
return []
|
||||
}, [isSelectInput, options, t])
|
||||
|
||||
const handleTypeChange = useCallback((type: string) => {
|
||||
if ([InputVarType.singleFile, InputVarType.multiFiles].includes(type as InputVarType)) {
|
||||
setFieldValue('allowedFileUploadMethods', DEFAULT_FILE_UPLOAD_SETTING.allowed_file_upload_methods)
|
||||
setFieldValue('allowedTypesAndExtensions', {
|
||||
allowedFileTypes: DEFAULT_FILE_UPLOAD_SETTING.allowed_file_types,
|
||||
allowedFileExtensions: DEFAULT_FILE_UPLOAD_SETTING.allowed_file_extensions,
|
||||
})
|
||||
if (type === InputVarType.multiFiles)
|
||||
setFieldValue('maxLength', DEFAULT_FILE_UPLOAD_SETTING.max_length)
|
||||
}
|
||||
if (type === InputVarType.paragraph)
|
||||
setFieldValue('maxLength', DEFAULT_VALUE_MAX_LEN)
|
||||
}, [setFieldValue])
|
||||
|
||||
const initialConfigurations = useMemo((): InputFieldConfiguration<FormData>[] => {
|
||||
return [{
|
||||
type: InputFieldType.inputTypeSelect,
|
||||
label: t('appDebug.variableConfig.fieldType'),
|
||||
variable: 'type',
|
||||
required: true,
|
||||
showConditions: [],
|
||||
listeners: {
|
||||
onChange: ({ value }) => handleTypeChange(value as string),
|
||||
},
|
||||
supportFile,
|
||||
}, {
|
||||
type: InputFieldType.textInput,
|
||||
label: t('appDebug.variableConfig.varName'),
|
||||
variable: 'variable',
|
||||
placeholder: t('appDebug.variableConfig.inputPlaceholder'),
|
||||
required: true,
|
||||
showConditions: [],
|
||||
}, {
|
||||
type: InputFieldType.textInput,
|
||||
label: t('appDebug.variableConfig.labelName'),
|
||||
variable: 'label',
|
||||
placeholder: t('appDebug.variableConfig.inputPlaceholder'),
|
||||
required: true,
|
||||
showConditions: [],
|
||||
}, {
|
||||
type: InputFieldType.numberInput,
|
||||
label: t('appDebug.variableConfig.maxLength'),
|
||||
variable: 'maxLength',
|
||||
placeholder: t('appDebug.variableConfig.inputPlaceholder'),
|
||||
required: true,
|
||||
showConditions: [{
|
||||
variable: 'type',
|
||||
value: 'text-input',
|
||||
}],
|
||||
min: 1,
|
||||
max: TEXT_MAX_LENGTH,
|
||||
}, {
|
||||
type: InputFieldType.options,
|
||||
label: t('appDebug.variableConfig.options'),
|
||||
variable: 'options',
|
||||
required: true,
|
||||
showConditions: [{
|
||||
variable: 'type',
|
||||
value: 'select',
|
||||
}],
|
||||
}, {
|
||||
type: InputFieldType.fileTypes,
|
||||
label: t('appDebug.variableConfig.file.supportFileTypes'),
|
||||
variable: 'allowedTypesAndExtensions',
|
||||
required: true,
|
||||
showConditions: [{
|
||||
variable: 'type',
|
||||
value: 'file',
|
||||
}],
|
||||
}, {
|
||||
type: InputFieldType.fileTypes,
|
||||
label: t('appDebug.variableConfig.file.supportFileTypes'),
|
||||
variable: 'allowedTypesAndExtensions',
|
||||
required: true,
|
||||
showConditions: [{
|
||||
variable: 'type',
|
||||
value: 'file-list',
|
||||
}],
|
||||
}, {
|
||||
type: InputFieldType.checkbox,
|
||||
label: t('appDebug.variableConfig.required'),
|
||||
variable: 'required',
|
||||
required: true,
|
||||
showConditions: [],
|
||||
}]
|
||||
}, [handleTypeChange, supportFile, t])
|
||||
|
||||
const hiddenConfigurations = useMemo((): InputFieldConfiguration<FormData>[] => {
|
||||
return [{
|
||||
type: InputFieldType.textInput,
|
||||
label: t('appDebug.variableConfig.defaultValue'),
|
||||
variable: 'default',
|
||||
placeholder: t('appDebug.variableConfig.defaultValuePlaceholder'),
|
||||
required: false,
|
||||
showConditions: [{
|
||||
variable: 'type',
|
||||
value: 'text-input',
|
||||
}],
|
||||
showOptional: true,
|
||||
}, {
|
||||
type: InputFieldType.textInput,
|
||||
label: t('appDebug.variableConfig.defaultValue'),
|
||||
variable: 'default',
|
||||
placeholder: t('appDebug.variableConfig.defaultValuePlaceholder'),
|
||||
required: false,
|
||||
showConditions: [{
|
||||
variable: 'type',
|
||||
value: 'number',
|
||||
}],
|
||||
showOptional: true,
|
||||
}, {
|
||||
type: InputFieldType.select,
|
||||
label: t('appDebug.variableConfig.startSelectedOption'),
|
||||
variable: 'default',
|
||||
required: false,
|
||||
showConditions: [{
|
||||
variable: 'type',
|
||||
value: 'select',
|
||||
}],
|
||||
showOptional: true,
|
||||
options: defaultSelectOptions,
|
||||
}, {
|
||||
type: InputFieldType.textInput,
|
||||
label: t('appDebug.variableConfig.placeholder'),
|
||||
variable: 'placeholder',
|
||||
placeholder: t('appDebug.variableConfig.placeholderPlaceholder'),
|
||||
required: false,
|
||||
showConditions: [{
|
||||
variable: 'type',
|
||||
value: 'text-input',
|
||||
}],
|
||||
showOptional: true,
|
||||
}, {
|
||||
type: InputFieldType.textInput,
|
||||
label: t('appDebug.variableConfig.unit'),
|
||||
variable: 'unit',
|
||||
placeholder: t('appDebug.variableConfig.unitPlaceholder'),
|
||||
required: false,
|
||||
showConditions: [{
|
||||
variable: 'type',
|
||||
value: 'number',
|
||||
}],
|
||||
showOptional: true,
|
||||
}, {
|
||||
type: InputFieldType.textInput,
|
||||
label: t('appDebug.variableConfig.placeholder'),
|
||||
variable: 'placeholder',
|
||||
placeholder: t('appDebug.variableConfig.placeholderPlaceholder'),
|
||||
required: false,
|
||||
showConditions: [{
|
||||
variable: 'type',
|
||||
value: 'number',
|
||||
}],
|
||||
showOptional: true,
|
||||
}, {
|
||||
type: InputFieldType.uploadMethod,
|
||||
label: t('appDebug.variableConfig.uploadFileTypes'),
|
||||
variable: 'allowedFileUploadMethods',
|
||||
required: false,
|
||||
showConditions: [{
|
||||
variable: 'type',
|
||||
value: 'file',
|
||||
}],
|
||||
}, {
|
||||
type: InputFieldType.uploadMethod,
|
||||
label: t('appDebug.variableConfig.uploadFileTypes'),
|
||||
variable: 'allowedFileUploadMethods',
|
||||
required: false,
|
||||
showConditions: [{
|
||||
variable: 'type',
|
||||
value: 'file-list',
|
||||
}],
|
||||
}, {
|
||||
type: InputFieldType.numberSlider,
|
||||
label: t('appDebug.variableConfig.maxNumberOfUploads'),
|
||||
variable: 'maxLength',
|
||||
required: false,
|
||||
showConditions: [{
|
||||
variable: 'type',
|
||||
value: 'file-list',
|
||||
}],
|
||||
description: t('appDebug.variableConfig.maxNumberTip', {
|
||||
imgLimit: formatFileSize(imgSizeLimit),
|
||||
docLimit: formatFileSize(docSizeLimit),
|
||||
audioLimit: formatFileSize(audioSizeLimit),
|
||||
videoLimit: formatFileSize(videoSizeLimit),
|
||||
}),
|
||||
}, {
|
||||
type: InputFieldType.textInput,
|
||||
label: t('appDebug.variableConfig.tooltips'),
|
||||
variable: 'hint',
|
||||
required: false,
|
||||
showConditions: [],
|
||||
showOptional: true,
|
||||
}]
|
||||
}, [defaultSelectOptions, imgSizeLimit, docSizeLimit, audioSizeLimit, videoSizeLimit, t])
|
||||
|
||||
return {
|
||||
initialConfigurations,
|
||||
hiddenConfigurations,
|
||||
}
|
||||
}
|
@ -0,0 +1,129 @@
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { useCallback, useState } from 'react'
|
||||
import type { DeepKeys } from '@tanstack/react-form'
|
||||
import { useStore } from '@tanstack/react-form'
|
||||
import { ChangeType } from '@/app/components/workflow/types'
|
||||
import { useFileUploadConfig } from '@/service/use-common'
|
||||
import type { FormData, InputFieldFormProps } from './types'
|
||||
import { useAppForm } from '@/app/components/base/form'
|
||||
import { createInputFieldSchema } from './schema'
|
||||
import Toast from '@/app/components/base/toast'
|
||||
import { useFileSizeLimit } from '@/app/components/base/file-uploader/hooks'
|
||||
import { useConfigurations, useHiddenFieldNames } from './hooks'
|
||||
import Divider from '@/app/components/base/divider'
|
||||
import ShowAllSettings from './show-all-settings'
|
||||
import Button from '@/app/components/base/button'
|
||||
import InputField from '@/app/components/base/form/form-scenarios/input-field/field'
|
||||
|
||||
const InputFieldForm = ({
|
||||
initialData,
|
||||
supportFile = false,
|
||||
onCancel,
|
||||
onSubmit,
|
||||
}: InputFieldFormProps) => {
|
||||
const { t } = useTranslation()
|
||||
|
||||
const { data: fileUploadConfigResponse } = useFileUploadConfig()
|
||||
const {
|
||||
maxFileUploadLimit,
|
||||
} = useFileSizeLimit(fileUploadConfigResponse)
|
||||
|
||||
const inputFieldForm = useAppForm({
|
||||
defaultValues: initialData,
|
||||
validators: {
|
||||
onSubmit: ({ value }) => {
|
||||
const { type } = value
|
||||
const schema = createInputFieldSchema(type, t, { maxFileUploadLimit })
|
||||
const result = schema.safeParse(value)
|
||||
if (!result.success) {
|
||||
const issues = result.error.issues
|
||||
const firstIssue = issues[0].message
|
||||
Toast.notify({
|
||||
type: 'error',
|
||||
message: firstIssue,
|
||||
})
|
||||
return firstIssue
|
||||
}
|
||||
return undefined
|
||||
},
|
||||
},
|
||||
onSubmit: ({ value }) => {
|
||||
const moreInfo = value.variable === initialData?.variable
|
||||
? undefined
|
||||
: {
|
||||
type: ChangeType.changeVarName,
|
||||
payload: { beforeKey: initialData?.variable || '', afterKey: value.variable },
|
||||
}
|
||||
onSubmit(value, moreInfo)
|
||||
},
|
||||
})
|
||||
|
||||
const [showAllSettings, setShowAllSettings] = useState(false)
|
||||
const type = useStore(inputFieldForm.store, state => state.values.type)
|
||||
const options = useStore(inputFieldForm.store, state => state.values.options)
|
||||
|
||||
const setFieldValue = useCallback((fieldName: DeepKeys<FormData>, value: any) => {
|
||||
inputFieldForm.setFieldValue(fieldName, value)
|
||||
}, [inputFieldForm])
|
||||
|
||||
const hiddenFieldNames = useHiddenFieldNames(type)
|
||||
const { initialConfigurations, hiddenConfigurations } = useConfigurations({
|
||||
type,
|
||||
options,
|
||||
setFieldValue,
|
||||
supportFile,
|
||||
})
|
||||
|
||||
const handleShowAllSettings = useCallback(() => {
|
||||
setShowAllSettings(true)
|
||||
}, [])
|
||||
|
||||
return (
|
||||
<form
|
||||
className='w-full'
|
||||
onSubmit={(e) => {
|
||||
e.preventDefault()
|
||||
e.stopPropagation()
|
||||
inputFieldForm.handleSubmit()
|
||||
}}
|
||||
>
|
||||
<div className='flex flex-col gap-4 px-4 py-2'>
|
||||
{initialConfigurations.map((config, index) => {
|
||||
const FieldComponent = InputField<FormData>({
|
||||
initialData,
|
||||
config,
|
||||
})
|
||||
return <FieldComponent key={`${config.type}-${index}`} form={inputFieldForm} />
|
||||
})}
|
||||
<Divider type='horizontal' />
|
||||
{!showAllSettings && (
|
||||
<ShowAllSettings
|
||||
handleShowAllSettings={handleShowAllSettings}
|
||||
description={hiddenFieldNames}
|
||||
/>
|
||||
)}
|
||||
{showAllSettings && (
|
||||
<>
|
||||
{hiddenConfigurations.map((config, index) => {
|
||||
const FieldComponent = InputField<FormData>({
|
||||
initialData,
|
||||
config,
|
||||
})
|
||||
return <FieldComponent key={`hidden-${config.type}-${index}`} form={inputFieldForm} />
|
||||
})}
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
<div className='flex items-center justify-end gap-x-2 p-4 pt-2'>
|
||||
<Button variant='secondary' onClick={onCancel}>
|
||||
{t('common.operation.cancel')}
|
||||
</Button>
|
||||
<inputFieldForm.AppForm>
|
||||
<inputFieldForm.Actions />
|
||||
</inputFieldForm.AppForm>
|
||||
</div>
|
||||
</form>
|
||||
)
|
||||
}
|
||||
|
||||
export default InputFieldForm
|
@ -0,0 +1,98 @@
|
||||
import { InputVarType } from '@/app/components/workflow/types'
|
||||
import { MAX_VAR_KEY_LENGTH } from '@/config'
|
||||
import type { TFunction } from 'i18next'
|
||||
import { z } from 'zod'
|
||||
import type { SchemaOptions } from './types'
|
||||
|
||||
export const TEXT_MAX_LENGTH = 256
|
||||
|
||||
export const InputType = z.enum([
|
||||
'text-input',
|
||||
'paragraph',
|
||||
'number',
|
||||
'select',
|
||||
'checkbox',
|
||||
'file',
|
||||
'file-list',
|
||||
])
|
||||
|
||||
const TransferMethod = z.enum([
|
||||
'all',
|
||||
'local_file',
|
||||
'remote_url',
|
||||
])
|
||||
|
||||
const SupportedFileTypes = z.enum([
|
||||
'image',
|
||||
'document',
|
||||
'video',
|
||||
'audio',
|
||||
'custom',
|
||||
])
|
||||
|
||||
export const createInputFieldSchema = (type: InputVarType, t: TFunction, options: SchemaOptions) => {
|
||||
const { maxFileUploadLimit } = options
|
||||
const commonSchema = z.object({
|
||||
type: InputType,
|
||||
variable: z.string({
|
||||
invalid_type_error: t('appDebug.varKeyError.notValid', { key: t('appDebug.variableConfig.varName') }),
|
||||
}).nonempty({
|
||||
message: t('appDebug.varKeyError.canNoBeEmpty', { key: t('appDebug.variableConfig.varName') }),
|
||||
}).max(MAX_VAR_KEY_LENGTH, {
|
||||
message: t('appDebug.varKeyError.tooLong', { key: t('appDebug.variableConfig.varName') }),
|
||||
}).regex(/^(?!\d)\w+/, {
|
||||
message: t('appDebug.varKeyError.notStartWithNumber', { key: t('appDebug.variableConfig.varName') }),
|
||||
}),
|
||||
label: z.string().nonempty({
|
||||
message: t('appDebug.variableConfig.errorMsg.labelNameRequired'),
|
||||
}),
|
||||
required: z.boolean(),
|
||||
hint: z.string().optional(),
|
||||
})
|
||||
if (type === InputVarType.textInput || type === InputVarType.paragraph) {
|
||||
return z.object({
|
||||
maxLength: z.number().min(1).max(TEXT_MAX_LENGTH),
|
||||
default: z.string().optional(),
|
||||
}).merge(commonSchema).passthrough()
|
||||
}
|
||||
if (type === InputVarType.number) {
|
||||
return z.object({
|
||||
default: z.number().optional(),
|
||||
unit: z.string().optional(),
|
||||
placeholder: z.string().optional(),
|
||||
}).merge(commonSchema).passthrough()
|
||||
}
|
||||
if (type === InputVarType.select) {
|
||||
return z.object({
|
||||
options: z.array(z.string()).nonempty({
|
||||
message: t('appDebug.variableConfig.errorMsg.atLeastOneOption'),
|
||||
}).refine(
|
||||
arr => new Set(arr).size === arr.length,
|
||||
{
|
||||
message: t('appDebug.variableConfig.errorMsg.optionRepeat'),
|
||||
},
|
||||
),
|
||||
default: z.string().optional(),
|
||||
}).merge(commonSchema).passthrough()
|
||||
}
|
||||
if (type === InputVarType.singleFile) {
|
||||
return z.object({
|
||||
allowedFileTypes: z.array(SupportedFileTypes),
|
||||
allowedTypesAndExtensions: z.object({
|
||||
allowedFileExtensions: z.string().optional(),
|
||||
allowedFileUploadMethods: z.array(TransferMethod),
|
||||
}),
|
||||
}).merge(commonSchema).passthrough()
|
||||
}
|
||||
if (type === InputVarType.multiFiles) {
|
||||
return z.object({
|
||||
allowedFileTypes: z.array(SupportedFileTypes),
|
||||
allowedTypesAndExtensions: z.object({
|
||||
allowedFileExtensions: z.string().optional(),
|
||||
allowedFileUploadMethods: z.array(TransferMethod),
|
||||
}),
|
||||
maxLength: z.number().min(1).max(maxFileUploadLimit),
|
||||
}).merge(commonSchema).passthrough()
|
||||
}
|
||||
return commonSchema.passthrough()
|
||||
}
|
@ -0,0 +1,31 @@
|
||||
import type { InputVarType, MoreInfo, SupportUploadFileTypes } from '@/app/components/workflow/types'
|
||||
import type { TransferMethod } from '@/types/app'
|
||||
|
||||
export type FormData = {
|
||||
type: InputVarType
|
||||
label: string
|
||||
variable: string
|
||||
maxLength?: number
|
||||
default?: string | number
|
||||
required: boolean
|
||||
hint?: string
|
||||
options?: string[]
|
||||
placeholder?: string
|
||||
unit?: string
|
||||
allowedFileUploadMethods?: TransferMethod[]
|
||||
allowedTypesAndExtensions: {
|
||||
allowedFileTypes?: SupportUploadFileTypes[]
|
||||
allowedFileExtensions?: string[]
|
||||
}
|
||||
}
|
||||
|
||||
export type InputFieldFormProps = {
|
||||
initialData: FormData
|
||||
supportFile?: boolean
|
||||
onCancel: () => void
|
||||
onSubmit: (value: FormData, moreInfo?: MoreInfo) => void
|
||||
}
|
||||
|
||||
export type SchemaOptions = {
|
||||
maxFileUploadLimit: number
|
||||
}
|
@ -1,7 +1,8 @@
|
||||
import InputFieldForm from '@/app/components/base/form/form-scenarios/input-field'
|
||||
import { RiCloseLine } from '@remixicon/react'
|
||||
import DialogWrapper from './dialog-wrapper'
|
||||
import DialogWrapper from '../dialog-wrapper'
|
||||
import type { InputVar } from '@/app/components/workflow/types'
|
||||
import InputFieldForm from './form'
|
||||
import { convertToInputFieldFormData } from './utils'
|
||||
|
||||
type InputFieldEditorProps = {
|
||||
show: boolean
|
||||
@ -14,6 +15,8 @@ const InputFieldEditor = ({
|
||||
onClose,
|
||||
initialData,
|
||||
}: InputFieldEditorProps) => {
|
||||
const formData = convertToInputFieldFormData(initialData)
|
||||
|
||||
return (
|
||||
<DialogWrapper
|
||||
show={show}
|
||||
@ -33,7 +36,7 @@ const InputFieldEditor = ({
|
||||
<RiCloseLine className='size-4 text-text-tertiary' />
|
||||
</button>
|
||||
<InputFieldForm
|
||||
initialData={initialData}
|
||||
initialData={formData}
|
||||
supportFile
|
||||
onCancel={onClose}
|
||||
onSubmit={(value) => {
|
@ -0,0 +1,39 @@
|
||||
import type { InputVar } from '@/app/components/workflow/types'
|
||||
import type { FormData } from './form/types'
|
||||
import { getNewVarInWorkflow } from '@/utils/var'
|
||||
|
||||
export const convertToInputFieldFormData = (data?: InputVar): FormData => {
|
||||
const {
|
||||
type,
|
||||
label,
|
||||
variable,
|
||||
max_length,
|
||||
'default': defaultValue,
|
||||
required,
|
||||
hint,
|
||||
options,
|
||||
placeholder,
|
||||
unit,
|
||||
allowed_file_upload_methods,
|
||||
allowed_file_types,
|
||||
allowed_file_extensions,
|
||||
} = data || getNewVarInWorkflow('')
|
||||
|
||||
return {
|
||||
type,
|
||||
label: label as string,
|
||||
variable,
|
||||
maxLength: max_length,
|
||||
default: defaultValue,
|
||||
required,
|
||||
hint,
|
||||
options,
|
||||
placeholder,
|
||||
unit,
|
||||
allowedFileUploadMethods: allowed_file_upload_methods,
|
||||
allowedTypesAndExtensions: {
|
||||
allowedFileTypes: allowed_file_types,
|
||||
allowedFileExtensions: allowed_file_extensions,
|
||||
},
|
||||
}
|
||||
}
|
@ -5,6 +5,7 @@ import {
|
||||
PRE_PROMPT_PLACEHOLDER_TEXT,
|
||||
QUERY_PLACEHOLDER_TEXT,
|
||||
} from '@/app/components/base/prompt-editor/constants'
|
||||
import type { InputVar } from '@/app/components/workflow/types'
|
||||
import { InputVarType } from '@/app/components/workflow/types'
|
||||
|
||||
const otherAllowedRegex = /^\w+$/
|
||||
@ -27,7 +28,7 @@ export const getNewVar = (key: string, type: string) => {
|
||||
}
|
||||
}
|
||||
|
||||
export const getNewVarInWorkflow = (key: string, type = InputVarType.textInput) => {
|
||||
export const getNewVarInWorkflow = (key: string, type = InputVarType.textInput): InputVar => {
|
||||
const { max_length, ...rest } = VAR_ITEM_TEMPLATE_IN_WORKFLOW
|
||||
if (type !== InputVarType.textInput) {
|
||||
return {
|
||||
|
Loading…
x
Reference in New Issue
Block a user