From c0820669d4bf0c3131837779e13c7ca9a042b872 Mon Sep 17 00:00:00 2001 From: "Yoan.Liu" Date: Thu, 27 Aug 2026 20:18:31 +0800 Subject: [PATCH] feat: form input trim --- ui/src/components/MultipleSplitValueInput.tsx | 6 +- ui/src/components/access/AccessEditDrawer.tsx | 2 + .../certificate/CertificateDownloadModal.tsx | 6 +- .../workflow/WorkflowGraphImportInputBox.tsx | 2 + .../BranchBlockNodeConfigExprInputBox.tsx | 2 + .../workflow/designer/forms/_shared.tsx | 2 + ui/src/hooks/useAntdForm.ts | 28 ++++-- ui/src/pages/accesses/AccessNew.tsx | 2 + ui/src/utils/form.ts | 91 +++++++++++++++++++ 9 files changed, 130 insertions(+), 11 deletions(-) create mode 100644 ui/src/utils/form.ts diff --git a/ui/src/components/MultipleSplitValueInput.tsx b/ui/src/components/MultipleSplitValueInput.tsx index 8055cf08a..af07edfcd 100644 --- a/ui/src/components/MultipleSplitValueInput.tsx +++ b/ui/src/components/MultipleSplitValueInput.tsx @@ -56,12 +56,12 @@ const MultipleSplitValueInput = ({ name: "componentMultipleSplitValueInput_" + nanoid(), initialValues: { value: value?.split(delimiter) }, onSubmit: (values) => { - const temp = (values.value ?? []) as string[]; + let temp = (values.value ?? []) as string[]; if (splitOptions.trimSpace) { - temp.map((e) => e.trim()); + temp = temp.map((e) => e.trim()); } if (splitOptions.removeEmpty) { - temp.filter((e) => !!e); + temp = temp.filter((e) => !!e); } setValue(temp.join(delimiter)); diff --git a/ui/src/components/access/AccessEditDrawer.tsx b/ui/src/components/access/AccessEditDrawer.tsx index 88ebdfc3c..ed95716a6 100644 --- a/ui/src/components/access/AccessEditDrawer.tsx +++ b/ui/src/components/access/AccessEditDrawer.tsx @@ -12,6 +12,7 @@ import { ACCESS_USAGES } from "@/domain/provider"; import { useTriggerElement, useZustandShallowSelector } from "@/hooks"; import { useAccessesStore } from "@/stores/access"; import { unwrapErrMsg } from "@/utils/error"; +import { applyTrimmedFormValues } from "@/utils/form"; import AccessForm, { type AccessFormModes, type AccessFormProps, type AccessFormUsages } from "./AccessForm"; @@ -60,6 +61,7 @@ const AccessEditDrawer = ({ afterSubmit, mode, data, loading, trigger, usage, .. setFormPending(true); try { + applyTrimmedFormValues(formInst); formValues = await formInst.validateFields(); formValues.reserve = usage === "ca" ? "ca" : usage === "notification" ? "notif" : void 0; } catch (err) { diff --git a/ui/src/components/certificate/CertificateDownloadModal.tsx b/ui/src/components/certificate/CertificateDownloadModal.tsx index fc479969d..39c533727 100644 --- a/ui/src/components/certificate/CertificateDownloadModal.tsx +++ b/ui/src/components/certificate/CertificateDownloadModal.tsx @@ -8,6 +8,7 @@ import { saveAs } from "file-saver"; import { download as downloadCertificate } from "@/api/certificates"; import { CERTIFICATE_FORMATS, type CertificateFormatType, type CertificateModel } from "@/domain/certificate"; import { useAntdForm, useTriggerElement } from "@/hooks"; +import { applyTrimmedFormValues } from "@/utils/form"; export interface CertificateDownloadModalProps { className?: string; @@ -153,10 +154,11 @@ const CertificateDownloadModal = ({ afterClose, data, trigger, ...props }: Certi }; const handleDownloadClick = async (format: CertificateFormatType) => { - await formInst.validateFields(); + applyTrimmedFormValues(formInst); + const formValues = await formInst.validateFields(); try { - const res = await downloadCertificate(data.id, format, formInst.getFieldsValue()); + const res = await downloadCertificate(data.id, format, formValues); const bstr = atob(res.data.zipBytes); const u8arr = Uint8Array.from(bstr, (ch) => ch.charCodeAt(0)); const blob = new Blob([u8arr], { type: "application/zip" }); diff --git a/ui/src/components/workflow/WorkflowGraphImportInputBox.tsx b/ui/src/components/workflow/WorkflowGraphImportInputBox.tsx index 04c042a0f..8ca268be9 100644 --- a/ui/src/components/workflow/WorkflowGraphImportInputBox.tsx +++ b/ui/src/components/workflow/WorkflowGraphImportInputBox.tsx @@ -8,6 +8,7 @@ import { z } from "zod"; import CodeTextInput from "@/components/CodeTextInput"; import { WORKFLOW_NODE_TYPES, type WorkflowGraph, type WorkflowNode, type WorkflowNodeType } from "@/domain/workflow"; import { useAntdForm } from "@/hooks"; +import { applyTrimmedFormValues } from "@/utils/form"; export type WorkflowGraphImportInputBoxFormats = "json" | "yaml"; @@ -220,6 +221,7 @@ const WorkflowGraphImportInputBox = forwardRef { return { validate: async () => { + applyTrimmedFormValues(formInst); const formValues = await formInst.validateFields(); return deserialize(formValues.content, formValues.format); }, diff --git a/ui/src/components/workflow/designer/forms/BranchBlockNodeConfigExprInputBox.tsx b/ui/src/components/workflow/designer/forms/BranchBlockNodeConfigExprInputBox.tsx index 02d0dfebd..bb8ea5db7 100644 --- a/ui/src/components/workflow/designer/forms/BranchBlockNodeConfigExprInputBox.tsx +++ b/ui/src/components/workflow/designer/forms/BranchBlockNodeConfigExprInputBox.tsx @@ -15,6 +15,7 @@ import { type ExprValueType, } from "@/domain/workflow"; import { useAntdFormName } from "@/hooks"; +import { applyTrimmedFormValues } from "@/utils/form"; import { useNodeFormContext } from "./_context"; import { getAllPreviousNodes } from "../_util"; @@ -261,6 +262,7 @@ const BranchBlockNodeConfigExprInputBox = forwardRef { return { validate: async () => { + applyTrimmedFormValues(formInst); const formValues = await formInst.validateFields(); return formValuesToExpr(formValues); }, diff --git a/ui/src/components/workflow/designer/forms/_shared.tsx b/ui/src/components/workflow/designer/forms/_shared.tsx index 7a8fab018..3c493b970 100644 --- a/ui/src/components/workflow/designer/forms/_shared.tsx +++ b/ui/src/components/workflow/designer/forms/_shared.tsx @@ -8,6 +8,7 @@ import { isEqual } from "radash"; import Show from "@/components/Show"; import { unwrapErrMsg } from "@/utils/error"; +import { applyTrimmedFormValues } from "@/utils/form"; import { type NodeRegistry } from "../nodes/typings"; @@ -48,6 +49,7 @@ export const NodeConfigDrawer = ({ children, afterClose, anchor, footer = true, setFormPending(true); try { + applyTrimmedFormValues(formInst); formValues = await formInst.validateFields(); } catch (err) { message.warning(t("common.errmsg.form_invalid")); diff --git a/ui/src/hooks/useAntdForm.ts b/ui/src/hooks/useAntdForm.ts index f50abeba1..a9174871b 100644 --- a/ui/src/hooks/useAntdForm.ts +++ b/ui/src/hooks/useAntdForm.ts @@ -1,7 +1,9 @@ -import { useState } from "react"; +import { useMemo, useState } from "react"; import { useDeepCompareEffect } from "ahooks"; import { Form, type FormInstance, type FormProps } from "antd"; +import { applyTrimmedFormValues, trimFormValues } from "@/utils/form"; + import useAntdFormName from "./useAntdFormName"; export interface UseAntdFormOptions = any> { @@ -64,11 +66,24 @@ const useAntdForm = = any>({ form, initialValues }; }, [formInst, initialValues]); - const onFinish = (values: T) => { + const wrappedFormInst = useMemo(() => { + const originalSubmit = formInst.submit; + return { + ...formInst, + submit: () => { + applyTrimmedFormValues(formInst); + return originalSubmit(); + }, + }; + }, [formInst]); + + const doFinish = () => { if (formPending) return Promise.reject(new Error("Form is pending")); setFormPending(true); + const values = trimFormValues(formInst.getFieldsValue(true)); + return new Promise((resolve, reject) => { formInst .validateFields() @@ -93,18 +108,19 @@ const useAntdForm = = any>({ form, initialValues }; const formProps: FormProps = { - form: formInst, + form: wrappedFormInst, initialValues: formInitialValues, name: formName, - onFinish, + onFinish: doFinish, }; return { - form: formInst, + form: wrappedFormInst, formProps: formProps, formPending: formPending, submit: () => { - return onFinish(formInst.getFieldsValue(true)); + applyTrimmedFormValues(formInst); + return doFinish(); }, }; }; diff --git a/ui/src/pages/accesses/AccessNew.tsx b/ui/src/pages/accesses/AccessNew.tsx index 5f41320e2..1cf98dde8 100644 --- a/ui/src/pages/accesses/AccessNew.tsx +++ b/ui/src/pages/accesses/AccessNew.tsx @@ -12,6 +12,7 @@ import { ACCESS_USAGES } from "@/domain/provider"; import { useZustandShallowSelector } from "@/hooks"; import { useAccessesStore } from "@/stores/access"; import { unwrapErrMsg } from "@/utils/error"; +import { applyTrimmedFormValues } from "@/utils/form"; const AccessNew = () => { const navigate = useNavigate(); @@ -47,6 +48,7 @@ const AccessNew = () => { setFormPending(true); try { + applyTrimmedFormValues(formInst); formValues = await formInst.validateFields(); formValues.reserve = providerUsage === "ca" ? "ca" : providerUsage === "notification" ? "notif" : void 0; } catch (err) { diff --git a/ui/src/utils/form.ts b/ui/src/utils/form.ts new file mode 100644 index 000000000..723713edb --- /dev/null +++ b/ui/src/utils/form.ts @@ -0,0 +1,91 @@ +import type { FormInstance } from "antd"; + +export const TRIM_EXEMPT_FIELD_NAMES = new Set([ + "accesskey", + "accesskeyid", + "accesskeypassword", + "accesskeysecret", + "accesstoken", + "accesstokenid", + "apiaccesstoken", + "apikey", + "apikeysecret", + "apipassword", + "apisecret", + "apitoken", + "apitokenforzone", + "apitokensecret", + "appkey", + "applicationkey", + "applicationsecret", + "applicationtoken", + "authpassword", + "bottoken", + "clientsecret", + "clienttoken", + "confirmpassword", + "consumerkey", + "credentials", + "eabhmackey", + "httptoken", + "jkskeypass", + "jksstorepass", + "key", + "keypassphrase", + "mtlsprivatekey", + "newpassword", + "oldpassword", + "password", + "personalaccesstoken", + "pfxpassword", + "privatekey", + "privatekeypassphrase", + "secret", + "secretaccesskey", + "secretapikey", + "secretid", + "secretkey", + "serviceaccountkey", + "token", + "totpsecret", + "tsiggsspassword", + "tsigkey", + "tsigsecret", +]); + +const isPlainObject = (value: unknown): value is Record => { + if (typeof value !== "object" || value === null) { + return false; + } + + const proto = Object.getPrototypeOf(value); + return proto === Object.prototype || proto === null; +}; + +const trimValue = (value: unknown, key?: string): unknown => { + if (typeof value === "string") { + return key != null && TRIM_EXEMPT_FIELD_NAMES.has(key.toLowerCase()) ? value : value.trim(); + } + + if (Array.isArray(value)) { + return value.map((item) => trimValue(item)); + } + + if (isPlainObject(value)) { + const obj: Record = {}; + Object.keys(value).forEach((k) => { + obj[k] = trimValue(value[k], k); + }); + return obj; + } + + return value; +}; + +export const trimFormValues = (values: T): T => { + return trimValue(values) as T; +}; + +export const applyTrimmedFormValues = (formInst: FormInstance): void => { + formInst.setFieldsValue(trimFormValues(formInst.getFieldsValue(true))); +};