| type InputTypeSelectFieldProps = { | type InputTypeSelectFieldProps = { | ||||
| label: string | label: string | ||||
| labeOptions?: Omit<LabelProps, 'htmlFor' | 'label'> | |||||
| labelOptions?: Omit<LabelProps, 'htmlFor' | 'label'> | |||||
| supportFile: boolean | supportFile: boolean | ||||
| className?: string | className?: string | ||||
| } & Omit<CustomSelectProps<FileTypeSelectOption>, 'options' | 'value' | 'onChange' | 'CustomTrigger' | 'CustomOption'> | } & Omit<CustomSelectProps<FileTypeSelectOption>, 'options' | 'value' | 'onChange' | 'CustomTrigger' | 'CustomOption'> | ||||
| const InputTypeSelectField = ({ | const InputTypeSelectField = ({ | ||||
| label, | label, | ||||
| labeOptions, | |||||
| labelOptions, | |||||
| supportFile, | supportFile, | ||||
| className, | className, | ||||
| ...customSelectProps | ...customSelectProps | ||||
| <Label | <Label | ||||
| htmlFor={field.name} | htmlFor={field.name} | ||||
| label={label} | label={label} | ||||
| {...(labeOptions ?? {})} | |||||
| {...(labelOptions ?? {})} | |||||
| /> | /> | ||||
| <CustomSelect<FileTypeSelectOption> | <CustomSelect<FileTypeSelectOption> | ||||
| value={field.state.value} | value={field.state.value} | 
| config, | config, | ||||
| }: BaseFieldProps<T>) => withForm({ | }: BaseFieldProps<T>) => withForm({ | ||||
| defaultValues: initialData, | defaultValues: initialData, | ||||
| props: { | |||||
| config, | |||||
| }, | |||||
| render: function Render({ | render: function Render({ | ||||
| form, | form, | ||||
| config, | |||||
| }) { | }) { | ||||
| const { type, label, placeholder, variable, tooltip, showConditions, max, min, options, required, showOptional } = config | const { type, label, placeholder, variable, tooltip, showConditions, max, min, options, required, showOptional } = config | ||||
| initialData, | initialData, | ||||
| config, | config, | ||||
| }) | }) | ||||
| return <FieldComponent key={index} form={baseForm} config={config} /> | |||||
| return <FieldComponent key={index} form={baseForm} /> | |||||
| })} | })} | ||||
| </div> | </div> | ||||
| <baseForm.AppForm> | <baseForm.AppForm> | 
| } | } | ||||
| export type SelectConfiguration = { | export type SelectConfiguration = { | ||||
| options?: Option[] // Options for select field | |||||
| options: Option[] // Options for select field | |||||
| } | } | ||||
| export type BaseConfiguration<T> = { | export type BaseConfiguration<T> = { | ||||
| showConditions: ShowCondition<T>[] // Show this field only when all conditions are met | showConditions: ShowCondition<T>[] // Show this field only when all conditions are met | ||||
| type: BaseFieldType | type: BaseFieldType | ||||
| tooltip?: string // Tooltip for this field | tooltip?: string // Tooltip for this field | ||||
| } & NumberConfiguration & SelectConfiguration | |||||
| } & NumberConfiguration & Partial<SelectConfiguration> | |||||
| export type BaseFormProps<T> = { | export type BaseFormProps<T> = { | ||||
| initialData?: T | initialData?: T | 
| 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 | 
| 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 | |||||
| } | 
| 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 | 
| 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 | 
| 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 | 
| 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 | 
| 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 | 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> | 
| 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) | |||||
| } | 
| import UploadMethodField from './components/field/upload-method' | import UploadMethodField from './components/field/upload-method' | ||||
| import NumberSliderField from './components/field/number-slider' | import NumberSliderField from './components/field/number-slider' | ||||
| export type FormType = ReturnType<typeof useFormContext> | |||||
| export const { fieldContext, useFieldContext, formContext, useFormContext } | export const { fieldContext, useFieldContext, formContext, useFormContext } | ||||
| = createFormHookContexts() | = createFormHookContexts() | ||||
| fieldContext, | fieldContext, | ||||
| formContext, | formContext, | ||||
| }) | }) | ||||
| export type FormType = ReturnType<typeof useFormContext> | 
| 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, | |||||
| } | |||||
| } | 
| 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 | 
| 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() | |||||
| } | 
| 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 | |||||
| } | 
| import InputFieldForm from '@/app/components/base/form/form-scenarios/input-field' | |||||
| import { RiCloseLine } from '@remixicon/react' | import { RiCloseLine } from '@remixicon/react' | ||||
| import DialogWrapper from './dialog-wrapper' | |||||
| import DialogWrapper from '../dialog-wrapper' | |||||
| import type { InputVar } from '@/app/components/workflow/types' | import type { InputVar } from '@/app/components/workflow/types' | ||||
| import InputFieldForm from './form' | |||||
| import { convertToInputFieldFormData } from './utils' | |||||
| type InputFieldEditorProps = { | type InputFieldEditorProps = { | ||||
| show: boolean | show: boolean | ||||
| onClose, | onClose, | ||||
| initialData, | initialData, | ||||
| }: InputFieldEditorProps) => { | }: InputFieldEditorProps) => { | ||||
| const formData = convertToInputFieldFormData(initialData) | |||||
| return ( | return ( | ||||
| <DialogWrapper | <DialogWrapper | ||||
| show={show} | show={show} | ||||
| <RiCloseLine className='size-4 text-text-tertiary' /> | <RiCloseLine className='size-4 text-text-tertiary' /> | ||||
| </button> | </button> | ||||
| <InputFieldForm | <InputFieldForm | ||||
| initialData={initialData} | |||||
| initialData={formData} | |||||
| supportFile | supportFile | ||||
| onCancel={onClose} | onCancel={onClose} | ||||
| onSubmit={(value) => { | onSubmit={(value) => { | 
| 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, | |||||
| }, | |||||
| } | |||||
| } | 
| PRE_PROMPT_PLACEHOLDER_TEXT, | PRE_PROMPT_PLACEHOLDER_TEXT, | ||||
| QUERY_PLACEHOLDER_TEXT, | QUERY_PLACEHOLDER_TEXT, | ||||
| } from '@/app/components/base/prompt-editor/constants' | } from '@/app/components/base/prompt-editor/constants' | ||||
| import type { InputVar } from '@/app/components/workflow/types' | |||||
| import { InputVarType } from '@/app/components/workflow/types' | import { InputVarType } from '@/app/components/workflow/types' | ||||
| const otherAllowedRegex = /^\w+$/ | const otherAllowedRegex = /^\w+$/ | ||||
| } | } | ||||
| } | } | ||||
| 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 | const { max_length, ...rest } = VAR_ITEM_TEMPLATE_IN_WORKFLOW | ||||
| if (type !== InputVarType.textInput) { | if (type !== InputVarType.textInput) { | ||||
| return { | return { |