aboutsummaryrefslogtreecommitdiff
path: root/FrontEnd/src/components/dialog/OperationDialog.tsx
diff options
context:
space:
mode:
authorcrupest <crupest@outlook.com>2023-09-20 20:26:42 +0800
committerGitHub <noreply@github.com>2023-09-20 20:26:42 +0800
commitf836d77e73f3ea0af45c5f71dae7268143d6d86f (patch)
tree573cfafd972106d69bef0d41ff5f270ec3c43ec2 /FrontEnd/src/components/dialog/OperationDialog.tsx
parent4a069bf1268f393d5467166356f691eb89963152 (diff)
parent901fe3d7c032d284da5c9bce24c4aaee9054c7ac (diff)
downloadtimeline-f836d77e73f3ea0af45c5f71dae7268143d6d86f.tar.gz
timeline-f836d77e73f3ea0af45c5f71dae7268143d6d86f.tar.bz2
timeline-f836d77e73f3ea0af45c5f71dae7268143d6d86f.zip
Merge pull request #1395 from crupest/dev
Refector 2023 v0.1
Diffstat (limited to 'FrontEnd/src/components/dialog/OperationDialog.tsx')
-rw-r--r--FrontEnd/src/components/dialog/OperationDialog.tsx221
1 files changed, 221 insertions, 0 deletions
diff --git a/FrontEnd/src/components/dialog/OperationDialog.tsx b/FrontEnd/src/components/dialog/OperationDialog.tsx
new file mode 100644
index 00000000..6ca4d0a0
--- /dev/null
+++ b/FrontEnd/src/components/dialog/OperationDialog.tsx
@@ -0,0 +1,221 @@
+import { useState, ReactNode, ComponentProps } from "react";
+import classNames from "classnames";
+
+import { useC, Text, ThemeColor } from "../common";
+import {
+ useInputs,
+ InputGroup,
+ Initializer as InputInitializer,
+ InputConfirmValueDict,
+} from "../input";
+import { ButtonRowV2 } from "../button";
+import Dialog from "./Dialog";
+import DialogContainer from "./DialogContainer";
+import { useDialogController } from "./DialogProvider";
+
+import "./OperationDialog.css";
+
+interface OperationDialogPromptProps {
+ message?: Text;
+ customMessage?: Text;
+ customMessageNode?: ReactNode;
+ className?: string;
+}
+
+function OperationDialogPrompt(props: OperationDialogPromptProps) {
+ const { message, customMessage, customMessageNode, className } = props;
+
+ const c = useC();
+
+ return (
+ <div className={classNames(className, "cru-operation-dialog-prompt")}>
+ {message && <p>{c(message)}</p>}
+ {customMessageNode ?? (customMessage != null ? c(customMessage) : null)}
+ </div>
+ );
+}
+
+export interface OperationDialogProps<TData> {
+ color?: ThemeColor;
+ inputColor?: ThemeColor;
+ title: Text;
+ inputPrompt?: Text;
+ inputPromptNode?: ReactNode;
+ successPrompt?: (data: TData) => Text;
+ successPromptNode?: (data: TData) => ReactNode;
+ failurePrompt?: (error: unknown) => Text;
+ failurePromptNode?: (error: unknown) => ReactNode;
+
+ inputs: InputInitializer;
+
+ onProcess: (inputs: InputConfirmValueDict) => Promise<TData>;
+ onSuccessAndClose?: (data: TData) => void;
+}
+
+function OperationDialog<TData>(props: OperationDialogProps<TData>) {
+ const {
+ color,
+ inputColor,
+ title,
+ inputPrompt,
+ inputPromptNode,
+ successPrompt,
+ successPromptNode,
+ failurePrompt,
+ failurePromptNode,
+ inputs,
+ onProcess,
+ onSuccessAndClose,
+ } = props;
+
+ if (process.env.NODE_ENV === "development") {
+ if (inputPrompt && inputPromptNode) {
+ console.log("InputPrompt and inputPromptNode are both set.");
+ }
+ if (successPrompt && successPromptNode) {
+ console.log("SuccessPrompt and successPromptNode are both set.");
+ }
+ if (failurePrompt && failurePromptNode) {
+ console.log("FailurePrompt and failurePromptNode are both set.");
+ }
+ }
+
+ type Step =
+ | { type: "input" }
+ | { type: "process" }
+ | {
+ type: "success";
+ data: TData;
+ }
+ | {
+ type: "failure";
+ data: unknown;
+ };
+
+ const dialogController = useDialogController();
+
+ const [step, setStep] = useState<Step>({ type: "input" });
+
+ const { inputGroupProps, hasErrorAndDirty, setAllDisabled, confirm } =
+ useInputs({
+ init: inputs,
+ });
+
+ function close() {
+ if (step.type !== "process") {
+ dialogController.closeDialog();
+ if (step.type === "success" && onSuccessAndClose) {
+ onSuccessAndClose?.(step.data);
+ }
+ } else {
+ console.log("Attempt to close modal dialog when processing.");
+ }
+ }
+
+ function onConfirm() {
+ 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,
+ });
+ },
+ )
+ .finally(() => {
+ dialogController.setCanSwitchDialog(true);
+ });
+ }
+ }
+
+ let body: ReactNode;
+ let buttons: ComponentProps<typeof ButtonRowV2>["buttons"];
+
+ if (step.type === "input" || step.type === "process") {
+ const isProcessing = step.type === "process";
+
+ body = (
+ <div>
+ <OperationDialogPrompt
+ customMessage={inputPrompt}
+ customMessageNode={inputPromptNode}
+ />
+ <InputGroup
+ containerClassName="cru-operation-dialog-input-group"
+ color={inputColor ?? "primary"}
+ {...inputGroupProps}
+ />
+ </div>
+ );
+ buttons = [
+ {
+ key: "cancel",
+ text: "operationDialog.cancel",
+ onClick: close,
+ disabled: isProcessing,
+ },
+ {
+ key: "confirm",
+ type: "loading",
+ action: "major",
+ text: "operationDialog.confirm",
+ color,
+ loading: isProcessing,
+ disabled: hasErrorAndDirty,
+ onClick: onConfirm,
+ },
+ ];
+ } else {
+ const result = step;
+
+ const promptProps: OperationDialogPromptProps =
+ result.type === "success"
+ ? {
+ message: "operationDialog.success",
+ customMessage: successPrompt?.(result.data),
+ customMessageNode: successPromptNode?.(result.data),
+ }
+ : {
+ message: "operationDialog.error",
+ customMessage: failurePrompt?.(result.data),
+ customMessageNode: failurePromptNode?.(result.data),
+ };
+ body = (
+ <div>
+ <OperationDialogPrompt {...promptProps} />
+ </div>
+ );
+
+ buttons = [
+ {
+ key: "ok",
+ type: "normal",
+ action: "major",
+ color: "create",
+ text: "operationDialog.ok",
+ onClick: close,
+ },
+ ];
+ }
+
+ return (
+ <Dialog color={color}>
+ <DialogContainer title={title} titleColor={color} buttonsV2={buttons}>
+ {body}
+ </DialogContainer>
+ </Dialog>
+ );
+}
+
+export default OperationDialog;