aboutsummaryrefslogtreecommitdiff
path: root/FrontEnd/src/views/common/dialog
diff options
context:
space:
mode:
authorcrupest <crupest@outlook.com>2023-07-24 00:31:02 +0800
committercrupest <crupest@outlook.com>2023-07-24 00:31:02 +0800
commitc7934c59cb17a4266ea882cdb40be79f22043d10 (patch)
treeff368fe5611014cd20db5a1f53d4dcbd5fdabf9f /FrontEnd/src/views/common/dialog
parentae1b296b5e967d1e329f5a1e6165ca0f05dce0cb (diff)
downloadtimeline-c7934c59cb17a4266ea882cdb40be79f22043d10.tar.gz
timeline-c7934c59cb17a4266ea882cdb40be79f22043d10.tar.bz2
timeline-c7934c59cb17a4266ea882cdb40be79f22043d10.zip
...
Diffstat (limited to 'FrontEnd/src/views/common/dialog')
-rw-r--r--FrontEnd/src/views/common/dialog/OperationDialog.tsx375
1 files changed, 165 insertions, 210 deletions
diff --git a/FrontEnd/src/views/common/dialog/OperationDialog.tsx b/FrontEnd/src/views/common/dialog/OperationDialog.tsx
index 71be030a..ad00c424 100644
--- a/FrontEnd/src/views/common/dialog/OperationDialog.tsx
+++ b/FrontEnd/src/views/common/dialog/OperationDialog.tsx
@@ -1,13 +1,8 @@
-import { useState } from "react";
-import * as React from "react";
-import { useTranslation } from "react-i18next";
-import { TwitterPicker } from "react-color";
+import { useState, ReactNode, ComponentPropsWithoutRef } from "react";
import classNames from "classnames";
import moment from "moment";
-import { convertI18nText, I18nText, UiLogicError } from "@/common";
-
-import { PaletteColorType } from "@/palette";
+import { useC, Text, ThemeColor } from "../common";
import Button from "../button/Button";
import LoadingButton from "../button/LoadingButton";
@@ -15,69 +10,61 @@ import Dialog from "./Dialog";
import "./OperationDialog.css";
-interface DefaultErrorPromptProps {
- error?: string;
+interface DefaultPromptProps {
+ color?: ThemeColor;
+ message?: Text;
+ customMessage?: ReactNode;
+ className?: string;
}
-const DefaultErrorPrompt: React.FC<DefaultErrorPromptProps> = (props) => {
- const { t } = useTranslation();
+function DefaultPrompt(props: DefaultPromptProps) {
+ const { color, message, customMessage, className } = props;
- let result = <p className="cru-color-danger">{t("operationDialog.error")}</p>;
+ const c = useC();
- if (props.error != null) {
- result = (
- <>
- {result}
- <p className="cru-color-danger">{props.error}</p>
- </>
- );
- }
-
- return result;
-};
+ return (
+ <div className={classNames(className, `cru-${color ?? "primary"}`)}>
+ <p>{c(message)}</p>
+ {customMessage}
+ </div>
+ );
+}
export interface OperationDialogTextInput {
type: "text";
- label?: I18nText;
+ label?: Text;
password?: boolean;
initValue?: string;
textFieldProps?: Omit<
- React.InputHTMLAttributes<HTMLInputElement>,
+ ComponentPropsWithoutRef<"input">,
"type" | "value" | "onChange"
>;
- helperText?: string;
+ helperText?: Text;
}
export interface OperationDialogBoolInput {
type: "bool";
- label: I18nText;
+ label: Text;
initValue?: boolean;
- helperText?: string;
+ helperText?: Text;
}
export interface OperationDialogSelectInputOption {
value: string;
- label: I18nText;
- icon?: React.ReactElement;
+ label: Text;
+ icon?: ReactNode;
}
export interface OperationDialogSelectInput {
type: "select";
- label: I18nText;
+ label: Text;
options: OperationDialogSelectInputOption[];
initValue?: string;
}
-export interface OperationDialogColorInput {
- type: "color";
- label?: I18nText;
- initValue?: string | null;
- canBeNull?: boolean;
-}
-
export interface OperationDialogDateTimeInput {
type: "datetime";
- label?: I18nText;
+ label?: Text;
initValue?: string;
helperText?: string;
}
@@ -86,17 +73,18 @@ export type OperationDialogInput =
| OperationDialogTextInput
| OperationDialogBoolInput
| OperationDialogSelectInput
- | OperationDialogColorInput
| OperationDialogDateTimeInput;
interface OperationInputTypeStringToValueTypeMap {
text: string;
bool: boolean;
select: string;
- color: string | null;
datetime: string;
}
+type OperationInputValueType =
+ OperationInputTypeStringToValueTypeMap[keyof OperationInputTypeStringToValueTypeMap];
+
type MapOperationInputTypeStringToValueType<Type> =
Type extends keyof OperationInputTypeStringToValueTypeMap
? OperationInputTypeStringToValueTypeMap[Type]
@@ -106,33 +94,15 @@ type MapOperationInputInfoValueType<T> = T extends OperationDialogInput
? MapOperationInputTypeStringToValueType<T["type"]>
: T;
-const initValueMapperMap: {
- [T in OperationDialogInput as T["type"]]: (
- item: T
- ) => MapOperationInputInfoValueType<T>;
-} = {
- bool: (item) => item.initValue ?? false,
- color: (item) => item.initValue ?? null,
- datetime: (item) => {
- if (item.initValue != null) {
- return moment(item.initValue).format("YYYY-MM-DDTHH:mm:ss");
- } else {
- return "";
- }
- },
- select: (item) => item.initValue ?? item.options[0].value,
- text: (item) => item.initValue ?? "",
-};
-
type MapOperationInputInfoValueTypeList<
- Tuple extends readonly OperationDialogInput[]
+ Tuple extends readonly OperationDialogInput[],
> = {
[Index in keyof Tuple]: MapOperationInputInfoValueType<Tuple[Index]>;
-} & { length: Tuple["length"] };
+};
export type OperationInputError =
| {
- [index: number]: I18nText | null | undefined;
+ [index: number]: Text | null | undefined;
}
| null
| undefined;
@@ -145,38 +115,68 @@ const isNoError = (error: OperationInputError): boolean => {
return true;
};
+type ItemValueMapper = {
+ [T in OperationDialogInput as T["type"]]: (
+ item: T,
+ ) => MapOperationInputInfoValueType<T>;
+};
+
+type ValueValueMapper = {
+ [T in OperationDialogInput as T["type"]]: (
+ item: MapOperationInputInfoValueType<T>,
+ ) => MapOperationInputInfoValueType<T>;
+};
+
+const initValueMapperMap: ItemValueMapper = {
+ bool: (item) => item.initValue ?? false,
+ datetime: (item) =>
+ item.initValue != null
+ ? /* cspell: disable-next-line */
+ moment(item.initValue).format("YYYY-MM-DDTHH:mm:ss")
+ : "",
+ select: (item) => item.initValue ?? item.options[0].value,
+ text: (item) => item.initValue ?? "",
+};
+
+const finalValueMapperMap: ValueValueMapper = {
+ bool: (value) => value,
+ datetime: (value) => new Date(value).toISOString(),
+ select: (value) => value,
+ text: (value) => value,
+};
+
export interface OperationDialogProps<
TData,
- OperationInputInfoList extends readonly OperationDialogInput[]
+ OperationInputInfoList extends readonly OperationDialogInput[],
> {
open: boolean;
onClose: () => void;
- title: I18nText | (() => React.ReactNode);
- themeColor?: PaletteColorType;
- onProcess: (
- inputs: MapOperationInputInfoValueTypeList<OperationInputInfoList>
- ) => Promise<TData>;
+
+ themeColor?: ThemeColor;
+ title: Text;
+ inputPrompt?: Text;
+ processPrompt?: Text;
+ successPrompt?: (data: TData) => ReactNode;
+ failurePrompt?: (error: unknown) => ReactNode;
+
inputScheme?: OperationInputInfoList;
inputValidator?: (
- inputs: MapOperationInputInfoValueTypeList<OperationInputInfoList>
+ inputs: MapOperationInputInfoValueTypeList<OperationInputInfoList>,
) => OperationInputError;
- inputPrompt?: I18nText | (() => React.ReactNode);
- processPrompt?: () => React.ReactNode;
- successPrompt?: (data: TData) => React.ReactNode;
- failurePrompt?: (error: unknown) => React.ReactNode;
+
+ onProcess: (
+ inputs: MapOperationInputInfoValueTypeList<OperationInputInfoList>,
+ ) => Promise<TData>;
onSuccessAndClose?: (data: TData) => void;
}
-const OperationDialog = <
+function OperationDialog<
TData,
- OperationInputInfoList extends readonly OperationDialogInput[]
->(
- props: OperationDialogProps<TData, OperationInputInfoList>
-): React.ReactElement => {
- const inputScheme = (props.inputScheme ??
- []) as readonly OperationDialogInput[];
+ OperationInputInfoList extends readonly OperationDialogInput[],
+>(props: OperationDialogProps<TData, OperationInputInfoList>) {
+ const inputScheme = props.inputScheme ?? ([] as const);
- const { t } = useTranslation();
+ const c = useC();
type Step =
| { type: "input" }
@@ -189,48 +189,42 @@ const OperationDialog = <
type: "failure";
data: unknown;
};
+
const [step, setStep] = useState<Step>({ type: "input" });
- type ValueType = boolean | string | null | undefined;
-
- const [values, setValues] = useState<ValueType[]>(
- inputScheme.map((item) => {
- if (item.type in initValueMapperMap) {
- return (
- initValueMapperMap[item.type] as (
- i: OperationDialogInput
- ) => ValueType
- )(item);
- } else {
- throw new UiLogicError("Unknown input scheme.");
- }
- })
+ type Values = MapOperationInputInfoValueTypeList<OperationInputInfoList>;
+
+ const [values, setValues] = useState<Values>(
+ () =>
+ inputScheme.map((item) =>
+ initValueMapperMap[item.type](item as never),
+ ) as Values,
);
+
const [dirtyList, setDirtyList] = useState<boolean[]>(() =>
- inputScheme.map(() => false)
+ inputScheme.map(() => false),
);
+
const [inputError, setInputError] = useState<OperationInputError>();
- const close = (): void => {
+ function close() {
if (step.type !== "process") {
props.onClose();
if (step.type === "success" && props.onSuccessAndClose) {
props.onSuccessAndClose(step.data);
}
} else {
- console.log("Attempt to close modal when processing.");
+ console.log("Attempt to close modal dialog when processing.");
}
- };
+ }
- const onConfirm = (): void => {
+ function onConfirm() {
setStep({ type: "process" });
props
.onProcess(
- values.map((v, index) => {
- if (inputScheme[index].type === "datetime" && v !== "")
- return new Date(v as string).toISOString();
- else return v;
- }) as unknown as MapOperationInputInfoValueTypeList<OperationInputInfoList>
+ values.map((value, index) =>
+ finalValueMapperMap[inputScheme[index].type](value as never),
+ ) as Values,
)
.then(
(d) => {
@@ -244,56 +238,51 @@ const OperationDialog = <
type: "failure",
data: e,
});
- }
+ },
);
- };
+ }
- let body: React.ReactNode;
+ let body: ReactNode;
if (step.type === "input" || step.type === "process") {
const process = step.type === "process";
- let inputPrompt =
- typeof props.inputPrompt === "function"
- ? props.inputPrompt()
- : convertI18nText(props.inputPrompt, t);
- inputPrompt = <h6>{inputPrompt}</h6>;
-
- const validate = (values: ValueType[]): boolean => {
+ const validate = (values: Values): boolean => {
const { inputValidator } = props;
if (inputValidator != null) {
- const result = inputValidator(
- values as unknown as MapOperationInputInfoValueTypeList<OperationInputInfoList>
- );
+ const result = inputValidator(values);
setInputError(result);
return isNoError(result);
}
return true;
};
- const updateValue = (index: number, newValue: ValueType): void => {
+ const updateValue = (
+ index: number,
+ newValue: OperationInputValueType,
+ ): void => {
const oldValues = values;
const newValues = oldValues.slice();
newValues[index] = newValue;
- setValues(newValues);
+ setValues(newValues as Values);
if (dirtyList[index] === false) {
const newDirtyList = dirtyList.slice();
newDirtyList[index] = true;
setDirtyList(newDirtyList);
}
- validate(newValues);
+ validate(newValues as Values);
};
const canProcess = isNoError(inputError);
body = (
- <>
+ <div className="cru-operation-dialog-main-area">
<div>
- {inputPrompt}
- {inputScheme.map((item, index) => {
+ <div>{c(props.inputPrompt)}</div>
+ {inputScheme.map((item: OperationDialogInput, index: number) => {
const value = values[index];
const error: string | null =
dirtyList[index] && inputError != null
- ? convertI18nText(inputError[index], t)
+ ? c(inputError[index])
: null;
if (item.type === "text") {
@@ -302,31 +291,31 @@ const OperationDialog = <
key={index}
className={classNames(
"cru-operation-dialog-group",
- error != null ? "error" : null
+ error && "error",
)}
>
{item.label && (
<label className="cru-operation-dialog-label">
- {convertI18nText(item.label, t)}
+ {c(item.label)}
</label>
)}
<input
type={item.password === true ? "password" : "text"}
value={value as string}
- onChange={(e) => {
- const v = e.target.value;
+ onChange={(event) => {
+ const v = event.target.value;
updateValue(index, v);
}}
disabled={process}
/>
- {error != null && (
+ {error && (
<div className="cru-operation-dialog-error-text">
{error}
</div>
)}
{item.helperText && (
<div className="cru-operation-dialog-helper-text">
- {t(item.helperText)}
+ {c(item.helperText)}
</div>
)}
</div>
@@ -337,28 +326,29 @@ const OperationDialog = <
key={index}
className={classNames(
"cru-operation-dialog-group",
- error != null ? "error" : null
+ error && "error",
)}
>
<input
type="checkbox"
checked={value as boolean}
onChange={(event) => {
- updateValue(index, event.currentTarget.checked);
+ const v = event.currentTarget.checked;
+ updateValue(index, v);
}}
disabled={process}
/>
<label className="cru-operation-dialog-inline-label">
- {convertI18nText(item.label, t)}
+ {c(item.label)}
</label>
- {error != null && (
+ {error && (
<div className="cru-operation-dialog-error-text">
{error}
</div>
)}
{item.helperText && (
<div className="cru-operation-dialog-helper-text">
- {t(item.helperText)}
+ {c(item.helperText)}
</div>
)}
</div>
@@ -369,16 +359,17 @@ const OperationDialog = <
key={index}
className={classNames(
"cru-operation-dialog-group",
- error != null ? "error" : null
+ error && "error",
)}
>
<label className="cru-operation-dialog-label">
- {convertI18nText(item.label, t)}
+ {c(item.label)}
</label>
<select
value={value as string}
onChange={(event) => {
- updateValue(index, event.target.value);
+ const e = event.target.value;
+ updateValue(index, e);
}}
disabled={process}
>
@@ -386,72 +377,41 @@ const OperationDialog = <
return (
<option value={option.value} key={i}>
{option.icon}
- {convertI18nText(option.label, t)}
+ {c(option.label)}
</option>
);
})}
</select>
</div>
);
- } else if (item.type === "color") {
- return (
- <div
- key={index}
- className={classNames(
- "cru-operation-dialog-group",
- error != null ? "error" : null
- )}
- >
- {item.canBeNull ? (
- <input
- type="checkbox"
- checked={value !== null}
- onChange={(event) => {
- if (event.currentTarget.checked) {
- updateValue(index, "#007bff");
- } else {
- updateValue(index, null);
- }
- }}
- disabled={process}
- />
- ) : null}
- <label className="cru-operation-dialog-inline-label">
- {convertI18nText(item.label, t)}
- </label>
- {value !== null && (
- <TwitterPicker
- color={value as string}
- triangle="hide"
- onChange={(result) => updateValue(index, result.hex)}
- />
- )}
- </div>
- );
} else if (item.type === "datetime") {
return (
<div
key={index}
className={classNames(
"cru-operation-dialog-group",
- error != null ? "error" : null
+ error && "error",
)}
>
{item.label && (
<label className="cru-operation-dialog-label">
- {convertI18nText(item.label, t)}
+ {c(item.label)}
</label>
)}
<input
type="datetime-local"
value={value as string}
- onChange={(e) => {
- const v = e.target.value;
+ onChange={(event) => {
+ const v = event.target.value;
updateValue(index, v);
}}
disabled={process}
/>
- {error != null && <div>{error}</div>}
+ {error && (
+ <div className="cru-operation-dialog-error-text">
+ {error}
+ </div>
+ )}
</div>
);
}
@@ -477,55 +437,50 @@ const OperationDialog = <
}
}}
>
- {t("operationDialog.confirm")}
+ {c("operationDialog.confirm")}
</LoadingButton>
</div>
- </>
+ </div>
);
} else {
- let content: React.ReactNode;
const result = step;
- if (result.type === "success") {
- content =
- props.successPrompt?.(result.data) ?? t("operationDialog.success");
- if (typeof content === "string")
- content = <p className="cru-color-success">{content}</p>;
- } else {
- content = props.failurePrompt?.(result.data) ?? <DefaultErrorPrompt />;
- if (typeof content === "string")
- content = <DefaultErrorPrompt error={content} />;
- }
+
+ const promptProps: DefaultPromptProps =
+ result.type === "success"
+ ? {
+ color: "success",
+ message: "operationDialog.success",
+ customMessage: props.successPrompt?.(result.data),
+ }
+ : {
+ color: "danger",
+ message: "operationDialog.error",
+ customMessage: props.failurePrompt?.(result.data),
+ };
body = (
- <>
- <div>{content}</div>
+ <div className="cru-operation-dialog-main-area">
+ <DefaultPrompt {...promptProps} />
<hr />
<div className="cru-dialog-bottom-area">
<Button text="operationDialog.ok" color="primary" onClick={close} />
</div>
- </>
+ </div>
);
}
- const title =
- typeof props.title === "function"
- ? props.title()
- : convertI18nText(props.title, t);
-
return (
<Dialog open={props.open} onClose={close}>
- <h3
- className={
- props.themeColor != null
- ? "cru-color-" + props.themeColor
- : "cru-color-primary"
- }
+ <div
+ className={`cru-operation-dialog-title cru-${
+ props.themeColor ?? "primary"
+ }`}
>
- {title}
- </h3>
+ {c(props.title)}
+ </div>
<hr />
{body}
</Dialog>
);
-};
+}
export default OperationDialog;