aboutsummaryrefslogtreecommitdiff
path: root/FrontEnd/src/components
diff options
context:
space:
mode:
authorcrupest <crupest@outlook.com>2023-08-29 01:30:30 +0800
committercrupest <crupest@outlook.com>2023-08-29 01:30:30 +0800
commitb05860b6d2ea17db29a338659def49dc31082346 (patch)
treecbbd9d99af5e6b006f142effeae5222278dad02f /FrontEnd/src/components
parent502dd817c79018c84b0a958dd4b2e24781e68ae1 (diff)
downloadtimeline-b05860b6d2ea17db29a338659def49dc31082346.tar.gz
timeline-b05860b6d2ea17db29a338659def49dc31082346.tar.bz2
timeline-b05860b6d2ea17db29a338659def49dc31082346.zip
Refactor dialog module.
Diffstat (limited to 'FrontEnd/src/components')
-rw-r--r--FrontEnd/src/components/dialog/ConfirmDialog.tsx14
-rw-r--r--FrontEnd/src/components/dialog/Dialog.tsx18
-rw-r--r--FrontEnd/src/components/dialog/DialogProvider.tsx95
-rw-r--r--FrontEnd/src/components/dialog/OperationDialog.tsx45
-rw-r--r--FrontEnd/src/components/dialog/index.ts65
-rw-r--r--FrontEnd/src/components/dialog/index.tsx12
6 files changed, 144 insertions, 105 deletions
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 (
- <Dialog onClose={onClose} open={open}>
+ <Dialog>
<DialogContainer
title={title}
titleColor={color ?? "danger"}
@@ -34,7 +34,7 @@ export default function ConfirmDialog({
text: "operationDialog.cancel",
color: "secondary",
outline: true,
- onClick: onClose,
+ onClick: closeDialog,
},
},
{
@@ -45,7 +45,7 @@ export default function ConfirmDialog({
color: "danger",
onClick: () => {
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({
>
<div
className="cru-dialog-background"
- onClick={
- disableCloseOnClickOnOverlay
- ? undefined
- : () => {
- onClose();
- }
- }
+ onClick={disableCloseOnClickOnOverlay ? undefined : closeDialog}
/>
<div className="cru-dialog-container">{children}</div>
</div>
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<D extends string> = {
+ [K in D]: ReactNode;
+};
+
+interface DialogController<D extends string> {
+ 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<D extends string>(
+ dialogs: DialogMap<D>,
+ options?: {
+ initDialog?: D | null;
+ onClose?: {
+ [K in D]?: () => void;
+ };
+ },
+): {
+ controller: DialogController<D>;
+ switchDialog: (newDialog: D | null) => void;
+ forceSwitchDialog: (newDialog: D | null) => void;
+ createDialogSwitch: (newDialog: D | null) => () => void;
+} {
+ const [canSwitchDialog, setCanSwitchDialog] = useState<boolean>(true);
+ const [dialog, setDialog] = useState<D | null>(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<D> = {
+ 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<DialogController<string> | null>(
+ null,
+);
+
+export function useDialogController(): DialogController<string> {
+ 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<D extends string>({
+ controller,
+}: {
+ controller: DialogController<D>;
+}) {
+ return (
+ <DialogControllerContext.Provider value={controller as never}>
+ {controller.currentDialogReactNode}
+ </DialogControllerContext.Provider>
+ );
+}
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<TData> {
- open: boolean;
- onClose: () => void;
-
color?: ThemeColor;
inputColor?: ThemeColor;
title: Text;
@@ -56,8 +54,6 @@ export interface OperationDialogProps<TData> {
function OperationDialog<TData>(props: OperationDialogProps<TData>) {
const {
- open,
- onClose,
color,
inputColor,
title,
@@ -96,6 +92,8 @@ function OperationDialog<TData>(props: OperationDialogProps<TData>) {
data: unknown;
};
+ const dialogController = useDialogController();
+
const [step, setStep] = useState<Step>({ type: "input" });
const { inputGroupProps, hasErrorAndDirty, setAllDisabled, confirm } =
@@ -105,7 +103,7 @@ function OperationDialog<TData>(props: OperationDialogProps<TData>) {
function close() {
if (step.type !== "process") {
- onClose();
+ dialogController.closeDialog();
if (step.type === "success" && onSuccessAndClose) {
onSuccessAndClose?.(step.data);
}
@@ -118,21 +116,26 @@ function OperationDialog<TData>(props: OperationDialogProps<TData>) {
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<TData>(props: OperationDialogProps<TData>) {
}
return (
- <Dialog open={open} onClose={close}>
+ <Dialog>
<DialogContainer title={title} titleColor={color} buttons={buttons}>
{body}
</DialogContainer>
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<D extends string, V> = {
- [K in D]: V;
-};
-
-type DialogKeyMap<D extends string> = DialogMap<D, number>;
-
-type DialogPropsMap<D extends string> = DialogMap<
- D,
- { key: number | string; open: boolean; onClose: () => void }
->;
-
-export function useDialog<D extends string>(
- dialogs: D[],
- options?: {
- initDialog?: D | null;
- onClose?: {
- [K in D]?: () => void;
- };
- },
-): {
- dialog: D | null;
- switchDialog: (newDialog: D | null) => void;
- dialogPropsMap: DialogPropsMap<D>;
- createDialogSwitch: (newDialog: D | null) => () => void;
-} {
- const [dialog, setDialog] = useState<D | null>(options?.initDialog ?? null);
-
- const [dialogKeys, setDialogKeys] = useState<DialogKeyMap<D>>(
- () => Object.fromEntries(dialogs.map((d) => [d, 0])) as DialogKeyMap<D>,
- );
-
- 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<D>,
- 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";