From b05860b6d2ea17db29a338659def49dc31082346 Mon Sep 17 00:00:00 2001 From: crupest Date: Tue, 29 Aug 2023 01:30:30 +0800 Subject: Refactor dialog module. --- FrontEnd/src/components/dialog/ConfirmDialog.tsx | 14 ++-- FrontEnd/src/components/dialog/Dialog.tsx | 18 ++-- FrontEnd/src/components/dialog/DialogProvider.tsx | 95 ++++++++++++++++++++++ FrontEnd/src/components/dialog/OperationDialog.tsx | 45 +++++----- FrontEnd/src/components/dialog/index.ts | 65 --------------- FrontEnd/src/components/dialog/index.tsx | 12 +++ 6 files changed, 144 insertions(+), 105 deletions(-) create mode 100644 FrontEnd/src/components/dialog/DialogProvider.tsx delete mode 100644 FrontEnd/src/components/dialog/index.ts create mode 100644 FrontEnd/src/components/dialog/index.tsx (limited to 'FrontEnd/src/components') diff --git a/FrontEnd/src/components/dialog/ConfirmDialog.tsx b/FrontEnd/src/components/dialog/ConfirmDialog.tsx index 1d997305..a7b3917f 100644 --- a/FrontEnd/src/components/dialog/ConfirmDialog.tsx +++ b/FrontEnd/src/components/dialog/ConfirmDialog.tsx @@ -1,18 +1,16 @@ import { useC, Text, ThemeColor } from "../common"; + import Dialog from "./Dialog"; import DialogContainer from "./DialogContainer"; +import { useCloseDialog } from "./DialogProvider"; export default function ConfirmDialog({ - open, - onClose, onConfirm, title, body, color, bodyColor, }: { - open: boolean; - onClose: () => void; onConfirm: () => void; title: Text; body: Text; @@ -21,8 +19,10 @@ export default function ConfirmDialog({ }) { const c = useC(); + const closeDialog = useCloseDialog(); + return ( - + { onConfirm(); - onClose(); + closeDialog(); }, }, }, diff --git a/FrontEnd/src/components/dialog/Dialog.tsx b/FrontEnd/src/components/dialog/Dialog.tsx index 2ff7bea8..b1d66704 100644 --- a/FrontEnd/src/components/dialog/Dialog.tsx +++ b/FrontEnd/src/components/dialog/Dialog.tsx @@ -5,6 +5,8 @@ import classNames from "classnames"; import { ThemeColor } from "../common"; +import { useCloseDialog } from "./DialogProvider"; + import "./Dialog.css"; const optionalPortalElement = document.getElementById("portal"); @@ -14,22 +16,20 @@ if (optionalPortalElement == null) { const portalElement = optionalPortalElement; interface DialogProps { - open: boolean; - onClose: () => void; color?: ThemeColor; children?: ReactNode; disableCloseOnClickOnOverlay?: boolean; } export default function Dialog({ - open, - onClose, color, children, disableCloseOnClickOnOverlay, }: DialogProps) { color = color ?? "primary"; + const closeDialog = useCloseDialog(); + const nodeRef = useRef(null); return ReactDOM.createPortal( @@ -37,7 +37,7 @@ export default function Dialog({ nodeRef={nodeRef} mountOnEnter unmountOnExit - in={open} + in timeout={300} classNames="cru-dialog" > @@ -47,13 +47,7 @@ export default function Dialog({ >
{ - onClose(); - } - } + onClick={disableCloseOnClickOnOverlay ? undefined : closeDialog} />
{children}
diff --git a/FrontEnd/src/components/dialog/DialogProvider.tsx b/FrontEnd/src/components/dialog/DialogProvider.tsx new file mode 100644 index 00000000..bb85e4cf --- /dev/null +++ b/FrontEnd/src/components/dialog/DialogProvider.tsx @@ -0,0 +1,95 @@ +import { useState, useContext, createContext, ReactNode } from "react"; + +import { UiLogicError } from "../common"; + +type DialogMap = { + [K in D]: ReactNode; +}; + +interface DialogController { + currentDialog: D | null; + currentDialogReactNode: ReactNode; + canSwitchDialog: boolean; + switchDialog: (newDialog: D | null) => void; + setCanSwitchDialog: (enable: boolean) => void; + closeDialog: () => void; + forceSwitchDialog: (newDialog: D | null) => void; + forceCloseDialog: () => void; +} + +export function useDialog( + dialogs: DialogMap, + options?: { + initDialog?: D | null; + onClose?: { + [K in D]?: () => void; + }; + }, +): { + controller: DialogController; + switchDialog: (newDialog: D | null) => void; + forceSwitchDialog: (newDialog: D | null) => void; + createDialogSwitch: (newDialog: D | null) => () => void; +} { + const [canSwitchDialog, setCanSwitchDialog] = useState(true); + const [dialog, setDialog] = useState(options?.initDialog ?? null); + + const forceSwitchDialog = (newDialog: D | null) => { + if (dialog != null) { + options?.onClose?.[dialog]?.(); + } + setDialog(newDialog); + setCanSwitchDialog(true); + }; + + const switchDialog = (newDialog: D | null) => { + if (canSwitchDialog) { + forceSwitchDialog(newDialog); + } + }; + + const controller: DialogController = { + currentDialog: dialog, + currentDialogReactNode: dialog == null ? null : dialogs[dialog], + canSwitchDialog, + switchDialog, + setCanSwitchDialog, + closeDialog: () => switchDialog(null), + forceSwitchDialog, + forceCloseDialog: () => forceSwitchDialog(null), + }; + + return { + controller, + switchDialog, + forceSwitchDialog, + createDialogSwitch: (newDialog: D | null) => () => switchDialog(newDialog), + }; +} + +const DialogControllerContext = createContext | null>( + null, +); + +export function useDialogController(): DialogController { + const controller = useContext(DialogControllerContext); + if (controller == null) throw new UiLogicError("not in dialog provider"); + return controller; +} + +export function useCloseDialog(): () => void { + const controller = useDialogController(); + return controller.closeDialog; +} + +export function DialogProvider({ + controller, +}: { + controller: DialogController; +}) { + return ( + + {controller.currentDialogReactNode} + + ); +} diff --git a/FrontEnd/src/components/dialog/OperationDialog.tsx b/FrontEnd/src/components/dialog/OperationDialog.tsx index 96766825..902d60c6 100644 --- a/FrontEnd/src/components/dialog/OperationDialog.tsx +++ b/FrontEnd/src/components/dialog/OperationDialog.tsx @@ -11,6 +11,7 @@ import { import { ButtonRow } from "../button"; import Dialog from "./Dialog"; import DialogContainer from "./DialogContainer"; +import { useDialogController } from "./DialogProvider"; import "./OperationDialog.css"; @@ -35,9 +36,6 @@ function OperationDialogPrompt(props: OperationDialogPromptProps) { } export interface OperationDialogProps { - open: boolean; - onClose: () => void; - color?: ThemeColor; inputColor?: ThemeColor; title: Text; @@ -56,8 +54,6 @@ export interface OperationDialogProps { function OperationDialog(props: OperationDialogProps) { const { - open, - onClose, color, inputColor, title, @@ -96,6 +92,8 @@ function OperationDialog(props: OperationDialogProps) { data: unknown; }; + const dialogController = useDialogController(); + const [step, setStep] = useState({ type: "input" }); const { inputGroupProps, hasErrorAndDirty, setAllDisabled, confirm } = @@ -105,7 +103,7 @@ function OperationDialog(props: OperationDialogProps) { function close() { if (step.type !== "process") { - onClose(); + dialogController.closeDialog(); if (step.type === "success" && onSuccessAndClose) { onSuccessAndClose?.(step.data); } @@ -118,21 +116,26 @@ function OperationDialog(props: OperationDialogProps) { const result = confirm(); if (result.type === "ok") { setStep({ type: "process" }); + dialogController.setCanSwitchDialog(false); setAllDisabled(true); - onProcess(result.values).then( - (d) => { - setStep({ - type: "success", - data: d, - }); - }, - (e: unknown) => { - setStep({ - type: "failure", - data: e, - }); - }, - ); + onProcess(result.values) + .then( + (d) => { + setStep({ + type: "success", + data: d, + }); + }, + (e: unknown) => { + setStep({ + type: "failure", + data: e, + }); + }, + ) + .finally(() => { + dialogController.setCanSwitchDialog(true); + }); } } @@ -214,7 +217,7 @@ function OperationDialog(props: OperationDialogProps) { } return ( - + {body} diff --git a/FrontEnd/src/components/dialog/index.ts b/FrontEnd/src/components/dialog/index.ts deleted file mode 100644 index 17db8fd0..00000000 --- a/FrontEnd/src/components/dialog/index.ts +++ /dev/null @@ -1,65 +0,0 @@ -import { useState } from "react"; - -export { default as Dialog } from "./Dialog"; -export { default as FullPageDialog } from "./FullPageDialog"; -export { default as OperationDialog } from "./OperationDialog"; -export { default as ConfirmDialog } from "./ConfirmDialog"; -export { default as DialogContainer } from "./DialogContainer"; - -type DialogMap = { - [K in D]: V; -}; - -type DialogKeyMap = DialogMap; - -type DialogPropsMap = DialogMap< - D, - { key: number | string; open: boolean; onClose: () => void } ->; - -export function useDialog( - dialogs: D[], - options?: { - initDialog?: D | null; - onClose?: { - [K in D]?: () => void; - }; - }, -): { - dialog: D | null; - switchDialog: (newDialog: D | null) => void; - dialogPropsMap: DialogPropsMap; - createDialogSwitch: (newDialog: D | null) => () => void; -} { - const [dialog, setDialog] = useState(options?.initDialog ?? null); - - const [dialogKeys, setDialogKeys] = useState>( - () => Object.fromEntries(dialogs.map((d) => [d, 0])) as DialogKeyMap, - ); - - const switchDialog = (newDialog: D | null) => { - if (dialog !== null) { - setDialogKeys({ ...dialogKeys, [dialog]: dialogKeys[dialog] + 1 }); - } - setDialog(newDialog); - }; - - return { - dialog, - switchDialog, - dialogPropsMap: Object.fromEntries( - dialogs.map((d) => [ - d, - { - key: `${d}-${dialogKeys[d]}`, - open: dialog === d, - onClose: () => { - switchDialog(null); - options?.onClose?.[d]?.(); - }, - }, - ]), - ) as DialogPropsMap, - createDialogSwitch: (newDialog: D | null) => () => switchDialog(newDialog), - }; -} diff --git a/FrontEnd/src/components/dialog/index.tsx b/FrontEnd/src/components/dialog/index.tsx new file mode 100644 index 00000000..9ca06de2 --- /dev/null +++ b/FrontEnd/src/components/dialog/index.tsx @@ -0,0 +1,12 @@ +export { default as Dialog } from "./Dialog"; +export { default as FullPageDialog } from "./FullPageDialog"; +export { default as OperationDialog } from "./OperationDialog"; +export { default as ConfirmDialog } from "./ConfirmDialog"; +export { default as DialogContainer } from "./DialogContainer"; + +export { + useDialog, + useDialogController, + useCloseDialog, + DialogProvider, +} from "./DialogProvider"; -- cgit v1.2.3