dify
This commit is contained in:
@@ -0,0 +1,25 @@
|
||||
import { memo } from 'react'
|
||||
import { BaseForm } from '../../components/base'
|
||||
import type { BaseFormProps } from '../../components/base'
|
||||
|
||||
const AuthForm = ({
|
||||
formSchemas = [],
|
||||
defaultValues,
|
||||
ref,
|
||||
formFromProps,
|
||||
...rest
|
||||
}: BaseFormProps) => {
|
||||
return (
|
||||
<BaseForm
|
||||
ref={ref}
|
||||
formSchemas={formSchemas}
|
||||
defaultValues={defaultValues}
|
||||
formClassName='space-y-4'
|
||||
labelClassName='h-6 flex items-center mb-1 system-sm-medium text-text-secondary'
|
||||
formFromProps={formFromProps}
|
||||
{...rest}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
export default memo(AuthForm)
|
||||
197
dify/web/app/components/base/form/form-scenarios/base/field.tsx
Normal file
197
dify/web/app/components/base/form/form-scenarios/base/field.tsx
Normal file
@@ -0,0 +1,197 @@
|
||||
import React from 'react'
|
||||
import { type BaseConfiguration, BaseFieldType } from './types'
|
||||
import { withForm } from '../..'
|
||||
import { useStore } from '@tanstack/react-form'
|
||||
|
||||
type BaseFieldProps = {
|
||||
initialData?: Record<string, any>
|
||||
config: BaseConfiguration
|
||||
}
|
||||
|
||||
const BaseField = ({
|
||||
initialData,
|
||||
config,
|
||||
}: BaseFieldProps) => withForm({
|
||||
defaultValues: initialData,
|
||||
render: function Render({
|
||||
form,
|
||||
}) {
|
||||
const {
|
||||
type,
|
||||
label,
|
||||
placeholder,
|
||||
variable,
|
||||
tooltip,
|
||||
showConditions,
|
||||
max,
|
||||
min,
|
||||
options,
|
||||
required,
|
||||
showOptional,
|
||||
popupProps,
|
||||
allowedFileExtensions,
|
||||
allowedFileTypes,
|
||||
allowedFileUploadMethods,
|
||||
maxLength,
|
||||
unit,
|
||||
} = config
|
||||
|
||||
const isAllConditionsMet = useStore(form.store, (state) => {
|
||||
const fieldValues = state.values
|
||||
if (!showConditions.length) return true
|
||||
return showConditions.every((condition) => {
|
||||
const { variable, value } = condition
|
||||
const fieldValue = fieldValues[variable as keyof typeof fieldValues]
|
||||
return fieldValue === value
|
||||
})
|
||||
})
|
||||
|
||||
if (!isAllConditionsMet)
|
||||
return <></>
|
||||
|
||||
if (type === BaseFieldType.textInput) {
|
||||
return (
|
||||
<form.AppField
|
||||
name={variable}
|
||||
children={field => (
|
||||
<field.TextField
|
||||
label={label}
|
||||
labelOptions={{
|
||||
tooltip,
|
||||
isRequired: required,
|
||||
showOptional,
|
||||
}}
|
||||
placeholder={placeholder}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
if (type === BaseFieldType.paragraph) {
|
||||
return (
|
||||
<form.AppField
|
||||
name={variable}
|
||||
children={field => (
|
||||
<field.TextAreaField
|
||||
label={label}
|
||||
labelOptions={{
|
||||
tooltip,
|
||||
isRequired: required,
|
||||
showOptional,
|
||||
}}
|
||||
placeholder={placeholder}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
if (type === BaseFieldType.numberInput) {
|
||||
return (
|
||||
<form.AppField
|
||||
name={variable}
|
||||
children={field => (
|
||||
<field.NumberInputField
|
||||
label={label}
|
||||
labelOptions={{
|
||||
tooltip,
|
||||
isRequired: required,
|
||||
showOptional,
|
||||
}}
|
||||
placeholder={placeholder}
|
||||
max={max}
|
||||
min={min}
|
||||
unit={unit}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
if (type === BaseFieldType.checkbox) {
|
||||
return (
|
||||
<form.AppField
|
||||
name={variable}
|
||||
children={field => (
|
||||
<field.CheckboxField
|
||||
label={label}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
if (type === BaseFieldType.select) {
|
||||
return (
|
||||
<form.AppField
|
||||
name={variable}
|
||||
children={field => (
|
||||
<field.SelectField
|
||||
label={label}
|
||||
labelOptions={{
|
||||
tooltip,
|
||||
isRequired: required,
|
||||
showOptional,
|
||||
}}
|
||||
options={options!}
|
||||
popupProps={popupProps}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
if (type === BaseFieldType.file) {
|
||||
return (
|
||||
<form.AppField
|
||||
name={variable}
|
||||
children={field => (
|
||||
<field.FileUploaderField
|
||||
label={label}
|
||||
labelOptions={{
|
||||
tooltip,
|
||||
isRequired: required,
|
||||
showOptional,
|
||||
}}
|
||||
fileConfig={{
|
||||
allowed_file_extensions: allowedFileExtensions,
|
||||
allowed_file_types: allowedFileTypes,
|
||||
allowed_file_upload_methods: allowedFileUploadMethods,
|
||||
number_limits: 1,
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
if (type === BaseFieldType.fileList) {
|
||||
return (
|
||||
<form.AppField
|
||||
name={variable}
|
||||
children={field => (
|
||||
<field.FileUploaderField
|
||||
label={label}
|
||||
labelOptions={{
|
||||
tooltip,
|
||||
isRequired: required,
|
||||
showOptional,
|
||||
}}
|
||||
fileConfig={{
|
||||
allowed_file_extensions: allowedFileExtensions,
|
||||
allowed_file_types: allowedFileTypes,
|
||||
allowed_file_upload_methods: allowedFileUploadMethods,
|
||||
number_limits: maxLength,
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
return <></>
|
||||
},
|
||||
})
|
||||
|
||||
export default BaseField
|
||||
@@ -0,0 +1,63 @@
|
||||
import React, { useMemo } from 'react'
|
||||
import { useAppForm } from '../..'
|
||||
import BaseField from './field'
|
||||
import type { BaseFormProps } from './types'
|
||||
import { generateZodSchema } from './utils'
|
||||
|
||||
const BaseForm = ({
|
||||
initialData,
|
||||
configurations,
|
||||
onSubmit,
|
||||
CustomActions,
|
||||
}: BaseFormProps) => {
|
||||
const schema = useMemo(() => {
|
||||
const schema = generateZodSchema(configurations)
|
||||
return schema
|
||||
}, [configurations])
|
||||
|
||||
const baseForm = useAppForm({
|
||||
defaultValues: initialData,
|
||||
validators: {
|
||||
onChange: ({ value }) => {
|
||||
const result = schema.safeParse(value)
|
||||
if (!result.success) {
|
||||
const issues = result.error.issues
|
||||
const firstIssue = issues[0].message
|
||||
return firstIssue
|
||||
}
|
||||
return undefined
|
||||
},
|
||||
},
|
||||
onSubmit: ({ value }) => {
|
||||
onSubmit(value)
|
||||
},
|
||||
})
|
||||
|
||||
return (
|
||||
<form
|
||||
className='w-full'
|
||||
onSubmit={(e) => {
|
||||
e.preventDefault()
|
||||
e.stopPropagation()
|
||||
baseForm.handleSubmit()
|
||||
}}
|
||||
>
|
||||
<div className='flex flex-col gap-4 px-4 py-2'>
|
||||
{configurations.map((config, index) => {
|
||||
const FieldComponent = BaseField({
|
||||
initialData,
|
||||
config,
|
||||
})
|
||||
return <FieldComponent key={index} form={baseForm} />
|
||||
})}
|
||||
</div>
|
||||
<baseForm.AppForm>
|
||||
<baseForm.Actions
|
||||
CustomActions={CustomActions}
|
||||
/>
|
||||
</baseForm.AppForm>
|
||||
</form>
|
||||
)
|
||||
}
|
||||
|
||||
export default React.memo(BaseForm)
|
||||
@@ -0,0 +1,61 @@
|
||||
import type { TransferMethod } from '@/types/app'
|
||||
import type { Option } from '../../../select/pure'
|
||||
import type { CustomActionsProps } from '../../components/form/actions'
|
||||
|
||||
export enum BaseFieldType {
|
||||
textInput = 'text-input',
|
||||
paragraph = 'paragraph',
|
||||
numberInput = 'number-input',
|
||||
checkbox = 'checkbox',
|
||||
select = 'select',
|
||||
file = 'file',
|
||||
fileList = 'file-list',
|
||||
}
|
||||
|
||||
export type ShowCondition = {
|
||||
variable: string
|
||||
value: any
|
||||
}
|
||||
|
||||
export type NumberConfiguration = {
|
||||
max?: number
|
||||
min?: number
|
||||
unit?: string
|
||||
}
|
||||
|
||||
export type SelectConfiguration = {
|
||||
options: Option[] // Options for select field
|
||||
popupProps?: {
|
||||
wrapperClassName?: string
|
||||
className?: string
|
||||
itemClassName?: string
|
||||
title?: string
|
||||
}
|
||||
}
|
||||
|
||||
export type FileConfiguration = {
|
||||
allowedFileTypes: string[]
|
||||
allowedFileExtensions: string[]
|
||||
allowedFileUploadMethods: TransferMethod[]
|
||||
}
|
||||
|
||||
export type BaseConfiguration = {
|
||||
label: string
|
||||
variable: string // Variable name
|
||||
maxLength?: number // Max length for text input
|
||||
placeholder?: string
|
||||
required: boolean
|
||||
showOptional?: boolean // show optional label
|
||||
showConditions: ShowCondition[] // Show this field only when all conditions are met
|
||||
type: BaseFieldType
|
||||
tooltip?: string // Tooltip for this field
|
||||
} & NumberConfiguration
|
||||
& Partial<SelectConfiguration>
|
||||
& Partial<FileConfiguration>
|
||||
|
||||
export type BaseFormProps = {
|
||||
initialData?: Record<string, any>
|
||||
configurations: BaseConfiguration[]
|
||||
CustomActions?: (props: CustomActionsProps) => React.ReactNode
|
||||
onSubmit: (value: Record<string, any>) => void
|
||||
}
|
||||
@@ -0,0 +1,57 @@
|
||||
import type { ZodNumber, ZodSchema, ZodString } from 'zod'
|
||||
import { z } from 'zod'
|
||||
import { type BaseConfiguration, BaseFieldType } from './types'
|
||||
|
||||
export const generateZodSchema = (fields: BaseConfiguration[]) => {
|
||||
const shape: Record<string, ZodSchema> = {}
|
||||
|
||||
fields.forEach((field) => {
|
||||
let zodType
|
||||
|
||||
switch (field.type) {
|
||||
case BaseFieldType.textInput:
|
||||
case BaseFieldType.paragraph:
|
||||
zodType = z.string()
|
||||
break
|
||||
case BaseFieldType.numberInput:
|
||||
zodType = z.number()
|
||||
break
|
||||
case BaseFieldType.checkbox:
|
||||
zodType = z.boolean()
|
||||
break
|
||||
case BaseFieldType.select:
|
||||
zodType = z.string()
|
||||
break
|
||||
default:
|
||||
zodType = z.any()
|
||||
break
|
||||
}
|
||||
|
||||
if (field.maxLength) {
|
||||
if ([BaseFieldType.textInput, BaseFieldType.paragraph].includes(field.type))
|
||||
zodType = (zodType as ZodString).max(field.maxLength, `${field.label} exceeds max length of ${field.maxLength}`)
|
||||
}
|
||||
|
||||
if (field.min) {
|
||||
if ([BaseFieldType.numberInput].includes(field.type))
|
||||
zodType = (zodType as ZodNumber).min(field.min, `${field.label} must be at least ${field.min}`)
|
||||
}
|
||||
|
||||
if (field.max) {
|
||||
if ([BaseFieldType.numberInput].includes(field.type))
|
||||
zodType = (zodType as ZodNumber).max(field.max, `${field.label} exceeds max value of ${field.max}`)
|
||||
}
|
||||
|
||||
if (field.required) {
|
||||
if ([BaseFieldType.textInput, BaseFieldType.paragraph].includes(field.type))
|
||||
zodType = (zodType as ZodString).nonempty(`${field.label} is required`)
|
||||
}
|
||||
else {
|
||||
zodType = zodType.optional().nullable()
|
||||
}
|
||||
|
||||
shape[field.variable] = zodType
|
||||
})
|
||||
|
||||
return z.object(shape)
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
import { withForm } from '../..'
|
||||
import { demoFormOpts } from './shared-options'
|
||||
import { ContactMethods } from './types'
|
||||
|
||||
const ContactFields = withForm({
|
||||
...demoFormOpts,
|
||||
render: ({ form }) => {
|
||||
return (
|
||||
<div className='my-2'>
|
||||
<h3 className='title-lg-bold text-text-primary'>Contacts</h3>
|
||||
<div className='flex flex-col gap-4'>
|
||||
<form.AppField
|
||||
name='contact.email'
|
||||
children={field => <field.TextField label='Email' />}
|
||||
/>
|
||||
<form.AppField
|
||||
name='contact.phone'
|
||||
children={field => <field.TextField label='Phone' />}
|
||||
/>
|
||||
<form.AppField
|
||||
name='contact.preferredContactMethod'
|
||||
children={field => (
|
||||
<field.SelectField
|
||||
label='Preferred Contact Method'
|
||||
options={ContactMethods}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
},
|
||||
})
|
||||
|
||||
export default ContactFields
|
||||
@@ -0,0 +1,68 @@
|
||||
import { useStore } from '@tanstack/react-form'
|
||||
import { useAppForm } from '../..'
|
||||
import ContactFields from './contact-fields'
|
||||
import { demoFormOpts } from './shared-options'
|
||||
import { UserSchema } from './types'
|
||||
|
||||
const DemoForm = () => {
|
||||
const form = useAppForm({
|
||||
...demoFormOpts,
|
||||
validators: {
|
||||
onSubmit: ({ value }) => {
|
||||
// Validate the entire form
|
||||
const result = UserSchema.safeParse(value)
|
||||
if (!result.success) {
|
||||
const issues = result.error.issues
|
||||
console.log('Validation errors:', issues)
|
||||
return issues[0].message
|
||||
}
|
||||
return undefined
|
||||
},
|
||||
},
|
||||
onSubmit: ({ value }) => {
|
||||
console.log('Form submitted:', value)
|
||||
},
|
||||
})
|
||||
|
||||
const name = useStore(form.store, state => state.values.name)
|
||||
|
||||
return (
|
||||
<form
|
||||
className='flex w-[400px] flex-col gap-4'
|
||||
onSubmit={(e) => {
|
||||
e.preventDefault()
|
||||
e.stopPropagation()
|
||||
form.handleSubmit()
|
||||
}}
|
||||
>
|
||||
<form.AppField
|
||||
name='name'
|
||||
children={field => (
|
||||
<field.TextField label='Name' />
|
||||
)}
|
||||
/>
|
||||
<form.AppField
|
||||
name='surname'
|
||||
children={field => (
|
||||
<field.TextField label='Surname' />
|
||||
)}
|
||||
/>
|
||||
<form.AppField
|
||||
name='isAcceptingTerms'
|
||||
children={field => (
|
||||
<field.CheckboxField label='I accept the terms and conditions.' />
|
||||
)}
|
||||
/>
|
||||
{
|
||||
!!name && (
|
||||
<ContactFields form={form} />
|
||||
)
|
||||
}
|
||||
<form.AppForm>
|
||||
<form.Actions />
|
||||
</form.AppForm>
|
||||
</form>
|
||||
)
|
||||
}
|
||||
|
||||
export default DemoForm
|
||||
@@ -0,0 +1,14 @@
|
||||
import { formOptions } from '@tanstack/react-form'
|
||||
|
||||
export const demoFormOpts = formOptions({
|
||||
defaultValues: {
|
||||
name: '',
|
||||
surname: '',
|
||||
isAcceptingTerms: false,
|
||||
contact: {
|
||||
email: '',
|
||||
phone: '',
|
||||
preferredContactMethod: 'email',
|
||||
},
|
||||
},
|
||||
})
|
||||
@@ -0,0 +1,34 @@
|
||||
import { z } from 'zod'
|
||||
|
||||
const ContactMethod = z.union([
|
||||
z.literal('email'),
|
||||
z.literal('phone'),
|
||||
z.literal('whatsapp'),
|
||||
z.literal('sms'),
|
||||
])
|
||||
|
||||
export const ContactMethods = ContactMethod.options.map(({ value }) => ({
|
||||
value,
|
||||
label: value.charAt(0).toUpperCase() + value.slice(1),
|
||||
}))
|
||||
|
||||
export const UserSchema = z.object({
|
||||
name: z
|
||||
.string()
|
||||
.regex(/^[A-Z]/, 'Name must start with a capital letter')
|
||||
.min(3, 'Name must be at least 3 characters long'),
|
||||
surname: z
|
||||
.string()
|
||||
.min(3, 'Surname must be at least 3 characters long')
|
||||
.regex(/^[A-Z]/, 'Surname must start with a capital letter'),
|
||||
isAcceptingTerms: z.boolean().refine(val => val, {
|
||||
message: 'You must accept the terms and conditions',
|
||||
}),
|
||||
contact: z.object({
|
||||
email: z.string().email('Invalid email address'),
|
||||
phone: z.string().optional(),
|
||||
preferredContactMethod: ContactMethod,
|
||||
}),
|
||||
})
|
||||
|
||||
export type User = z.infer<typeof UserSchema>
|
||||
@@ -0,0 +1,222 @@
|
||||
import React from 'react'
|
||||
import { type InputFieldConfiguration, InputFieldType } from './types'
|
||||
import { withForm } from '../..'
|
||||
import { useStore } from '@tanstack/react-form'
|
||||
|
||||
type InputFieldProps = {
|
||||
initialData?: Record<string, any>
|
||||
config: InputFieldConfiguration
|
||||
}
|
||||
|
||||
const InputField = ({
|
||||
initialData,
|
||||
config,
|
||||
}: InputFieldProps) => withForm({
|
||||
defaultValues: initialData,
|
||||
render: function Render({
|
||||
form,
|
||||
}) {
|
||||
const {
|
||||
type,
|
||||
label,
|
||||
placeholder,
|
||||
variable,
|
||||
tooltip,
|
||||
showConditions,
|
||||
max,
|
||||
min,
|
||||
required,
|
||||
showOptional,
|
||||
supportFile,
|
||||
description,
|
||||
options,
|
||||
listeners,
|
||||
popupProps,
|
||||
} = config
|
||||
|
||||
const isAllConditionsMet = useStore(form.store, (state) => {
|
||||
const fieldValues = state.values
|
||||
return showConditions.every((condition) => {
|
||||
const { variable, value } = condition
|
||||
const fieldValue = fieldValues[variable as keyof typeof fieldValues]
|
||||
return fieldValue === value
|
||||
})
|
||||
})
|
||||
|
||||
if (!isAllConditionsMet)
|
||||
return <></>
|
||||
|
||||
if (type === InputFieldType.textInput) {
|
||||
return (
|
||||
<form.AppField
|
||||
name={variable}
|
||||
listeners={listeners}
|
||||
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!}
|
||||
popupProps={popupProps}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
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
|
||||
@@ -0,0 +1,39 @@
|
||||
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 InputTypeSelectConfiguration = {
|
||||
supportFile: boolean
|
||||
}
|
||||
|
||||
export type NumberSliderConfiguration = {
|
||||
description: string
|
||||
max?: number
|
||||
min?: number
|
||||
}
|
||||
|
||||
export type InputFieldConfiguration = {
|
||||
label: string
|
||||
variable: string // Variable name
|
||||
maxLength?: number // Max length for text input
|
||||
placeholder?: string
|
||||
required: boolean
|
||||
showOptional?: boolean // show optional label
|
||||
showConditions: ShowCondition[] // Show this field only when all conditions are met
|
||||
type: InputFieldType
|
||||
tooltip?: string // Tooltip for this field
|
||||
listeners?: FieldListeners<Record<string, any>, DeepKeys<Record<string, any>>> // Listener for this field
|
||||
} & NumberConfiguration & Partial<InputTypeSelectConfiguration>
|
||||
& Partial<NumberSliderConfiguration>
|
||||
& Partial<SelectConfiguration>
|
||||
@@ -0,0 +1,75 @@
|
||||
import type { ZodSchema, ZodString } from 'zod'
|
||||
import { z } from 'zod'
|
||||
import { type InputFieldConfiguration, InputFieldType } from './types'
|
||||
import { SupportedFileTypes, TransferMethod } from '@/app/components/rag-pipeline/components/panel/input-field/editor/form/schema'
|
||||
|
||||
export const generateZodSchema = (fields: InputFieldConfiguration[]) => {
|
||||
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.options:
|
||||
zodType = z.array(z.string())
|
||||
break
|
||||
case InputFieldType.select:
|
||||
zodType = z.string()
|
||||
break
|
||||
case InputFieldType.fileTypes:
|
||||
zodType = z.object({
|
||||
allowedFileExtensions: z.string().optional(),
|
||||
allowedFileTypes: z.array(SupportedFileTypes),
|
||||
})
|
||||
break
|
||||
case InputFieldType.inputTypeSelect:
|
||||
zodType = z.string()
|
||||
break
|
||||
case InputFieldType.uploadMethod:
|
||||
zodType = z.array(TransferMethod)
|
||||
break
|
||||
default:
|
||||
zodType = z.any()
|
||||
break
|
||||
}
|
||||
|
||||
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}`)
|
||||
}
|
||||
|
||||
if (field.required) {
|
||||
if ([InputFieldType.textInput].includes(field.type))
|
||||
zodType = (zodType as ZodString).nonempty(`${field.label} is required`)
|
||||
}
|
||||
else {
|
||||
zodType = zodType.optional()
|
||||
}
|
||||
|
||||
shape[field.variable] = zodType
|
||||
})
|
||||
|
||||
return z.object(shape)
|
||||
}
|
||||
@@ -0,0 +1,239 @@
|
||||
import React from 'react'
|
||||
import { type InputFieldConfiguration, InputFieldType } from './types'
|
||||
import { withForm } from '../..'
|
||||
import { useStore } from '@tanstack/react-form'
|
||||
|
||||
type InputFieldProps = {
|
||||
initialData?: Record<string, any>
|
||||
config: InputFieldConfiguration
|
||||
}
|
||||
|
||||
const NodePanelField = ({
|
||||
initialData,
|
||||
config,
|
||||
}: InputFieldProps) => withForm({
|
||||
defaultValues: initialData,
|
||||
render: function Render({
|
||||
form,
|
||||
}) {
|
||||
const {
|
||||
type,
|
||||
label,
|
||||
placeholder,
|
||||
variable,
|
||||
tooltip,
|
||||
showConditions,
|
||||
max,
|
||||
min,
|
||||
required,
|
||||
showOptional,
|
||||
supportFile,
|
||||
description,
|
||||
options,
|
||||
listeners,
|
||||
popupProps,
|
||||
} = config
|
||||
|
||||
const isAllConditionsMet = useStore(form.store, (state) => {
|
||||
const fieldValues = state.values
|
||||
return showConditions.every((condition) => {
|
||||
const { variable, value } = condition
|
||||
const fieldValue = fieldValues[variable as keyof typeof fieldValues]
|
||||
return fieldValue === value
|
||||
})
|
||||
})
|
||||
|
||||
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!}
|
||||
popupProps={popupProps}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
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,
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
if (type === InputFieldType.variableOrConstant) {
|
||||
return (
|
||||
<form.AppField
|
||||
name={variable}
|
||||
children={field => (
|
||||
<field.VariableOrConstantInputField
|
||||
label={label}
|
||||
labelOptions={{
|
||||
tooltip,
|
||||
isRequired: required,
|
||||
showOptional,
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
return <></>
|
||||
},
|
||||
})
|
||||
|
||||
export default NodePanelField
|
||||
@@ -0,0 +1,40 @@
|
||||
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',
|
||||
variableOrConstant = 'variableOrConstant',
|
||||
}
|
||||
|
||||
export type InputTypeSelectConfiguration = {
|
||||
supportFile: boolean
|
||||
}
|
||||
|
||||
export type NumberSliderConfiguration = {
|
||||
description: string
|
||||
max?: number
|
||||
min?: number
|
||||
}
|
||||
|
||||
export type InputFieldConfiguration = {
|
||||
label: string
|
||||
variable: string // Variable name
|
||||
maxLength?: number // Max length for text input
|
||||
placeholder?: string
|
||||
required: boolean
|
||||
showOptional?: boolean // show optional label
|
||||
showConditions: ShowCondition[] // Show this field only when all conditions are met
|
||||
type: InputFieldType
|
||||
tooltip?: string // Tooltip for this field
|
||||
listeners?: FieldListeners<Record<string, any>, DeepKeys<Record<string, any>>> // Listener for this field
|
||||
} & NumberConfiguration & Partial<InputTypeSelectConfiguration>
|
||||
& Partial<NumberSliderConfiguration>
|
||||
& Partial<SelectConfiguration>
|
||||
Reference in New Issue
Block a user