refactor(ui): useTriggerElement

This commit is contained in:
Fu Diwei 2024-12-27 12:47:45 +08:00
parent 77537e7005
commit 75cf552e72
8 changed files with 65 additions and 72 deletions

View File

@ -1,8 +1,9 @@
import { cloneElement, useMemo, useRef, useState } from "react"; import { useRef, useState } from "react";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
import { useControllableValue } from "ahooks"; import { useControllableValue } from "ahooks";
import { Modal, notification } from "antd"; import { Modal, notification } from "antd";
import { useTriggerElement } from "@/hooks";
import { type AccessModel } from "@/domain/access"; import { type AccessModel } from "@/domain/access";
import { useAccessStore } from "@/stores/access"; import { useAccessStore } from "@/stores/access";
import { getErrMsg } from "@/utils/error"; import { getErrMsg } from "@/utils/error";
@ -13,7 +14,7 @@ export type AccessEditModalProps = {
loading?: boolean; loading?: boolean;
open?: boolean; open?: boolean;
preset: AccessEditFormProps["preset"]; preset: AccessEditFormProps["preset"];
trigger?: React.ReactElement; trigger?: React.ReactNode;
onOpenChange?: (open: boolean) => void; onOpenChange?: (open: boolean) => void;
onSubmit?: (record: AccessModel) => void; onSubmit?: (record: AccessModel) => void;
}; };
@ -31,19 +32,7 @@ const AccessEditModal = ({ data, loading, trigger, preset, onSubmit, ...props }:
trigger: "onOpenChange", trigger: "onOpenChange",
}); });
const triggerEl = useMemo(() => { const triggerDom = useTriggerElement(trigger, { onClick: () => setOpen(true) });
if (!trigger) {
return null;
}
return cloneElement(trigger, {
...trigger.props,
onClick: () => {
setOpen(true);
trigger.props?.onClick?.();
},
});
}, [trigger, setOpen]);
const formRef = useRef<AccessEditFormInstance>(null); const formRef = useRef<AccessEditFormInstance>(null);
const [formPending, setFormPending] = useState(false); const [formPending, setFormPending] = useState(false);
@ -94,7 +83,7 @@ const AccessEditModal = ({ data, loading, trigger, preset, onSubmit, ...props }:
<> <>
{NotificationContextHolder} {NotificationContextHolder}
{triggerEl} {triggerDom}
<Modal <Modal
afterClose={() => setOpen(false)} afterClose={() => setOpen(false)}

View File

@ -1,16 +1,16 @@
import { cloneElement, useMemo } from "react";
import { useControllableValue } from "ahooks"; import { useControllableValue } from "ahooks";
import { Drawer } from "antd"; import { Drawer } from "antd";
import Show from "@/components/Show"; import Show from "@/components/Show";
import CertificateDetail from "./CertificateDetail"; import CertificateDetail from "./CertificateDetail";
import { useTriggerElement } from "@/hooks";
import { type CertificateModel } from "@/domain/certificate"; import { type CertificateModel } from "@/domain/certificate";
export type CertificateDetailDrawerProps = { export type CertificateDetailDrawerProps = {
data?: CertificateModel; data?: CertificateModel;
loading?: boolean; loading?: boolean;
open?: boolean; open?: boolean;
trigger?: React.ReactElement; trigger?: React.ReactNode;
onOpenChange?: (open: boolean) => void; onOpenChange?: (open: boolean) => void;
}; };
@ -21,23 +21,11 @@ const CertificateDetailDrawer = ({ data, loading, trigger, ...props }: Certifica
trigger: "onOpenChange", trigger: "onOpenChange",
}); });
const triggerEl = useMemo(() => { const triggerDom = useTriggerElement(trigger, { onClick: () => setOpen(true) });
if (!trigger) {
return null;
}
return cloneElement(trigger, {
...trigger.props,
onClick: () => {
setOpen(true);
trigger.props?.onClick?.();
},
});
}, [trigger, setOpen]);
return ( return (
<> <>
{triggerEl} {triggerDom}
<Drawer closable destroyOnClose open={open} loading={loading} placement="right" title={data?.id} width={640} onClose={() => setOpen(false)}> <Drawer closable destroyOnClose open={open} loading={loading} placement="right" title={data?.id} width={640} onClose={() => setOpen(false)}>
<Show when={!!data}> <Show when={!!data}>

View File

@ -1,7 +1,7 @@
import { memo, useCallback, useEffect, useState } from "react"; import { memo, useCallback, useEffect, useState } from "react";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
import { useControllableValue } from "ahooks"; 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 { createSchemaFieldRule } from "antd-zod";
import { PlusOutlined as PlusOutlinedIcon, QuestionCircleOutlined as QuestionCircleOutlinedIcon } from "@ant-design/icons"; import { PlusOutlined as PlusOutlinedIcon, QuestionCircleOutlined as QuestionCircleOutlinedIcon } from "@ant-design/icons";
import z from "zod"; import z from "zod";
@ -61,7 +61,12 @@ const ApplyNodeForm = ({ data }: ApplyNodeFormProps) => {
{ message: t("common.errmsg.host_invalid") } { message: t("common.errmsg.host_invalid") }
) )
.nullish(), .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(), disableFollowCNAME: z.boolean().nullish(),
}); });
const formRule = createSchemaFieldRule(formSchema); const formRule = createSchemaFieldRule(formSchema);
@ -161,7 +166,7 @@ const ApplyNodeForm = ({ data }: ApplyNodeFormProps) => {
rules={[formRule]} rules={[formRule]}
tooltip={<span dangerouslySetInnerHTML={{ __html: t("workflow.nodes.apply.form.nameservers.tooltip") }}></span>} tooltip={<span dangerouslySetInnerHTML={{ __html: t("workflow.nodes.apply.form.nameservers.tooltip") }}></span>}
> >
<Input placeholder={t("workflow.nodes.apply.form.nameservers.placeholder")} /> <Input allowClear placeholder={t("workflow.nodes.apply.form.nameservers.placeholder")} />
</Form.Item> </Form.Item>
<Form.Item <Form.Item
@ -170,8 +175,9 @@ const ApplyNodeForm = ({ data }: ApplyNodeFormProps) => {
rules={[formRule]} rules={[formRule]}
tooltip={<span dangerouslySetInnerHTML={{ __html: t("workflow.nodes.apply.form.propagation_timeout.tooltip") }}></span>} tooltip={<span dangerouslySetInnerHTML={{ __html: t("workflow.nodes.apply.form.propagation_timeout.tooltip") }}></span>}
> >
<InputNumber <Input
className="w-full" type="number"
allowClear
min={0} min={0}
max={3600} max={3600}
placeholder={t("workflow.nodes.apply.form.propagation_timeout.placeholder")} placeholder={t("workflow.nodes.apply.form.propagation_timeout.placeholder")}

View File

@ -4,13 +4,14 @@ import { useControllableValue } from "ahooks";
import { Alert, Drawer } from "antd"; import { Alert, Drawer } from "antd";
import Show from "@/components/Show"; import Show from "@/components/Show";
import { useTriggerElement } from "@/hooks";
import { type WorkflowRunModel } from "@/domain/workflowRun"; import { type WorkflowRunModel } from "@/domain/workflowRun";
export type WorkflowRunDetailDrawerProps = { export type WorkflowRunDetailDrawerProps = {
data?: WorkflowRunModel; data?: WorkflowRunModel;
loading?: boolean; loading?: boolean;
open?: boolean; open?: boolean;
trigger?: React.ReactElement; trigger?: React.ReactNode;
onOpenChange?: (open: boolean) => void; onOpenChange?: (open: boolean) => void;
}; };
@ -23,23 +24,11 @@ const WorkflowRunDetailDrawer = ({ data, loading, trigger, ...props }: WorkflowR
trigger: "onOpenChange", trigger: "onOpenChange",
}); });
const triggerEl = useMemo(() => { const triggerDom = useTriggerElement(trigger, { onClick: () => setOpen(true) });
if (!trigger) {
return null;
}
return cloneElement(trigger, {
...trigger.props,
onClick: () => {
setOpen(true);
trigger.props?.onClick?.();
},
});
}, [trigger, setOpen]);
return ( return (
<> <>
{triggerEl} {triggerDom}
<Drawer closable destroyOnClose open={open} loading={loading} placement="right" title={data?.id} width={640} onClose={() => setOpen(false)}> <Drawer closable destroyOnClose open={open} loading={loading} placement="right" title={data?.id} width={640} onClose={() => setOpen(false)}>
<Show when={!!data}> <Show when={!!data}>

View File

@ -1,5 +1,6 @@
import useAntdForm from "./useAntdForm"; import useAntdForm from "./useAntdForm";
import useBrowserTheme from "./useBrowserTheme"; import useBrowserTheme from "./useBrowserTheme";
import useTriggerElement from "./useTriggerElement";
import useZustandShallowSelector from "./useZustandShallowSelector"; import useZustandShallowSelector from "./useZustandShallowSelector";
export { useAntdForm, useBrowserTheme, useZustandShallowSelector }; export { useAntdForm, useBrowserTheme, useTriggerElement, useZustandShallowSelector };

View File

@ -0,0 +1,32 @@
import { cloneElement, createElement, Fragment, isValidElement, useMemo } from "react";
export type UseTriggerElementOptions = {
onClick?: (e: MouseEvent) => void;
};
/**
* DrawerModal 使
* @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;

View File

@ -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 { useNavigate, useParams } from "react-router-dom";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
import { Button, Card, Dropdown, Form, Input, message, Modal, notification, Tabs, Typography } from "antd"; 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 NodeRender from "@/components/workflow/NodeRender";
import WorkflowRuns from "@/components/workflow/run/WorkflowRuns"; import WorkflowRuns from "@/components/workflow/run/WorkflowRuns";
import WorkflowProvider from "@/components/workflow/WorkflowProvider"; 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 { allNodesValidated, type WorkflowModel, type WorkflowNode } from "@/domain/workflow";
import { useWorkflowStore } from "@/stores/workflow"; import { useWorkflowStore } from "@/stores/workflow";
import { remove as removeWorkflow } from "@/repository/workflow"; import { remove as removeWorkflow } from "@/repository/workflow";
@ -189,26 +189,14 @@ const WorkflowBaseInfoModalForm = memo(
onFinish, onFinish,
}: { }: {
initialValues: Pick<WorkflowModel, "name" | "description">; initialValues: Pick<WorkflowModel, "name" | "description">;
trigger?: React.ReactElement; trigger?: React.ReactNode;
onFinish?: (values: Pick<WorkflowModel, "name" | "description">) => Promise<void | boolean>; onFinish?: (values: Pick<WorkflowModel, "name" | "description">) => Promise<void | boolean>;
}) => { }) => {
const { t } = useTranslation(); const { t } = useTranslation();
const [open, setOpen] = useState(false); const [open, setOpen] = useState(false);
const triggerEl = useMemo(() => { const triggerDom = useTriggerElement(trigger, { onClick: () => setOpen(true) });
if (!trigger) {
return null;
}
return cloneElement(trigger, {
...trigger.props,
onClick: () => {
setOpen(true);
trigger.props?.onClick?.();
},
});
}, [trigger, setOpen]);
const formSchema = z.object({ const formSchema = z.object({
name: z name: z
@ -251,7 +239,7 @@ const WorkflowBaseInfoModalForm = memo(
return ( return (
<> <>
{triggerEl} {triggerDom}
<Modal <Modal
afterClose={() => setOpen(false)} afterClose={() => setOpen(false)}

View File

@ -10,13 +10,13 @@ export const validDomainName = (value: string, wildcard = false) => {
}; };
export const validEmailAddress = (value: string) => { 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); return re.test(value);
}; };
export const validIPv4Address = (value: string) => { export const validIPv4Address = (value: string) => {
const re = 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); return re.test(value);
}; };