feat: introduce trigger functionality (#27644)
Signed-off-by: lyzno1 <yuanyouhuilyz@gmail.com> Co-authored-by: Stream <Stream_2@qq.com> Co-authored-by: lyzno1 <92089059+lyzno1@users.noreply.github.com> Co-authored-by: zhsama <torvalds@linux.do> Co-authored-by: Harry <xh001x@hotmail.com> Co-authored-by: lyzno1 <yuanyouhuilyz@gmail.com> Co-authored-by: yessenia <yessenia.contact@gmail.com> Co-authored-by: hjlarry <hjlarry@163.com> Co-authored-by: gemini-code-assist[bot] <176961590+gemini-code-assist[bot]@users.noreply.github.com> Co-authored-by: Copilot Autofix powered by AI <62310815+github-advanced-security[bot]@users.noreply.github.com> Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com> Co-authored-by: WTW0313 <twwu@dify.ai> Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
This commit is contained in:
@@ -1,20 +1,71 @@
|
||||
import CheckboxList from '@/app/components/base/checkbox-list'
|
||||
import type { FieldState, FormSchema, TypeWithI18N } from '@/app/components/base/form/types'
|
||||
import { FormItemValidateStatusEnum, FormTypeEnum } from '@/app/components/base/form/types'
|
||||
import Input from '@/app/components/base/input'
|
||||
import Radio from '@/app/components/base/radio'
|
||||
import RadioE from '@/app/components/base/radio/ui'
|
||||
import PureSelect from '@/app/components/base/select/pure'
|
||||
import Tooltip from '@/app/components/base/tooltip'
|
||||
import { useRenderI18nObject } from '@/hooks/use-i18n'
|
||||
import { useTriggerPluginDynamicOptions } from '@/service/use-triggers'
|
||||
import cn from '@/utils/classnames'
|
||||
import { RiExternalLinkLine } from '@remixicon/react'
|
||||
import type { AnyFieldApi } from '@tanstack/react-form'
|
||||
import { useStore } from '@tanstack/react-form'
|
||||
import {
|
||||
isValidElement,
|
||||
memo,
|
||||
useCallback,
|
||||
useMemo,
|
||||
} from 'react'
|
||||
import { RiExternalLinkLine } from '@remixicon/react'
|
||||
import type { AnyFieldApi } from '@tanstack/react-form'
|
||||
import { useStore } from '@tanstack/react-form'
|
||||
import cn from '@/utils/classnames'
|
||||
import Input from '@/app/components/base/input'
|
||||
import PureSelect from '@/app/components/base/select/pure'
|
||||
import type { FormSchema } from '@/app/components/base/form/types'
|
||||
import { FormTypeEnum } from '@/app/components/base/form/types'
|
||||
import { useRenderI18nObject } from '@/hooks/use-i18n'
|
||||
import Radio from '@/app/components/base/radio'
|
||||
import RadioE from '@/app/components/base/radio/ui'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
|
||||
const getExtraProps = (type: FormTypeEnum) => {
|
||||
switch (type) {
|
||||
case FormTypeEnum.secretInput:
|
||||
return { type: 'password', autoComplete: 'new-password' }
|
||||
case FormTypeEnum.textNumber:
|
||||
return { type: 'number' }
|
||||
default:
|
||||
return { type: 'text' }
|
||||
}
|
||||
}
|
||||
|
||||
const getTranslatedContent = ({ content, render }: {
|
||||
content: React.ReactNode | string | null | undefined | TypeWithI18N<string> | Record<string, string>
|
||||
render: (content: TypeWithI18N<string> | Record<string, string>) => string
|
||||
}): string => {
|
||||
if (isValidElement(content) || typeof content === 'string')
|
||||
return content as string
|
||||
|
||||
if (typeof content === 'object' && content !== null)
|
||||
return render(content as TypeWithI18N<string>)
|
||||
|
||||
return ''
|
||||
}
|
||||
|
||||
const VALIDATE_STATUS_STYLE_MAP: Record<FormItemValidateStatusEnum, { componentClassName: string, textClassName: string, infoFieldName: string }> = {
|
||||
[FormItemValidateStatusEnum.Error]: {
|
||||
componentClassName: 'border-components-input-border-destructive focus:border-components-input-border-destructive',
|
||||
textClassName: 'text-text-destructive',
|
||||
infoFieldName: 'errors',
|
||||
},
|
||||
[FormItemValidateStatusEnum.Warning]: {
|
||||
componentClassName: 'border-components-input-border-warning focus:border-components-input-border-warning',
|
||||
textClassName: 'text-text-warning',
|
||||
infoFieldName: 'warnings',
|
||||
},
|
||||
[FormItemValidateStatusEnum.Success]: {
|
||||
componentClassName: '',
|
||||
textClassName: '',
|
||||
infoFieldName: '',
|
||||
},
|
||||
[FormItemValidateStatusEnum.Validating]: {
|
||||
componentClassName: '',
|
||||
textClassName: '',
|
||||
infoFieldName: '',
|
||||
},
|
||||
}
|
||||
|
||||
export type BaseFieldProps = {
|
||||
fieldClassName?: string
|
||||
@@ -25,7 +76,9 @@ export type BaseFieldProps = {
|
||||
field: AnyFieldApi
|
||||
disabled?: boolean
|
||||
onChange?: (field: string, value: any) => void
|
||||
fieldState?: FieldState
|
||||
}
|
||||
|
||||
const BaseField = ({
|
||||
fieldClassName,
|
||||
labelClassName,
|
||||
@@ -35,204 +88,259 @@ const BaseField = ({
|
||||
field,
|
||||
disabled: propsDisabled,
|
||||
onChange,
|
||||
fieldState,
|
||||
}: BaseFieldProps) => {
|
||||
const renderI18nObject = useRenderI18nObject()
|
||||
const { t } = useTranslation()
|
||||
const {
|
||||
name,
|
||||
label,
|
||||
required,
|
||||
placeholder,
|
||||
options,
|
||||
labelClassName: formLabelClassName,
|
||||
disabled: formSchemaDisabled,
|
||||
type: formItemType,
|
||||
dynamicSelectParams,
|
||||
multiple = false,
|
||||
tooltip,
|
||||
showCopy,
|
||||
description,
|
||||
url,
|
||||
help,
|
||||
} = formSchema
|
||||
const disabled = propsDisabled || formSchemaDisabled
|
||||
|
||||
const memorizedLabel = useMemo(() => {
|
||||
if (isValidElement(label))
|
||||
return label
|
||||
const [translatedLabel, translatedPlaceholder, translatedTooltip, translatedDescription, translatedHelp] = useMemo(() => {
|
||||
const results = [
|
||||
label,
|
||||
placeholder,
|
||||
tooltip,
|
||||
description,
|
||||
help,
|
||||
].map(v => getTranslatedContent({ content: v, render: renderI18nObject }))
|
||||
if (!results[1]) results[1] = t('common.placeholder.input')
|
||||
return results
|
||||
}, [label, placeholder, tooltip, description, help, renderI18nObject])
|
||||
|
||||
if (typeof label === 'string')
|
||||
return label
|
||||
const watchedVariables = useMemo(() => {
|
||||
const variables = new Set<string>()
|
||||
|
||||
if (typeof label === 'object' && label !== null)
|
||||
return renderI18nObject(label as Record<string, string>)
|
||||
}, [label, renderI18nObject])
|
||||
const memorizedPlaceholder = useMemo(() => {
|
||||
if (typeof placeholder === 'string')
|
||||
return placeholder
|
||||
for (const option of options || []) {
|
||||
for (const condition of option.show_on || [])
|
||||
variables.add(condition.variable)
|
||||
}
|
||||
|
||||
if (typeof placeholder === 'object' && placeholder !== null)
|
||||
return renderI18nObject(placeholder as Record<string, string>)
|
||||
}, [placeholder, renderI18nObject])
|
||||
const optionValues = useStore(field.form.store, (s) => {
|
||||
return Array.from(variables)
|
||||
}, [options])
|
||||
|
||||
const watchedValues = useStore(field.form.store, (s) => {
|
||||
const result: Record<string, any> = {}
|
||||
options?.forEach((option) => {
|
||||
if (option.show_on?.length) {
|
||||
option.show_on.forEach((condition) => {
|
||||
result[condition.variable] = s.values[condition.variable]
|
||||
})
|
||||
}
|
||||
})
|
||||
for (const variable of watchedVariables)
|
||||
result[variable] = s.values[variable]
|
||||
|
||||
return result
|
||||
})
|
||||
|
||||
const memorizedOptions = useMemo(() => {
|
||||
return options?.filter((option) => {
|
||||
if (!option.show_on || option.show_on.length === 0)
|
||||
if (!option.show_on?.length)
|
||||
return true
|
||||
|
||||
return option.show_on.every((condition) => {
|
||||
const conditionValue = optionValues[condition.variable]
|
||||
return conditionValue === condition.value
|
||||
return watchedValues[condition.variable] === condition.value
|
||||
})
|
||||
}).map((option) => {
|
||||
return {
|
||||
label: typeof option.label === 'string' ? option.label : renderI18nObject(option.label),
|
||||
label: getTranslatedContent({ content: option.label, render: renderI18nObject }),
|
||||
value: option.value,
|
||||
}
|
||||
}) || []
|
||||
}, [options, renderI18nObject, optionValues])
|
||||
}, [options, renderI18nObject, watchedValues])
|
||||
|
||||
const value = useStore(field.form.store, s => s.values[field.name])
|
||||
|
||||
const { data: dynamicOptionsData, isLoading: isDynamicOptionsLoading, error: dynamicOptionsError } = useTriggerPluginDynamicOptions(
|
||||
dynamicSelectParams || {
|
||||
plugin_id: '',
|
||||
provider: '',
|
||||
action: '',
|
||||
parameter: '',
|
||||
credential_id: '',
|
||||
},
|
||||
formItemType === FormTypeEnum.dynamicSelect,
|
||||
)
|
||||
|
||||
const dynamicOptions = useMemo(() => {
|
||||
if (!dynamicOptionsData?.options)
|
||||
return []
|
||||
return dynamicOptionsData.options.map(option => ({
|
||||
label: getTranslatedContent({ content: option.label, render: renderI18nObject }),
|
||||
value: option.value,
|
||||
}))
|
||||
}, [dynamicOptionsData, renderI18nObject])
|
||||
|
||||
const handleChange = useCallback((value: any) => {
|
||||
field.handleChange(value)
|
||||
onChange?.(field.name, value)
|
||||
}, [field, onChange])
|
||||
|
||||
return (
|
||||
<div className={cn(fieldClassName)}>
|
||||
<div className={cn(labelClassName, formLabelClassName)}>
|
||||
{memorizedLabel}
|
||||
{
|
||||
required && !isValidElement(label) && (
|
||||
<span className='ml-1 text-text-destructive-secondary'>*</span>
|
||||
)
|
||||
}
|
||||
</div>
|
||||
<div className={cn(inputContainerClassName)}>
|
||||
{
|
||||
formSchema.type === FormTypeEnum.textInput && (
|
||||
<Input
|
||||
id={field.name}
|
||||
name={field.name}
|
||||
className={cn(inputClassName)}
|
||||
value={value || ''}
|
||||
onChange={(e) => {
|
||||
handleChange(e.target.value)
|
||||
}}
|
||||
onBlur={field.handleBlur}
|
||||
disabled={disabled}
|
||||
placeholder={memorizedPlaceholder}
|
||||
<>
|
||||
<div className={cn(fieldClassName)}>
|
||||
<div className={cn(labelClassName, formLabelClassName)}>
|
||||
{translatedLabel}
|
||||
{
|
||||
required && !isValidElement(label) && (
|
||||
<span className='ml-1 text-text-destructive-secondary'>*</span>
|
||||
)
|
||||
}
|
||||
{tooltip && (
|
||||
<Tooltip
|
||||
popupContent={<div className='w-[200px]'>{translatedTooltip}</div>}
|
||||
triggerClassName='ml-0.5 w-4 h-4'
|
||||
/>
|
||||
)
|
||||
}
|
||||
{
|
||||
formSchema.type === FormTypeEnum.secretInput && (
|
||||
<Input
|
||||
id={field.name}
|
||||
name={field.name}
|
||||
type='password'
|
||||
className={cn(inputClassName)}
|
||||
value={value || ''}
|
||||
onChange={e => handleChange(e.target.value)}
|
||||
onBlur={field.handleBlur}
|
||||
disabled={disabled}
|
||||
placeholder={memorizedPlaceholder}
|
||||
autoComplete={'new-password'}
|
||||
/>
|
||||
)
|
||||
}
|
||||
{
|
||||
formSchema.type === FormTypeEnum.textNumber && (
|
||||
<Input
|
||||
id={field.name}
|
||||
name={field.name}
|
||||
type='number'
|
||||
className={cn(inputClassName)}
|
||||
value={value || ''}
|
||||
onChange={e => handleChange(e.target.value)}
|
||||
onBlur={field.handleBlur}
|
||||
disabled={disabled}
|
||||
placeholder={memorizedPlaceholder}
|
||||
/>
|
||||
)
|
||||
}
|
||||
{
|
||||
formSchema.type === FormTypeEnum.select && (
|
||||
<PureSelect
|
||||
value={value}
|
||||
onChange={v => handleChange(v)}
|
||||
disabled={disabled}
|
||||
placeholder={memorizedPlaceholder}
|
||||
options={memorizedOptions}
|
||||
triggerPopupSameWidth
|
||||
popupProps={{
|
||||
className: 'max-h-[320px] overflow-y-auto',
|
||||
}}
|
||||
/>
|
||||
)
|
||||
}
|
||||
{
|
||||
formSchema.type === FormTypeEnum.radio && (
|
||||
)}
|
||||
</div>
|
||||
<div className={cn(inputContainerClassName)}>
|
||||
{
|
||||
[FormTypeEnum.textInput, FormTypeEnum.secretInput, FormTypeEnum.textNumber].includes(formItemType) && (
|
||||
<Input
|
||||
id={field.name}
|
||||
name={field.name}
|
||||
className={cn(inputClassName, VALIDATE_STATUS_STYLE_MAP[fieldState?.validateStatus as FormItemValidateStatusEnum]?.componentClassName)}
|
||||
value={value || ''}
|
||||
onChange={(e) => {
|
||||
handleChange(e.target.value)
|
||||
}}
|
||||
onBlur={field.handleBlur}
|
||||
disabled={disabled}
|
||||
placeholder={translatedPlaceholder}
|
||||
{...getExtraProps(formItemType)}
|
||||
showCopyIcon={showCopy}
|
||||
/>
|
||||
)
|
||||
}
|
||||
{
|
||||
formItemType === FormTypeEnum.select && !multiple && (
|
||||
<PureSelect
|
||||
value={value}
|
||||
onChange={v => handleChange(v)}
|
||||
disabled={disabled}
|
||||
placeholder={translatedPlaceholder}
|
||||
options={memorizedOptions}
|
||||
triggerPopupSameWidth
|
||||
popupProps={{
|
||||
className: 'max-h-[320px] overflow-y-auto',
|
||||
}}
|
||||
/>
|
||||
)
|
||||
}
|
||||
{
|
||||
formItemType === FormTypeEnum.checkbox /* && multiple */ && (
|
||||
<CheckboxList
|
||||
title={name}
|
||||
value={value}
|
||||
onChange={v => field.handleChange(v)}
|
||||
options={memorizedOptions}
|
||||
maxHeight='200px'
|
||||
/>
|
||||
)
|
||||
}
|
||||
{
|
||||
formItemType === FormTypeEnum.dynamicSelect && (
|
||||
<PureSelect
|
||||
options={dynamicOptions}
|
||||
value={value}
|
||||
onChange={field.handleChange}
|
||||
disabled={disabled || isDynamicOptionsLoading}
|
||||
placeholder={
|
||||
isDynamicOptionsLoading
|
||||
? t('common.dynamicSelect.loading')
|
||||
: translatedPlaceholder
|
||||
}
|
||||
{...(dynamicOptionsError ? { popupProps: { title: t('common.dynamicSelect.error'), titleClassName: 'text-text-destructive-secondary' } }
|
||||
: (!dynamicOptions.length ? { popupProps: { title: t('common.dynamicSelect.noData') } } : {}))}
|
||||
triggerPopupSameWidth
|
||||
multiple={multiple}
|
||||
/>
|
||||
)
|
||||
}
|
||||
{
|
||||
formItemType === FormTypeEnum.radio && (
|
||||
<div className={cn(
|
||||
memorizedOptions.length < 3 ? 'flex items-center space-x-2' : 'space-y-2',
|
||||
)}>
|
||||
{
|
||||
memorizedOptions.map(option => (
|
||||
<div
|
||||
key={option.value}
|
||||
className={cn(
|
||||
'system-sm-regular hover:bg-components-option-card-option-hover-bg hover:border-components-option-card-option-hover-border flex h-8 flex-[1] grow cursor-pointer items-center justify-center gap-2 rounded-lg border border-components-option-card-option-border bg-components-option-card-option-bg p-2 text-text-secondary',
|
||||
value === option.value && 'border-components-option-card-option-selected-border bg-components-option-card-option-selected-bg text-text-primary shadow-xs',
|
||||
disabled && 'cursor-not-allowed opacity-50',
|
||||
inputClassName,
|
||||
)}
|
||||
onClick={() => !disabled && handleChange(option.value)}
|
||||
>
|
||||
{
|
||||
formSchema.showRadioUI && (
|
||||
<RadioE
|
||||
className='mr-2'
|
||||
isChecked={value === option.value}
|
||||
/>
|
||||
)
|
||||
}
|
||||
{option.label}
|
||||
</div>
|
||||
))
|
||||
}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
{
|
||||
formItemType === FormTypeEnum.boolean && (
|
||||
<Radio.Group
|
||||
className='flex w-fit items-center'
|
||||
value={value}
|
||||
onChange={v => field.handleChange(v)}
|
||||
>
|
||||
<Radio value={true} className='!mr-1'>True</Radio>
|
||||
<Radio value={false}>False</Radio>
|
||||
</Radio.Group>
|
||||
)
|
||||
}
|
||||
{fieldState?.validateStatus && [FormItemValidateStatusEnum.Error, FormItemValidateStatusEnum.Warning].includes(fieldState?.validateStatus) && (
|
||||
<div className={cn(
|
||||
memorizedOptions.length < 3 ? 'flex items-center space-x-2' : 'space-y-2',
|
||||
'system-xs-regular mt-1 px-0 py-[2px]',
|
||||
VALIDATE_STATUS_STYLE_MAP[fieldState?.validateStatus].textClassName,
|
||||
)}>
|
||||
{
|
||||
memorizedOptions.map(option => (
|
||||
<div
|
||||
key={option.value}
|
||||
className={cn(
|
||||
'system-sm-regular hover:bg-components-option-card-option-hover-bg hover:border-components-option-card-option-hover-border flex h-8 flex-[1] grow cursor-pointer items-center justify-center rounded-lg border border-components-option-card-option-border bg-components-option-card-option-bg p-2 text-text-secondary',
|
||||
value === option.value && 'border-components-option-card-option-selected-border bg-components-option-card-option-selected-bg text-text-primary shadow-xs',
|
||||
disabled && 'cursor-not-allowed opacity-50',
|
||||
inputClassName,
|
||||
)}
|
||||
onClick={() => !disabled && handleChange(option.value)}
|
||||
>
|
||||
{
|
||||
formSchema.showRadioUI && (
|
||||
<RadioE
|
||||
className='mr-2'
|
||||
isChecked={value === option.value}
|
||||
/>
|
||||
)
|
||||
}
|
||||
{option.label}
|
||||
</div>
|
||||
))
|
||||
}
|
||||
{fieldState?.[VALIDATE_STATUS_STYLE_MAP[fieldState?.validateStatus].infoFieldName as keyof FieldState]}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
{
|
||||
formSchema.type === FormTypeEnum.boolean && (
|
||||
<Radio.Group
|
||||
className='flex w-fit items-center'
|
||||
value={value}
|
||||
onChange={v => field.handleChange(v)}
|
||||
>
|
||||
<Radio value={true} className='!mr-1'>True</Radio>
|
||||
<Radio value={false}>False</Radio>
|
||||
</Radio.Group>
|
||||
)
|
||||
}
|
||||
{
|
||||
formSchema.url && (
|
||||
<a
|
||||
className='system-xs-regular mt-4 flex items-center text-text-accent'
|
||||
href={formSchema?.url}
|
||||
target='_blank'
|
||||
>
|
||||
<span className='break-all'>
|
||||
{renderI18nObject(formSchema?.help as any)}
|
||||
</span>
|
||||
{
|
||||
<RiExternalLinkLine className='ml-1 h-3 w-3' />
|
||||
}
|
||||
</a>
|
||||
)
|
||||
}
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{description && (
|
||||
<div className='system-xs-regular mt-4 text-text-tertiary'>
|
||||
{translatedDescription}
|
||||
</div>
|
||||
)}
|
||||
{
|
||||
url && (
|
||||
<a
|
||||
className='system-xs-regular mt-4 flex items-center text-text-accent'
|
||||
href={url}
|
||||
target='_blank'
|
||||
>
|
||||
<span className='break-all'>
|
||||
{translatedHelp}
|
||||
</span>
|
||||
<RiExternalLinkLine className='ml-1 h-3 w-3 shrink-0' />
|
||||
</a>
|
||||
)
|
||||
}
|
||||
</>
|
||||
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@@ -3,6 +3,7 @@ import {
|
||||
useCallback,
|
||||
useImperativeHandle,
|
||||
useMemo,
|
||||
useState,
|
||||
} from 'react'
|
||||
import type {
|
||||
AnyFieldApi,
|
||||
@@ -12,9 +13,12 @@ import {
|
||||
useForm,
|
||||
useStore,
|
||||
} from '@tanstack/react-form'
|
||||
import type {
|
||||
FormRef,
|
||||
FormSchema,
|
||||
import {
|
||||
type FieldState,
|
||||
FormItemValidateStatusEnum,
|
||||
type FormRef,
|
||||
type FormSchema,
|
||||
type SetFieldsParam,
|
||||
} from '@/app/components/base/form/types'
|
||||
import {
|
||||
BaseField,
|
||||
@@ -36,6 +40,8 @@ export type BaseFormProps = {
|
||||
disabled?: boolean
|
||||
formFromProps?: AnyFormApi
|
||||
onChange?: (field: string, value: any) => void
|
||||
onSubmit?: (e: React.FormEvent<HTMLFormElement>) => void
|
||||
preventDefaultSubmit?: boolean
|
||||
} & Pick<BaseFieldProps, 'fieldClassName' | 'labelClassName' | 'inputContainerClassName' | 'inputClassName'>
|
||||
|
||||
const BaseForm = ({
|
||||
@@ -50,6 +56,8 @@ const BaseForm = ({
|
||||
disabled,
|
||||
formFromProps,
|
||||
onChange,
|
||||
onSubmit,
|
||||
preventDefaultSubmit = false,
|
||||
}: BaseFormProps) => {
|
||||
const initialDefaultValues = useMemo(() => {
|
||||
if (defaultValues)
|
||||
@@ -68,6 +76,8 @@ const BaseForm = ({
|
||||
const { getFormValues } = useGetFormValues(form, formSchemas)
|
||||
const { getValidators } = useGetValidators()
|
||||
|
||||
const [fieldStates, setFieldStates] = useState<Record<string, FieldState>>({})
|
||||
|
||||
const showOnValues = useStore(form.store, (s: any) => {
|
||||
const result: Record<string, any> = {}
|
||||
formSchemas.forEach((schema) => {
|
||||
@@ -81,6 +91,34 @@ const BaseForm = ({
|
||||
return result
|
||||
})
|
||||
|
||||
const setFields = useCallback((fields: SetFieldsParam[]) => {
|
||||
const newFieldStates: Record<string, FieldState> = { ...fieldStates }
|
||||
|
||||
for (const field of fields) {
|
||||
const { name, value, errors, warnings, validateStatus, help } = field
|
||||
|
||||
if (value !== undefined)
|
||||
form.setFieldValue(name, value)
|
||||
|
||||
let finalValidateStatus = validateStatus
|
||||
if (!finalValidateStatus) {
|
||||
if (errors && errors.length > 0)
|
||||
finalValidateStatus = FormItemValidateStatusEnum.Error
|
||||
else if (warnings && warnings.length > 0)
|
||||
finalValidateStatus = FormItemValidateStatusEnum.Warning
|
||||
}
|
||||
|
||||
newFieldStates[name] = {
|
||||
validateStatus: finalValidateStatus,
|
||||
help,
|
||||
errors,
|
||||
warnings,
|
||||
}
|
||||
}
|
||||
|
||||
setFieldStates(newFieldStates)
|
||||
}, [form, fieldStates])
|
||||
|
||||
useImperativeHandle(ref, () => {
|
||||
return {
|
||||
getForm() {
|
||||
@@ -89,8 +127,9 @@ const BaseForm = ({
|
||||
getFormValues: (option) => {
|
||||
return getFormValues(option)
|
||||
},
|
||||
setFields,
|
||||
}
|
||||
}, [form, getFormValues])
|
||||
}, [form, getFormValues, setFields])
|
||||
|
||||
const renderField = useCallback((field: AnyFieldApi) => {
|
||||
const formSchema = formSchemas?.find(schema => schema.name === field.name)
|
||||
@@ -100,18 +139,19 @@ const BaseForm = ({
|
||||
<BaseField
|
||||
field={field}
|
||||
formSchema={formSchema}
|
||||
fieldClassName={fieldClassName}
|
||||
labelClassName={labelClassName}
|
||||
fieldClassName={fieldClassName ?? formSchema.fieldClassName}
|
||||
labelClassName={labelClassName ?? formSchema.labelClassName}
|
||||
inputContainerClassName={inputContainerClassName}
|
||||
inputClassName={inputClassName}
|
||||
disabled={disabled}
|
||||
onChange={onChange}
|
||||
fieldState={fieldStates[field.name]}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
return null
|
||||
}, [formSchemas, fieldClassName, labelClassName, inputContainerClassName, inputClassName, disabled, onChange])
|
||||
}, [formSchemas, fieldClassName, labelClassName, inputContainerClassName, inputClassName, disabled, onChange, fieldStates])
|
||||
|
||||
const renderFieldWrapper = useCallback((formSchema: FormSchema) => {
|
||||
const validators = getValidators(formSchema)
|
||||
@@ -142,9 +182,18 @@ const BaseForm = ({
|
||||
if (!formSchemas?.length)
|
||||
return null
|
||||
|
||||
const handleSubmit = (e: React.FormEvent<HTMLFormElement>) => {
|
||||
if (preventDefaultSubmit) {
|
||||
e.preventDefault()
|
||||
e.stopPropagation()
|
||||
}
|
||||
onSubmit?.(e)
|
||||
}
|
||||
|
||||
return (
|
||||
<form
|
||||
className={cn(formClassName)}
|
||||
onSubmit={handleSubmit}
|
||||
>
|
||||
{formSchemas.map(renderFieldWrapper)}
|
||||
</form>
|
||||
|
||||
@@ -11,7 +11,9 @@ type SelectFieldProps = {
|
||||
options: Option[]
|
||||
onChange?: (value: string) => void
|
||||
className?: string
|
||||
} & Omit<PureSelectProps, 'options' | 'value' | 'onChange'>
|
||||
} & Omit<PureSelectProps, 'options' | 'value' | 'onChange' | 'multiple'> & {
|
||||
multiple?: false
|
||||
}
|
||||
|
||||
const SelectField = ({
|
||||
label,
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import type { ChangeEvent } from 'react'
|
||||
import { useState } from 'react'
|
||||
import { useCallback, useState } from 'react'
|
||||
import { RiEditLine } from '@remixicon/react'
|
||||
import cn from '@/utils/classnames'
|
||||
import SegmentedControl from '@/app/components/base/segmented-control'
|
||||
@@ -33,9 +33,9 @@ const VariableOrConstantInputField = ({
|
||||
},
|
||||
]
|
||||
|
||||
const handleVariableOrConstantChange = (value: string) => {
|
||||
const handleVariableOrConstantChange = useCallback((value: string) => {
|
||||
setVariableType(value)
|
||||
}
|
||||
}, [setVariableType])
|
||||
|
||||
const handleVariableValueChange = () => {
|
||||
console.log('Variable value changed')
|
||||
|
||||
Reference in New Issue
Block a user