| @@ -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' | |||
| 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() | |||
| import type { DeepKeys, FieldListeners } from '@tanstack/react-form' | |||
| import type { NumberConfiguration, SelectConfiguration, ShowCondition } from '../base/types' | |||
| 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 { | |||