mirror of
https://github.com/langgenius/dify.git
synced 2026-01-08 07:14:14 +00:00
feat: Implement dynamic form field rendering and replace SubmitButton with Actions component
This commit is contained in:
@@ -1,18 +1,13 @@
|
||||
import cn from '@/utils/classnames'
|
||||
import { useFieldContext } from '../..'
|
||||
import type { PureSelectProps } from '../../../select/pure'
|
||||
import type { Option, PureSelectProps } from '../../../select/pure'
|
||||
import PureSelect from '../../../select/pure'
|
||||
import Label from '../label'
|
||||
import { useCallback } from 'react'
|
||||
|
||||
type SelectOption = {
|
||||
value: string
|
||||
label: string
|
||||
}
|
||||
|
||||
type SelectFieldProps = {
|
||||
label: string
|
||||
options: SelectOption[]
|
||||
options: Option[]
|
||||
onChange?: (value: string) => void
|
||||
isRequired?: boolean
|
||||
showOptional?: boolean
|
||||
|
||||
39
web/app/components/base/form/components/form/actions.tsx
Normal file
39
web/app/components/base/form/components/form/actions.tsx
Normal file
@@ -0,0 +1,39 @@
|
||||
import { useStore } from '@tanstack/react-form'
|
||||
import type { FormType } from '../..'
|
||||
import { useFormContext } from '../..'
|
||||
import Button from '../../../button'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
|
||||
type ActionsProps = {
|
||||
CustomActions?: (form: FormType) => React.ReactNode
|
||||
}
|
||||
|
||||
const Actions = ({
|
||||
CustomActions,
|
||||
}: ActionsProps) => {
|
||||
const { t } = useTranslation()
|
||||
const form = useFormContext()
|
||||
|
||||
const [isSubmitting, canSubmit] = useStore(form.store, state => [
|
||||
state.isSubmitting,
|
||||
state.canSubmit,
|
||||
])
|
||||
|
||||
if (CustomActions)
|
||||
return CustomActions(form)
|
||||
|
||||
return (
|
||||
<div className='flex items-center justify-end p-4 pt-2'>
|
||||
<Button
|
||||
variant='primary'
|
||||
disabled = {isSubmitting || !canSubmit}
|
||||
loading={isSubmitting}
|
||||
onClick={() => form.handleSubmit()}
|
||||
>
|
||||
{t('common.operation.submit')}
|
||||
</Button>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export default Actions
|
||||
@@ -1,25 +0,0 @@
|
||||
import { useStore } from '@tanstack/react-form'
|
||||
import { useFormContext } from '../..'
|
||||
import Button, { type ButtonProps } from '../../../button'
|
||||
|
||||
type SubmitButtonProps = Omit<ButtonProps, 'disabled' | 'loading' | 'onClick'>
|
||||
|
||||
const SubmitButton = ({ ...buttonProps }: SubmitButtonProps) => {
|
||||
const form = useFormContext()
|
||||
|
||||
const [isSubmitting, canSubmit] = useStore(form.store, state => [
|
||||
state.isSubmitting,
|
||||
state.canSubmit,
|
||||
])
|
||||
|
||||
return (
|
||||
<Button
|
||||
disabled={isSubmitting || !canSubmit}
|
||||
loading={isSubmitting}
|
||||
onClick={() => form.handleSubmit()}
|
||||
{...buttonProps}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
export default SubmitButton
|
||||
102
web/app/components/base/form/form-scenarios/base/field.tsx
Normal file
102
web/app/components/base/form/form-scenarios/base/field.tsx
Normal file
@@ -0,0 +1,102 @@
|
||||
import React, { useMemo } from 'react'
|
||||
import { type BaseConfiguration, BaseVarType } from './types'
|
||||
import { withForm } from '../..'
|
||||
import { useStore } from '@tanstack/react-form'
|
||||
|
||||
type FieldProps<T> = {
|
||||
initialData?: T
|
||||
config: BaseConfiguration<T>
|
||||
}
|
||||
|
||||
const Field = <T,>({
|
||||
initialData,
|
||||
config,
|
||||
}: FieldProps<T>) => withForm({
|
||||
defaultValues: initialData,
|
||||
props: {
|
||||
config,
|
||||
},
|
||||
render: function Render({
|
||||
form,
|
||||
config,
|
||||
}) {
|
||||
const { type, label, placeholder, variable, tooltip, showConditions, max, min, options } = 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 ([BaseVarType.textInput].includes(type)) {
|
||||
return (
|
||||
<form.AppField
|
||||
name={variable}
|
||||
children={field => (
|
||||
<field.TextField
|
||||
label={label}
|
||||
tooltip={tooltip}
|
||||
placeholder={placeholder}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
if ([BaseVarType.numberInput].includes(type)) {
|
||||
return (
|
||||
<form.AppField
|
||||
name={variable}
|
||||
children={field => (
|
||||
<field.NumberInputField
|
||||
label={label}
|
||||
tooltip={tooltip}
|
||||
placeholder={placeholder}
|
||||
max={max}
|
||||
min={min}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
if ([BaseVarType.checkbox].includes(type)) {
|
||||
return (
|
||||
<form.AppField
|
||||
name={variable}
|
||||
children={field => (
|
||||
<field.CheckboxField
|
||||
label={label}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
if ([BaseVarType.select].includes(type)) {
|
||||
return (
|
||||
<form.AppField
|
||||
name={variable}
|
||||
children={field => (
|
||||
<field.SelectField
|
||||
options={options!}
|
||||
label={label}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
return <></>
|
||||
},
|
||||
})
|
||||
|
||||
export default Field
|
||||
51
web/app/components/base/form/form-scenarios/base/index.tsx
Normal file
51
web/app/components/base/form/form-scenarios/base/index.tsx
Normal file
@@ -0,0 +1,51 @@
|
||||
import React from 'react'
|
||||
import { useAppForm } from '../..'
|
||||
import Field from './field'
|
||||
import type { BaseFormProps } from './types'
|
||||
|
||||
const BaseForm = <T,>({
|
||||
initialData,
|
||||
configurations,
|
||||
onSubmit,
|
||||
CustomActions,
|
||||
}: BaseFormProps<T>) => {
|
||||
const baseForm = useAppForm({
|
||||
defaultValues: initialData,
|
||||
validators: {
|
||||
onSubmit: ({ value }) => {
|
||||
console.log('onSubmit', value)
|
||||
},
|
||||
},
|
||||
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 = Field<T>({
|
||||
initialData,
|
||||
config,
|
||||
})
|
||||
return <FieldComponent key={index} form={baseForm} config={config} />
|
||||
})}
|
||||
</div>
|
||||
<baseForm.AppForm>
|
||||
<baseForm.Actions
|
||||
CustomActions={CustomActions}
|
||||
/>
|
||||
</baseForm.AppForm>
|
||||
</form>
|
||||
)
|
||||
}
|
||||
|
||||
export default React.memo(BaseForm)
|
||||
42
web/app/components/base/form/form-scenarios/base/types.ts
Normal file
42
web/app/components/base/form/form-scenarios/base/types.ts
Normal file
@@ -0,0 +1,42 @@
|
||||
import type { DeepKeys } from '@tanstack/react-form'
|
||||
import type { FormType } from '../..'
|
||||
import type { Option } from '../../../select/pure'
|
||||
|
||||
export enum BaseVarType {
|
||||
textInput = 'textInput',
|
||||
numberInput = 'numberInput',
|
||||
checkbox = 'checkbox',
|
||||
select = 'select',
|
||||
}
|
||||
|
||||
export type ShowCondition<T> = {
|
||||
variable: DeepKeys<T>
|
||||
value: any
|
||||
}
|
||||
|
||||
export type NumberConfiguration = {
|
||||
max?: number
|
||||
min?: number
|
||||
}
|
||||
|
||||
export type SelectConfiguration = {
|
||||
options?: Option[] // Options for select field
|
||||
}
|
||||
|
||||
export type BaseConfiguration<T> = {
|
||||
label: string
|
||||
variable: DeepKeys<T> // Variable name
|
||||
maxLength?: number // Max length for text input
|
||||
placeholder?: string
|
||||
required: boolean
|
||||
showConditions: ShowCondition<T>[] // Show this field only when the all conditions are met
|
||||
type: BaseVarType
|
||||
tooltip?: string // Tooltip for this field
|
||||
} & NumberConfiguration & SelectConfiguration
|
||||
|
||||
export type BaseFormProps<T> = {
|
||||
initialData?: T
|
||||
configurations: BaseConfiguration<T>[]
|
||||
CustomActions?: (form: FormType) => React.ReactNode
|
||||
onSubmit: (value: T) => void
|
||||
}
|
||||
@@ -318,9 +318,7 @@ const InputFieldForm = ({
|
||||
{t('common.operation.cancel')}
|
||||
</Button>
|
||||
<form.AppForm>
|
||||
<form.SubmitButton variant='primary'>
|
||||
{t('common.operation.save')}
|
||||
</form.SubmitButton>
|
||||
<form.Actions />
|
||||
</form.AppForm>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
@@ -5,7 +5,9 @@ import CheckboxField from './components/field/checkbox'
|
||||
import SelectField from './components/field/select'
|
||||
import CustomSelectField from './components/field/custom-select'
|
||||
import OptionsField from './components/field/options'
|
||||
import SubmitButton from './components/form/submit-button'
|
||||
import Actions from './components/form/actions'
|
||||
|
||||
export type FormType = ReturnType<typeof useFormContext>
|
||||
|
||||
export const { fieldContext, useFieldContext, formContext, useFormContext }
|
||||
= createFormHookContexts()
|
||||
@@ -20,7 +22,7 @@ export const { useAppForm, withForm } = createFormHook({
|
||||
OptionsField,
|
||||
},
|
||||
formComponents: {
|
||||
SubmitButton,
|
||||
Actions,
|
||||
},
|
||||
fieldContext,
|
||||
formContext,
|
||||
|
||||
@@ -17,7 +17,7 @@ import type {
|
||||
} from '@/app/components/base/portal-to-follow-elem'
|
||||
import cn from '@/utils/classnames'
|
||||
|
||||
type Option = {
|
||||
export type Option = {
|
||||
label: string
|
||||
value: string
|
||||
}
|
||||
|
||||
@@ -1,19 +1,62 @@
|
||||
'use client'
|
||||
|
||||
import InputFieldForm from '../components/base/form/form-scenarios/input-field'
|
||||
// import DemoForm from '../components/base/form/form-scenarios/demo'
|
||||
import InputFieldForm from '../components/base/form/form-scenarios/base'
|
||||
import { BaseVarType } from '../components/base/form/form-scenarios/base/types'
|
||||
|
||||
export default function Page() {
|
||||
return (
|
||||
<div className='flex h-screen w-full items-center justify-center p-20'>
|
||||
<div className='w-[400px] rounded-lg border border-gray-800 bg-components-panel-bg'>
|
||||
<div className='w-[400px] rounded-lg border border-components-panel-border bg-components-panel-bg'>
|
||||
<InputFieldForm
|
||||
initialData={undefined}
|
||||
supportFile
|
||||
onCancel={() => { console.log('cancel') }}
|
||||
onSubmit={value => console.log('submit', value)}
|
||||
initialData={{
|
||||
type: 'option_1',
|
||||
variable: 'test',
|
||||
label: 'Test',
|
||||
required: true,
|
||||
}}
|
||||
configurations={[
|
||||
{
|
||||
type: BaseVarType.textInput,
|
||||
variable: 'variable',
|
||||
label: 'Variable',
|
||||
required: true,
|
||||
showConditions: [{
|
||||
variable: 'type',
|
||||
value: 'option_1',
|
||||
}],
|
||||
},
|
||||
{
|
||||
type: BaseVarType.numberInput,
|
||||
variable: 'max_length',
|
||||
label: 'Max Length',
|
||||
required: true,
|
||||
showConditions: [],
|
||||
max: 100,
|
||||
min: 1,
|
||||
},
|
||||
{
|
||||
type: BaseVarType.checkbox,
|
||||
variable: 'required',
|
||||
label: 'Required',
|
||||
required: true,
|
||||
showConditions: [],
|
||||
},
|
||||
{
|
||||
type: BaseVarType.select,
|
||||
variable: 'type',
|
||||
label: 'Type',
|
||||
required: true,
|
||||
showConditions: [],
|
||||
options: [
|
||||
{ label: 'Option 1', value: 'option_1' },
|
||||
{ label: 'Option 2', value: 'option_2' },
|
||||
{ label: 'Option 3', value: 'option_3' },
|
||||
],
|
||||
},
|
||||
]}
|
||||
onSubmit={(value) => {
|
||||
console.log('onSubmit', value)
|
||||
}}
|
||||
/>
|
||||
{/* <DemoForm /> */}
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
|
||||
Reference in New Issue
Block a user