diff --git a/ui/src/components/access/AccessEditModal.tsx b/ui/src/components/access/AccessEditModal.tsx index 7447bd7b..710f73f1 100644 --- a/ui/src/components/access/AccessEditModal.tsx +++ b/ui/src/components/access/AccessEditModal.tsx @@ -1,8 +1,9 @@ -import { cloneElement, useMemo, useRef, useState } from "react"; +import { useRef, useState } from "react"; import { useTranslation } from "react-i18next"; import { useControllableValue } from "ahooks"; import { Modal, notification } from "antd"; +import { useTriggerElement } from "@/hooks"; import { type AccessModel } from "@/domain/access"; import { useAccessStore } from "@/stores/access"; import { getErrMsg } from "@/utils/error"; @@ -13,7 +14,7 @@ export type AccessEditModalProps = { loading?: boolean; open?: boolean; preset: AccessEditFormProps["preset"]; - trigger?: React.ReactElement; + trigger?: React.ReactNode; onOpenChange?: (open: boolean) => void; onSubmit?: (record: AccessModel) => void; }; @@ -31,19 +32,7 @@ const AccessEditModal = ({ data, loading, trigger, preset, onSubmit, ...props }: trigger: "onOpenChange", }); - const triggerEl = useMemo(() => { - if (!trigger) { - return null; - } - - return cloneElement(trigger, { - ...trigger.props, - onClick: () => { - setOpen(true); - trigger.props?.onClick?.(); - }, - }); - }, [trigger, setOpen]); + const triggerDom = useTriggerElement(trigger, { onClick: () => setOpen(true) }); const formRef = useRef(null); const [formPending, setFormPending] = useState(false); @@ -94,7 +83,7 @@ const AccessEditModal = ({ data, loading, trigger, preset, onSubmit, ...props }: <> {NotificationContextHolder} - {triggerEl} + {triggerDom} setOpen(false)} diff --git a/ui/src/components/certificate/CertificateDetailDrawer.tsx b/ui/src/components/certificate/CertificateDetailDrawer.tsx index 1b1a0d12..522f90f5 100644 --- a/ui/src/components/certificate/CertificateDetailDrawer.tsx +++ b/ui/src/components/certificate/CertificateDetailDrawer.tsx @@ -1,16 +1,16 @@ -import { cloneElement, useMemo } from "react"; import { useControllableValue } from "ahooks"; import { Drawer } from "antd"; import Show from "@/components/Show"; import CertificateDetail from "./CertificateDetail"; +import { useTriggerElement } from "@/hooks"; import { type CertificateModel } from "@/domain/certificate"; export type CertificateDetailDrawerProps = { data?: CertificateModel; loading?: boolean; open?: boolean; - trigger?: React.ReactElement; + trigger?: React.ReactNode; onOpenChange?: (open: boolean) => void; }; @@ -21,23 +21,11 @@ const CertificateDetailDrawer = ({ data, loading, trigger, ...props }: Certifica trigger: "onOpenChange", }); - const triggerEl = useMemo(() => { - if (!trigger) { - return null; - } - - return cloneElement(trigger, { - ...trigger.props, - onClick: () => { - setOpen(true); - trigger.props?.onClick?.(); - }, - }); - }, [trigger, setOpen]); + const triggerDom = useTriggerElement(trigger, { onClick: () => setOpen(true) }); return ( <> - {triggerEl} + {triggerDom} setOpen(false)}> diff --git a/ui/src/components/workflow/node/ApplyNodeForm.tsx b/ui/src/components/workflow/node/ApplyNodeForm.tsx index 029d2501..10500a0b 100644 --- a/ui/src/components/workflow/node/ApplyNodeForm.tsx +++ b/ui/src/components/workflow/node/ApplyNodeForm.tsx @@ -1,7 +1,7 @@ import { memo, useCallback, useEffect, useState } from "react"; import { useTranslation } from "react-i18next"; import { useControllableValue } from "ahooks"; -import { AutoComplete, Button, Divider, Form, Input, InputNumber, Select, Switch, Tooltip, Typography, type AutoCompleteProps } from "antd"; +import { AutoComplete, Button, Divider, Form, Input, Select, Switch, Tooltip, Typography, type AutoCompleteProps } from "antd"; import { createSchemaFieldRule } from "antd-zod"; import { PlusOutlined as PlusOutlinedIcon, QuestionCircleOutlined as QuestionCircleOutlinedIcon } from "@ant-design/icons"; import z from "zod"; @@ -61,7 +61,12 @@ const ApplyNodeForm = ({ data }: ApplyNodeFormProps) => { { message: t("common.errmsg.host_invalid") } ) .nullish(), - timeout: z.number().gte(1, t("workflow.nodes.apply.form.propagation_timeout.placeholder")).nullish(), + timeout: z + .number() + .int() + .gte(1, t("workflow.nodes.apply.form.propagation_timeout.placeholder")) + .transform((v) => +v) + .nullish(), disableFollowCNAME: z.boolean().nullish(), }); const formRule = createSchemaFieldRule(formSchema); @@ -161,7 +166,7 @@ const ApplyNodeForm = ({ data }: ApplyNodeFormProps) => { rules={[formRule]} tooltip={} > - + { rules={[formRule]} tooltip={} > - void; }; @@ -23,23 +24,11 @@ const WorkflowRunDetailDrawer = ({ data, loading, trigger, ...props }: WorkflowR trigger: "onOpenChange", }); - const triggerEl = useMemo(() => { - if (!trigger) { - return null; - } - - return cloneElement(trigger, { - ...trigger.props, - onClick: () => { - setOpen(true); - trigger.props?.onClick?.(); - }, - }); - }, [trigger, setOpen]); + const triggerDom = useTriggerElement(trigger, { onClick: () => setOpen(true) }); return ( <> - {triggerEl} + {triggerDom} setOpen(false)}> diff --git a/ui/src/hooks/index.ts b/ui/src/hooks/index.ts index 6aa77f23..b4024a3c 100644 --- a/ui/src/hooks/index.ts +++ b/ui/src/hooks/index.ts @@ -1,5 +1,6 @@ import useAntdForm from "./useAntdForm"; import useBrowserTheme from "./useBrowserTheme"; +import useTriggerElement from "./useTriggerElement"; import useZustandShallowSelector from "./useZustandShallowSelector"; -export { useAntdForm, useBrowserTheme, useZustandShallowSelector }; +export { useAntdForm, useBrowserTheme, useTriggerElement, useZustandShallowSelector }; diff --git a/ui/src/hooks/useTriggerElement.ts b/ui/src/hooks/useTriggerElement.ts new file mode 100644 index 00000000..9ac41a62 --- /dev/null +++ b/ui/src/hooks/useTriggerElement.ts @@ -0,0 +1,32 @@ +import { cloneElement, createElement, Fragment, isValidElement, useMemo } from "react"; + +export type UseTriggerElementOptions = { + onClick?: (e: MouseEvent) => void; +}; + +/** + * 获取一个触发器元素。通常为配合 Drawer、Modal 等组件使用。 + * @param {React.ReactNode} trigger + * @param {UseTriggerElementOptions} [options] + * @returns {React.ReactElement} + */ +const useTriggerElement = (trigger: React.ReactNode, options?: UseTriggerElementOptions) => { + const onClick = options?.onClick; + const triggerDom = useMemo(() => { + if (!trigger) { + return null; + } + + const temp = isValidElement(trigger) ? trigger : createElement(Fragment, null, trigger); + return cloneElement(temp, { + ...temp.props, + onClick: (e: MouseEvent) => { + onClick?.(e); + temp.props?.onClick?.(e); + }, + }); + }, [trigger, onClick]); + return triggerDom; +}; + +export default useTriggerElement; diff --git a/ui/src/pages/workflows/WorkflowDetail.tsx b/ui/src/pages/workflows/WorkflowDetail.tsx index 3c80cc76..82c89628 100644 --- a/ui/src/pages/workflows/WorkflowDetail.tsx +++ b/ui/src/pages/workflows/WorkflowDetail.tsx @@ -1,4 +1,4 @@ -import { cloneElement, memo, useEffect, useMemo, useState } from "react"; +import { memo, useEffect, useMemo, useState } from "react"; import { useNavigate, useParams } from "react-router-dom"; import { useTranslation } from "react-i18next"; import { Button, Card, Dropdown, Form, Input, message, Modal, notification, Tabs, Typography } from "antd"; @@ -12,7 +12,7 @@ import End from "@/components/workflow/End"; import NodeRender from "@/components/workflow/NodeRender"; import WorkflowRuns from "@/components/workflow/run/WorkflowRuns"; import WorkflowProvider from "@/components/workflow/WorkflowProvider"; -import { useAntdForm, useZustandShallowSelector } from "@/hooks"; +import { useAntdForm, useTriggerElement, useZustandShallowSelector } from "@/hooks"; import { allNodesValidated, type WorkflowModel, type WorkflowNode } from "@/domain/workflow"; import { useWorkflowStore } from "@/stores/workflow"; import { remove as removeWorkflow } from "@/repository/workflow"; @@ -189,26 +189,14 @@ const WorkflowBaseInfoModalForm = memo( onFinish, }: { initialValues: Pick; - trigger?: React.ReactElement; + trigger?: React.ReactNode; onFinish?: (values: Pick) => Promise; }) => { const { t } = useTranslation(); const [open, setOpen] = useState(false); - const triggerEl = useMemo(() => { - if (!trigger) { - return null; - } - - return cloneElement(trigger, { - ...trigger.props, - onClick: () => { - setOpen(true); - trigger.props?.onClick?.(); - }, - }); - }, [trigger, setOpen]); + const triggerDom = useTriggerElement(trigger, { onClick: () => setOpen(true) }); const formSchema = z.object({ name: z @@ -251,7 +239,7 @@ const WorkflowBaseInfoModalForm = memo( return ( <> - {triggerEl} + {triggerDom} setOpen(false)} diff --git a/ui/src/utils/validators.ts b/ui/src/utils/validators.ts index 3eb4661b..75d57767 100644 --- a/ui/src/utils/validators.ts +++ b/ui/src/utils/validators.ts @@ -10,13 +10,13 @@ export const validDomainName = (value: string, wildcard = false) => { }; export const validEmailAddress = (value: string) => { - const re = /^[\w!#$%&'*+/=?^_`{|}~-]+(?:\.[\w!#$%&'*+/=?^_`{|}~-]+)*@(?:[\w](?:[\w-]*[\w])?\.)+[\w](?:[\w-]*[\w])?$/; + const re = /^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$/; return re.test(value); }; export const validIPv4Address = (value: string) => { const re = - /^(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)$/; + /^(25[0-5]|2[0-4][0-9]|1[0-9]{2}|[1-9]?[0-9])\.(25[0-5]|2[0-4][0-9]|1[0-9]{2}|[1-9]?[0-9])\.(25[0-5]|2[0-4][0-9]|1[0-9]{2}|[1-9]?[0-9])\.(25[0-5]|2[0-4][0-9]|1[0-9]{2}|[1-9]?[0-9])$/; return re.test(value); };