From 2572e99a4bc93af48e33c1eca110dc4eada371c6 Mon Sep 17 00:00:00 2001 From: zxhlyh Date: Mon, 14 Jul 2025 16:42:23 +0800 Subject: [PATCH] form --- .../base/form/components/base/base-field.tsx | 6 +-- .../base/form/components/base/base-form.tsx | 33 ++++++++++++-- web/app/components/base/form/hooks/index.ts | 2 + .../base/form/hooks/use-check-validated.ts | 36 +++++++++++++++ .../base/form/hooks/use-get-form-values.ts | 45 +++++++++++++++++++ web/app/components/base/form/types.ts | 8 ++++ web/app/components/base/form/utils/index.ts | 1 + .../base/form/utils/secret-input/index.ts | 29 ++++++++++++ .../plugin-auth/authorize/api-key-modal.tsx | 41 +++++++---------- .../authorize/oauth-client-settings.tsx | 39 ++++++---------- 10 files changed, 182 insertions(+), 58 deletions(-) create mode 100644 web/app/components/base/form/hooks/index.ts create mode 100644 web/app/components/base/form/hooks/use-check-validated.ts create mode 100644 web/app/components/base/form/hooks/use-get-form-values.ts create mode 100644 web/app/components/base/form/utils/index.ts create mode 100644 web/app/components/base/form/utils/secret-input/index.ts diff --git a/web/app/components/base/form/components/base/base-field.tsx b/web/app/components/base/form/components/base/base-field.tsx index 70cd3a9f78..e48ce780c0 100644 --- a/web/app/components/base/form/components/base/base-field.tsx +++ b/web/app/components/base/form/components/base/base-field.tsx @@ -99,7 +99,7 @@ const BaseField = ({ id={field.name} name={field.name} className={cn(inputClassName)} - value={value} + value={value || ''} onChange={e => field.handleChange(e.target.value)} onBlur={field.handleBlur} disabled={disabled} @@ -114,7 +114,7 @@ const BaseField = ({ name={field.name} type='password' className={cn(inputClassName)} - value={value} + value={value || ''} onChange={e => field.handleChange(e.target.value)} onBlur={field.handleBlur} disabled={disabled} @@ -129,7 +129,7 @@ const BaseField = ({ name={field.name} type='number' className={cn(inputClassName)} - value={value} + value={value || ''} onChange={e => field.handleChange(e.target.value)} onBlur={field.handleBlur} disabled={disabled} diff --git a/web/app/components/base/form/components/base/base-form.tsx b/web/app/components/base/form/components/base/base-form.tsx index 6911e4d95f..cde1c7619a 100644 --- a/web/app/components/base/form/components/base/base-form.tsx +++ b/web/app/components/base/form/components/base/base-form.tsx @@ -6,6 +6,7 @@ import { import type { AnyFieldApi, } from '@tanstack/react-form' +import { useTranslation } from 'react-i18next' import { useForm } from '@tanstack/react-form' import type { FormRef, @@ -18,6 +19,7 @@ import type { BaseFieldProps, } from '.' import cn from '@/utils/classnames' +import { useGetFormValues } from '@/app/components/base/form/hooks' export type BaseFormProps = { formSchemas?: FormSchema[] @@ -28,7 +30,7 @@ export type BaseFormProps = { } & Pick const BaseForm = ({ - formSchemas, + formSchemas = [], defaultValues, formClassName, fieldClassName, @@ -38,17 +40,22 @@ const BaseForm = ({ ref, disabled, }: BaseFormProps) => { + const { t } = useTranslation() const form = useForm({ defaultValues, }) + const { getFormValues } = useGetFormValues(form) useImperativeHandle(ref, () => { return { getForm() { return form }, + getFormValues: (option) => { + return getFormValues(formSchemas, option) + }, } - }, [form]) + }, [form, formSchemas, getFormValues]) const renderField = useCallback((field: AnyFieldApi) => { const formSchema = formSchemas?.find(schema => schema.name === field.name) @@ -73,17 +80,37 @@ const BaseForm = ({ const renderFieldWrapper = useCallback((formSchema: FormSchema) => { const { name, + validators, + required, } = formSchema + let mergedValidators = validators + if (required && !validators) { + mergedValidators = { + onMount: ({ value }: any) => { + if (!value) + return t('common.errorMsg.fieldRequired', { field: name }) + }, + onChange: ({ value }: any) => { + if (!value) + return t('common.errorMsg.fieldRequired', { field: name }) + }, + onBlur: ({ value }: any) => { + if (!value) + return t('common.errorMsg.fieldRequired', { field: name }) + }, + } + } return ( {renderField} ) - }, [renderField, form]) + }, [renderField, form, t]) if (!formSchemas?.length) return null diff --git a/web/app/components/base/form/hooks/index.ts b/web/app/components/base/form/hooks/index.ts new file mode 100644 index 0000000000..189369f240 --- /dev/null +++ b/web/app/components/base/form/hooks/index.ts @@ -0,0 +1,2 @@ +export * from './use-check-validated' +export * from './use-get-form-values' diff --git a/web/app/components/base/form/hooks/use-check-validated.ts b/web/app/components/base/form/hooks/use-check-validated.ts new file mode 100644 index 0000000000..975c6fcb1f --- /dev/null +++ b/web/app/components/base/form/hooks/use-check-validated.ts @@ -0,0 +1,36 @@ +import { useCallback } from 'react' +import type { AnyFormApi } from '@tanstack/react-form' +import { useToastContext } from '@/app/components/base/toast' + +export const useCheckValidated = (form: AnyFormApi) => { + const { notify } = useToastContext() + + const checkValidated = useCallback(() => { + const allError = form?.getAllErrors() + + if (allError) { + const fields = allError.fields + const errorArray = Object.keys(fields).reduce((acc: string[], key: string) => { + const errors: any[] = fields[key].errors + + return [...acc, ...errors] + }, [] as string[]) + + if (errorArray.length) { + notify({ + type: 'error', + message: errorArray[0], + }) + return false + } + + return true + } + + return true + }, [form, notify]) + + return { + checkValidated, + } +} diff --git a/web/app/components/base/form/hooks/use-get-form-values.ts b/web/app/components/base/form/hooks/use-get-form-values.ts new file mode 100644 index 0000000000..918e6220bc --- /dev/null +++ b/web/app/components/base/form/hooks/use-get-form-values.ts @@ -0,0 +1,45 @@ +import { useCallback } from 'react' +import type { AnyFormApi } from '@tanstack/react-form' +import { useCheckValidated } from './use-check-validated' +import type { + FormSchema, + GetValuesOptions, +} from '../types' +import { getTransformedValuesWhenSecretInputPristine } from '../utils' + +export const useGetFormValues = (form: AnyFormApi) => { + const { checkValidated } = useCheckValidated(form) + + const getFormValues = useCallback(( + formSchemas: FormSchema[], + { + needCheckValidatedValues, + needTransformWhenSecretFieldIsPristine, + }: GetValuesOptions, + ) => { + const values = form?.store.state.values || {} + if (!needCheckValidatedValues) { + return { + values, + isCheckValidated: false, + } + } + + if (checkValidated()) { + return { + values: needTransformWhenSecretFieldIsPristine ? getTransformedValuesWhenSecretInputPristine(formSchemas, form) : values, + isCheckValidated: true, + } + } + else { + return { + values: {}, + isCheckValidated: false, + } + } + }, [form, checkValidated]) + + return { + getFormValues, + } +} diff --git a/web/app/components/base/form/types.ts b/web/app/components/base/form/types.ts index 02a93c3285..0a26b3fad3 100644 --- a/web/app/components/base/form/types.ts +++ b/web/app/components/base/form/types.ts @@ -60,7 +60,15 @@ export type FormSchema = { export type FormValues = Record +export type GetValuesOptions = { + needTransformWhenSecretFieldIsPristine?: boolean + needCheckValidatedValues?: boolean +} export type FormRefObject = { getForm: () => AnyFormApi + getFormValues: (obj: GetValuesOptions) => { + values: Record + isCheckValidated: boolean + } } export type FormRef = ForwardedRef diff --git a/web/app/components/base/form/utils/index.ts b/web/app/components/base/form/utils/index.ts new file mode 100644 index 0000000000..0abb8d1ad5 --- /dev/null +++ b/web/app/components/base/form/utils/index.ts @@ -0,0 +1 @@ +export * from './secret-input' diff --git a/web/app/components/base/form/utils/secret-input/index.ts b/web/app/components/base/form/utils/secret-input/index.ts new file mode 100644 index 0000000000..e8458f90b2 --- /dev/null +++ b/web/app/components/base/form/utils/secret-input/index.ts @@ -0,0 +1,29 @@ +import type { AnyFormApi } from '@tanstack/react-form' +import type { FormSchema } from '@/app/components/base/form/types' +import { FormTypeEnum } from '@/app/components/base/form/types' + +export const transformFormSchemasSecretInput = (isPristineSecretInputNames: string[], values: Record) => { + const transformedValues: Record = { ...values } + + isPristineSecretInputNames.forEach((name) => { + if (transformedValues[name]) + transformedValues[name] = '[__HIDDEN__]' + }) + + return transformedValues +} + +export const getTransformedValuesWhenSecretInputPristine = (formSchemas: FormSchema[], form: AnyFormApi) => { + const values = form?.store.state.values || {} + const isPristineSecretInputNames: string[] = [] + for (let i = 0; i < formSchemas.length; i++) { + const schema = formSchemas[i] + if (schema.type === FormTypeEnum.secretInput) { + const fieldMeta = form?.getFieldMeta(schema.name) + if (fieldMeta?.isPristine) + isPristineSecretInputNames.push(schema.name) + } + } + + return transformFormSchemasSecretInput(isPristineSecretInputNames, values) +} diff --git a/web/app/components/plugins/plugin-auth/authorize/api-key-modal.tsx b/web/app/components/plugins/plugin-auth/authorize/api-key-modal.tsx index 5e7f8a7adb..0f81f8ace4 100644 --- a/web/app/components/plugins/plugin-auth/authorize/api-key-modal.tsx +++ b/web/app/components/plugins/plugin-auth/authorize/api-key-modal.tsx @@ -9,7 +9,6 @@ import { RiExternalLinkLine } from '@remixicon/react' import { Lock01 } from '@/app/components/base/icons/src/vender/solid/security' import Modal from '@/app/components/base/modal/modal' import { CredentialTypeEnum } from '../types' -import { transformFormSchemasSecretInput } from '../utils' import AuthForm from '@/app/components/base/form/form-scenarios/auth' import type { FormRefObject } from '@/app/components/base/form/types' import { FormTypeEnum } from '@/app/components/base/form/types' @@ -64,42 +63,32 @@ const ApiKeyModal = ({ const invalidatePluginCredentialInfo = useInvalidPluginCredentialInfoHook(pluginPayload) const formRef = useRef(null) const handleConfirm = useCallback(async () => { - const form = formRef.current?.getForm() - const store = form?.store + const { + isCheckValidated, + values, + } = formRef.current?.getFormValues({ + needCheckValidatedValues: true, + needTransformWhenSecretFieldIsPristine: true, + }) || { isCheckValidated: false, values: {} } + if (!isCheckValidated) + return + const { __name__, __credential_id__, - ...values - } = store?.state.values - const isPristineSecretInputNames: string[] = [] - for (let i = 0; i < formSchemas.length; i++) { - const schema = formSchemas[i] - if (schema.required && !values[schema.name]) { - notify({ - type: 'error', - message: t('common.errorMsg.fieldRequired', { field: schema.name }), - }) - return - } - if (schema.type === FormTypeEnum.secretInput) { - const fieldMeta = form?.getFieldMeta(schema.name) - if (fieldMeta?.isPristine) - isPristineSecretInputNames.push(schema.name) - } - } - - const transformedValues = transformFormSchemasSecretInput(isPristineSecretInputNames, values) + ...restValues + } = values if (editValues) { await updatePluginCredential({ - credentials: transformedValues, + credentials: restValues, credential_id: __credential_id__, name: __name__ || '', }) } else { await addPluginCredential({ - credentials: transformedValues, + credentials: restValues, type: CredentialTypeEnum.API_KEY, name: __name__ || '', }) @@ -111,7 +100,7 @@ const ApiKeyModal = ({ onClose?.() invalidatePluginCredentialInfo() - }, [addPluginCredential, onClose, invalidatePluginCredentialInfo, updatePluginCredential, notify, t, editValues, formSchemas]) + }, [addPluginCredential, onClose, invalidatePluginCredentialInfo, updatePluginCredential, notify, t, editValues]) return ( (null) const handleConfirm = useCallback(async () => { - const form = formRef.current?.getForm() - const store = form?.store + const { + isCheckValidated, + values, + } = formRef.current?.getFormValues({ + needCheckValidatedValues: true, + needTransformWhenSecretFieldIsPristine: true, + }) || { isCheckValidated: false, values: {} } + if (!isCheckValidated) + return const { __oauth_client__, - ...values - } = store?.state.values - const isPristineSecretInputNames: string[] = [] - for (let i = 0; i < schemas.length; i++) { - const schema = schemas[i] - if (schema.required && !values[schema.name]) { - notify({ - type: 'error', - message: t('common.errorMsg.fieldRequired', { field: schema.name }), - }) - return - } - if (schema.type === FormTypeEnum.secretInput) { - const fieldMeta = form?.getFieldMeta(schema.name) - if (fieldMeta?.isPristine) - isPristineSecretInputNames.push(schema.name) - } - } - - const transformedValues = transformFormSchemasSecretInput(isPristineSecretInputNames, values) + ...restValues + } = values await setPluginOAuthCustomClient({ - client_params: transformedValues, + client_params: restValues, enable_oauth_custom_client: __oauth_client__ === 'custom', }) notify({ @@ -82,7 +69,7 @@ const OAuthClientSettings = ({ onClose?.() invalidatePluginCredentialInfo() - }, [onClose, invalidatePluginCredentialInfo, setPluginOAuthCustomClient, notify, t, schemas]) + }, [onClose, invalidatePluginCredentialInfo, setPluginOAuthCustomClient, notify, t]) const handleConfirmAndAuthorize = useCallback(async () => { await handleConfirm()