diff --git a/ui/src/App.tsx b/ui/src/App.tsx index 65f98577..939f7b3d 100644 --- a/ui/src/App.tsx +++ b/ui/src/App.tsx @@ -30,7 +30,13 @@ const RootApp = () => { dayjs.locale(i18n.language); }; i18n.on("languageChanged", handleLanguageChanged); - useLayoutEffect(handleLanguageChanged, [antdLocalesMap, i18n]); + useLayoutEffect(() => { + handleLanguageChanged(); + + return () => { + i18n.off("languageChanged", handleLanguageChanged); + }; + }, [antdLocalesMap, i18n]); const antdThemesMap: Record = useMemo( () => ({ diff --git a/ui/src/components/DrawerForm.tsx b/ui/src/components/DrawerForm.tsx index 121f2e83..e9197cb0 100644 --- a/ui/src/components/DrawerForm.tsx +++ b/ui/src/components/DrawerForm.tsx @@ -46,7 +46,7 @@ const DrawerForm = = any>({ trigger: "onOpenChange", }); - const triggerDom = useTriggerElement(trigger, { onClick: () => setOpen(true) }); + const triggerEl = useTriggerElement(trigger, { onClick: () => setOpen(true) }); const { form: formInst, @@ -66,7 +66,7 @@ const DrawerForm = = any>({ }, }); const mergedFormProps = { - preserve: drawerProps?.destroyOnClose ? false : undefined, + clearOnDestroy: drawerProps?.destroyOnClose ? true : undefined, ...formProps, ...props, }; @@ -86,11 +86,18 @@ const DrawerForm = = any>({ return ( <> - {triggerDom} + {triggerEl} { + if (!open && !mergedFormProps.preserve) { + formInst.resetFields(); + } + + drawerProps?.afterOpenChange?.(open); + }} footer={ - + diff --git a/ui/src/components/ModalForm.tsx b/ui/src/components/ModalForm.tsx index c685bb69..56b4321b 100644 --- a/ui/src/components/ModalForm.tsx +++ b/ui/src/components/ModalForm.tsx @@ -57,7 +57,7 @@ const ModalForm = = any>({ trigger: "onOpenChange", }); - const triggerDom = useTriggerElement(trigger, { onClick: () => setOpen(true) }); + const triggerEl = useTriggerElement(trigger, { onClick: () => setOpen(true) }); const { form: formInst, @@ -77,7 +77,7 @@ const ModalForm = = any>({ }, }); const mergedFormProps = { - preserve: modalProps?.destroyOnClose ? false : undefined, + clearOnDestroy: modalProps?.destroyOnClose ? true : undefined, ...formProps, ...props, }; @@ -97,9 +97,16 @@ const ModalForm = = any>({ return ( <> - {triggerDom} + {triggerEl} { + if (!mergedFormProps.preserve) { + formInst.resetFields(); + } + + modalProps?.afterClose?.(); + }} cancelButtonProps={cancelButtonProps} cancelText={cancelText} confirmLoading={formPending} diff --git a/ui/src/components/access/AccessEditForm.tsx b/ui/src/components/access/AccessEditForm.tsx index 139b6d36..76250932 100644 --- a/ui/src/components/access/AccessEditForm.tsx +++ b/ui/src/components/access/AccessEditForm.tsx @@ -1,4 +1,4 @@ -import { forwardRef, useEffect, useImperativeHandle, useMemo, useState } from "react"; +import { forwardRef, useImperativeHandle, useMemo } from "react"; import { useTranslation } from "react-i18next"; import { Form, type FormInstance, Input } from "antd"; import { createSchemaFieldRule } from "antd-zod"; @@ -64,11 +64,7 @@ const AccessEditForm = forwardRef(( initialValues: initialValues, }); - const [fieldProvider, setFieldProvider] = useState(initialValues?.provider); - useEffect(() => { - setFieldProvider(initialValues?.provider); - }, [initialValues?.provider]); - + const configProvider = Form.useWatch("provider", formInst); const [configFormInst] = Form.useForm(); const configFormName = useAntdFormName({ form: configFormInst, name: "accessEditConfigForm" }); const configFormComponent = useMemo(() => { @@ -77,7 +73,7 @@ const AccessEditForm = forwardRef(( NOTICE: If you add new child component, please keep ASCII order. */ const configFormProps = { form: configFormInst, formName: configFormName, disabled: disabled, initialValues: initialValues?.config }; - switch (fieldProvider) { + switch (configProvider) { case ACCESS_PROVIDERS.ACMEHTTPREQ: return ; case ACCESS_PROVIDERS.ALIYUN: @@ -117,7 +113,7 @@ const AccessEditForm = forwardRef(( case ACCESS_PROVIDERS.WEBHOOK: return ; } - }, [disabled, initialValues, fieldProvider, configFormInst, configFormName]); + }, [disabled, initialValues, configProvider, configFormInst, configFormName]); const handleFormProviderChange = (name: string) => { if (name === configFormName) { @@ -127,8 +123,8 @@ const AccessEditForm = forwardRef(( }; const handleFormChange = (_: unknown, values: AccessEditFormFieldValues) => { - if (values.provider !== fieldProvider) { - setFieldProvider(values.provider); + if (values.provider !== configProvider) { + formInst.setFieldValue("provider", values.provider); } onValuesChange?.(values); @@ -153,7 +149,7 @@ const AccessEditForm = forwardRef(( return (
-
+ diff --git a/ui/src/components/access/AccessEditFormKubernetesConfig.tsx b/ui/src/components/access/AccessEditFormKubernetesConfig.tsx index 67205e96..a001c176 100644 --- a/ui/src/components/access/AccessEditFormKubernetesConfig.tsx +++ b/ui/src/components/access/AccessEditFormKubernetesConfig.tsx @@ -1,5 +1,4 @@ import { useState } from "react"; -import { flushSync } from "react-dom"; import { useTranslation } from "react-i18next"; import { UploadOutlined as UploadOutlinedIcon } from "@ant-design/icons"; import { useDeepCompareEffect } from "ahooks"; @@ -55,7 +54,7 @@ const AccessEditFormKubernetesConfig = ({ form, formName, disabled, initialValue setFieldKubeFileList([]); } - flushSync(() => onValuesChange?.(form.getFieldsValue(true))); + onValuesChange?.(form.getFieldsValue(true)); }; return ( diff --git a/ui/src/components/access/AccessEditFormSSHConfig.tsx b/ui/src/components/access/AccessEditFormSSHConfig.tsx index 40663f64..b5902939 100644 --- a/ui/src/components/access/AccessEditFormSSHConfig.tsx +++ b/ui/src/components/access/AccessEditFormSSHConfig.tsx @@ -1,5 +1,4 @@ import { useState } from "react"; -import { flushSync } from "react-dom"; import { useTranslation } from "react-i18next"; import { UploadOutlined as UploadOutlinedIcon } from "@ant-design/icons"; import { useDeepCompareEffect } from "ahooks"; @@ -33,8 +32,14 @@ const AccessEditFormSSHConfig = ({ form, formName, disabled, initialValues, onVa const { t } = useTranslation(); const formSchema = z.object({ - host: z.string().refine((v) => validDomainName(v) || validIPv4Address(v) || validIPv6Address(v), t("common.errmsg.host_invalid")), - port: z.number().int().gte(1, t("common.errmsg.port_invalid")).lte(65535, t("common.errmsg.port_invalid")), + host: z + .string({ message: t("access.form.ssh_host.placeholder") }) + .refine((v) => validDomainName(v) || validIPv4Address(v) || validIPv6Address(v), t("common.errmsg.host_invalid")), + port: z + .number({ message: t("access.form.ssh_port.placeholder") }) + .int() + .gte(1, t("common.errmsg.port_invalid")) + .lte(65535, t("common.errmsg.port_invalid")), username: z .string() .min(1, "access.form.ssh_username.placeholder") @@ -74,7 +79,7 @@ const AccessEditFormSSHConfig = ({ form, formName, disabled, initialValues, onVa setFieldKeyFileList([]); } - flushSync(() => onValuesChange?.(form.getFieldsValue(true))); + onValuesChange?.(form.getFieldsValue(true)); }; return ( diff --git a/ui/src/components/access/AccessEditModal.tsx b/ui/src/components/access/AccessEditModal.tsx index 8f0dab9e..cc735dee 100644 --- a/ui/src/components/access/AccessEditModal.tsx +++ b/ui/src/components/access/AccessEditModal.tsx @@ -34,7 +34,7 @@ const AccessEditModal = ({ data, loading, trigger, preset, onSubmit, ...props }: trigger: "onOpenChange", }); - const triggerDom = useTriggerElement(trigger, { onClick: () => setOpen(true) }); + const triggerEl = useTriggerElement(trigger, { onClick: () => setOpen(true) }); const formRef = useRef(null); const [formPending, setFormPending] = useState(false); @@ -86,7 +86,7 @@ const AccessEditModal = ({ data, loading, trigger, preset, onSubmit, ...props }: <> {NotificationContextHolder} - {triggerDom} + {triggerEl} setOpen(false)} diff --git a/ui/src/components/certificate/CertificateDetailDrawer.tsx b/ui/src/components/certificate/CertificateDetailDrawer.tsx index 448a6993..9db04ab6 100644 --- a/ui/src/components/certificate/CertificateDetailDrawer.tsx +++ b/ui/src/components/certificate/CertificateDetailDrawer.tsx @@ -21,11 +21,11 @@ const CertificateDetailDrawer = ({ data, loading, trigger, ...props }: Certifica trigger: "onOpenChange", }); - const triggerDom = useTriggerElement(trigger, { onClick: () => setOpen(true) }); + const triggerEl = useTriggerElement(trigger, { onClick: () => setOpen(true) }); return ( <> - {triggerDom} + {triggerEl} setOpen(false)} > diff --git a/ui/src/components/notification/NotifyChannelEditForm.tsx b/ui/src/components/notification/NotifyChannelEditForm.tsx index 9705c6db..2831e1e1 100644 --- a/ui/src/components/notification/NotifyChannelEditForm.tsx +++ b/ui/src/components/notification/NotifyChannelEditForm.tsx @@ -80,7 +80,16 @@ const NotifyChannelEditForm = forwardRef + {formFieldsComponent} ); diff --git a/ui/src/components/provider/DeployProviderPicker.tsx b/ui/src/components/provider/DeployProviderPicker.tsx index 5db7676e..c6daf18d 100644 --- a/ui/src/components/provider/DeployProviderPicker.tsx +++ b/ui/src/components/provider/DeployProviderPicker.tsx @@ -1,6 +1,5 @@ import { memo, useState } from "react"; import { useTranslation } from "react-i18next"; -import { useDebounceEffect } from "ahooks"; import { Avatar, Card, Col, Empty, Flex, Input, Row, Typography } from "antd"; import Show from "@/components/Show"; @@ -15,25 +14,17 @@ export type DeployProviderPickerProps = { const DeployProviderPicker = ({ className, style, onSelect }: DeployProviderPickerProps) => { const { t } = useTranslation(); - const allProviders = Array.from(deployProvidersMap.values()); - const [providers, setProviders] = useState(allProviders); const [keyword, setKeyword] = useState(); - useDebounceEffect( - () => { - if (keyword) { - setProviders( - allProviders.filter((provider) => { - const value = keyword.toLowerCase(); - return provider.type.toLowerCase().includes(value) || provider.name.toLowerCase().includes(value); - }) - ); - } else { - setProviders(allProviders); - } - }, - [keyword], - { wait: 300 } - ); + + const providers = Array.from(deployProvidersMap.values()); + const filteredProviders = providers.filter((provider) => { + if (keyword) { + const value = keyword.toLowerCase(); + return provider.type.toLowerCase().includes(value) || provider.name.toLowerCase().includes(value); + } + + return true; + }); const handleProviderTypeSelect = (value: string) => { onSelect?.(value); @@ -44,9 +35,9 @@ const DeployProviderPicker = ({ className, style, onSelect }: DeployProviderPick setKeyword(e.target.value.trim())} />
- 0} fallback={}> + 0} fallback={}> - {providers.map((provider, index) => { + {filteredProviders.map((provider, index) => { return ( setOpen(true) }); + const triggerEl = useTriggerElement(trigger, { onClick: () => setOpen(true) }); return ( <> - {triggerDom} + {triggerEl} - setOpen(false)}> + setOpen(false)}> {t("workflow_run.props.status.succeeded")}} /> diff --git a/ui/src/domain/workflow.ts b/ui/src/domain/workflow.ts index 05fc622d..6328e972 100644 --- a/ui/src/domain/workflow.ts +++ b/ui/src/domain/workflow.ts @@ -152,14 +152,14 @@ export const initWorkflow = (options: InitWorkflowOptions = {}): WorkflowModel = root.config = { trigger: WORKFLOW_TRIGGERS.MANUAL }; if (options.template === "standard") { - let temp = root; - temp.next = newNode(WorkflowNodeType.Apply, {}); + let current = root; + current.next = newNode(WorkflowNodeType.Apply, {}); - temp = temp.next; - temp.next = newNode(WorkflowNodeType.Deploy, {}); + current = current.next; + current.next = newNode(WorkflowNodeType.Deploy, {}); - temp = temp.next; - temp.next = newNode(WorkflowNodeType.Notify, {}); + current = current.next; + current.next = newNode(WorkflowNodeType.Notify, {}); } return { diff --git a/ui/src/hooks/useAntdForm.ts b/ui/src/hooks/useAntdForm.ts index 48c90a9a..ffaef47b 100644 --- a/ui/src/hooks/useAntdForm.ts +++ b/ui/src/hooks/useAntdForm.ts @@ -19,7 +19,8 @@ export interface UseAntdFormReturns = any> { } /** - * + * 生成并获取一个 antd 表单的实例、属性等。 + * 通常为配合 Form 组件使用,以减少样板代码。 * @param {UseAntdFormOptions} options * @returns {UseAntdFormReturns} */ @@ -74,9 +75,9 @@ const useAntdForm = = any>({ form, initialValues .then(() => { resolve( Promise.resolve(onSubmit?.(values)) - .then((data) => { + .then((ret) => { setFormPending(false); - return data; + return ret; }) .catch((err) => { setFormPending(false); diff --git a/ui/src/hooks/useAntdFormName.ts b/ui/src/hooks/useAntdFormName.ts index 9426db2f..fc9e4501 100644 --- a/ui/src/hooks/useAntdFormName.ts +++ b/ui/src/hooks/useAntdFormName.ts @@ -6,6 +6,12 @@ export interface UseAntdFormNameOptions = any> { name?: string; } +/** + * 生成并获取一个 antd 表单的唯一名称。 + * 通常为配合 Form 组件使用,避免页面上同时存在多个表单时若有同名的 FormItem 会产生冲突。 + * @param {UseAntdFormNameOptions} options + * @returns {string} + */ const useAntdFormName = = any>(options: UseAntdFormNameOptions) => { const formName = useCreation(() => `${options.name}_${Math.random().toString(36).substring(2, 10)}${new Date().getTime()}`, [options.name, options.form]); return formName; diff --git a/ui/src/hooks/useTriggerElement.ts b/ui/src/hooks/useTriggerElement.ts index a8d8cb8c..46c71fb2 100644 --- a/ui/src/hooks/useTriggerElement.ts +++ b/ui/src/hooks/useTriggerElement.ts @@ -5,7 +5,8 @@ export type UseTriggerElementOptions = { }; /** - * 获取一个触发器元素。通常为配合 Drawer、Modal 等组件使用。 + * 获取一个触发器元素。 + * 通常为配合 Drawer、Modal 等组件使用。 * @param {React.ReactNode} trigger * @param {UseTriggerElementOptions} [options] * @returns {React.ReactElement} diff --git a/ui/src/main.tsx b/ui/src/main.tsx index 699dbf2c..7c8acab7 100644 --- a/ui/src/main.tsx +++ b/ui/src/main.tsx @@ -1,8 +1,7 @@ -import React from "react"; +import { StrictMode } from "react"; import ReactDOM from "react-dom/client"; import dayjs from "dayjs"; import dayjsUtc from "dayjs/plugin/utc"; -import "dayjs/locale/zh-cn"; import App from "./App"; import "./i18n"; @@ -11,7 +10,7 @@ import "./global.css"; dayjs.extend(dayjsUtc); ReactDOM.createRoot(document.getElementById("root")!).render( - + - + ); diff --git a/ui/src/pages/ConsoleLayout.tsx b/ui/src/pages/ConsoleLayout.tsx index a3dc79e1..a5125be4 100644 --- a/ui/src/pages/ConsoleLayout.tsx +++ b/ui/src/pages/ConsoleLayout.tsx @@ -1,6 +1,6 @@ import { memo, useEffect, useState } from "react"; import { useTranslation } from "react-i18next"; -import { Link, Navigate, Outlet, useLocation, useNavigate } from "react-router-dom"; +import { Navigate, Outlet, useLocation, useNavigate } from "react-router-dom"; import { CloudServerOutlined as CloudServerOutlinedIcon, GlobalOutlined as GlobalOutlinedIcon, @@ -16,7 +16,7 @@ import { import { Button, type ButtonProps, Drawer, Dropdown, Layout, Menu, type MenuProps, Tooltip, theme } from "antd"; import Version from "@/components/Version"; -import { useBrowserTheme } from "@/hooks"; +import { useBrowserTheme, useTriggerElement } from "@/hooks"; import { getPocketBase } from "@/repository/pocketbase"; const ConsoleLayout = () => { @@ -26,16 +26,6 @@ const ConsoleLayout = () => { const { token: themeToken } = theme.useToken(); - const [siderOpen, setSiderOpen] = useState(false); - - const handleSiderOpen = () => { - setSiderOpen(true); - }; - - const handleSiderClose = () => { - setSiderOpen(false); - }; - const handleLogoutClick = () => { auth.clear(); navigate("/login"); @@ -67,20 +57,7 @@ const ConsoleLayout = () => {
-
@@ -159,10 +136,10 @@ const SiderMenu = memo(({ onSelect }: { onSelect?: (key: string) => void }) => { return ( <> - +
Certimate - +
void }) => { ); }); +const SiderMenuDrawer = memo(({ trigger }: { trigger: React.ReactNode }) => { + const { token: themeToken } = theme.useToken(); + + const [siderOpen, setSiderOpen] = useState(false); + + const triggerEl = useTriggerElement(trigger, { onClick: () => setSiderOpen(true) }); + + return ( + <> + {triggerEl} + + setSiderOpen(false)} + > + setSiderOpen(false)} /> + + + ); +}); + const ThemeToggleButton = memo(({ size }: { size?: ButtonProps["size"] }) => { const { t } = useTranslation(); diff --git a/ui/src/pages/workflows/WorkflowDetail.tsx b/ui/src/pages/workflows/WorkflowDetail.tsx index 0948f62b..242ff9f7 100644 --- a/ui/src/pages/workflows/WorkflowDetail.tsx +++ b/ui/src/pages/workflows/WorkflowDetail.tsx @@ -23,7 +23,7 @@ import ModalForm from "@/components/ModalForm"; import Show from "@/components/Show"; import WorkflowElements from "@/components/workflow/WorkflowElements"; import WorkflowRuns from "@/components/workflow/WorkflowRuns"; -import { type WorkflowModel, isAllNodesValidated } from "@/domain/workflow"; +import { isAllNodesValidated } from "@/domain/workflow"; import { useAntdForm, useZustandShallowSelector } from "@/hooks"; import { remove as removeWorkflow } from "@/repository/workflow"; import { useWorkflowStore } from "@/stores/workflow"; @@ -40,7 +40,7 @@ const WorkflowDetail = () => { const { id: workflowId } = useParams(); const { workflow, initialized, ...workflowState } = useWorkflowStore( - useZustandShallowSelector(["workflow", "initialized", "init", "destroy", "setBaseInfo", "setEnabled", "release", "discard"]) + useZustandShallowSelector(["workflow", "initialized", "init", "destroy", "setEnabled", "release", "discard"]) ); useEffect(() => { // TODO: loading & error @@ -66,16 +66,6 @@ const WorkflowDetail = () => { setAllowRun(hasReleased); }, [workflow.content, workflow.draft, workflow.hasDraft, isRunning]); - const handleBaseInfoFormFinish = async (values: Pick) => { - try { - await workflowState.setBaseInfo(values.name!, values.description!); - } catch (err) { - console.error(err); - notificationApi.error({ message: t("common.text.request_error"), description: getErrMsg(err) }); - return false; - } - }; - const handleEnableChange = async () => { if (!workflow.enabled && (!workflow.content || !isAllNodesValidated(workflow.content))) { messageApi.warning(t("workflow.action.enable.failed.uncompleted")); @@ -194,12 +184,7 @@ const WorkflowDetail = () => { extra={ initialized ? [ - {t("common.button.edit")}} - onFinish={handleBaseInfoFormFinish} - />, + {t("common.button.edit")}} />,