aboutsummaryrefslogtreecommitdiff
path: root/FrontEnd/src/views/common/dialog
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/views/common/dialog
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/views/common/dialog')
-rw-r--r--FrontEnd/src/views/common/dialog/ConfirmDialog.tsx43
-rw-r--r--FrontEnd/src/views/common/dialog/Dialog.css55
-rw-r--r--FrontEnd/src/views/common/dialog/Dialog.tsx51
-rw-r--r--FrontEnd/src/views/common/dialog/FullPageDialog.css44
-rw-r--r--FrontEnd/src/views/common/dialog/FullPageDialog.tsx53
-rw-r--r--FrontEnd/src/views/common/dialog/OperationDialog.css25
-rw-r--r--FrontEnd/src/views/common/dialog/OperationDialog.tsx531
7 files changed, 0 insertions, 802 deletions
diff --git a/FrontEnd/src/views/common/dialog/ConfirmDialog.tsx b/FrontEnd/src/views/common/dialog/ConfirmDialog.tsx
deleted file mode 100644
index 8c2cea5a..00000000
--- a/FrontEnd/src/views/common/dialog/ConfirmDialog.tsx
+++ /dev/null
@@ -1,43 +0,0 @@
-import { convertI18nText, I18nText } from "@/common";
-import * as React from "react";
-import { useTranslation } from "react-i18next";
-
-import Button from "../button/Button";
-import Dialog from "./Dialog";
-
-const ConfirmDialog: React.FC<{
- open: boolean;
- onClose: () => void;
- onConfirm: () => void;
- title: I18nText;
- body: I18nText;
-}> = ({ open, onClose, onConfirm, title, body }) => {
- const { t } = useTranslation();
-
- return (
- <Dialog onClose={onClose} open={open}>
- <h3 className="cru-color-danger">{convertI18nText(title, t)}</h3>
- <hr />
- <p>{convertI18nText(body, t)}</p>
- <hr />
- <div className="cru-dialog-bottom-area">
- <Button
- text="operationDialog.cancel"
- color="secondary"
- outline
- onClick={onClose}
- />
- <Button
- text="operationDialog.confirm"
- color="danger"
- onClick={() => {
- onConfirm();
- onClose();
- }}
- />
- </div>
- </Dialog>
- );
-};
-
-export default ConfirmDialog;
diff --git a/FrontEnd/src/views/common/dialog/Dialog.css b/FrontEnd/src/views/common/dialog/Dialog.css
deleted file mode 100644
index 21ea52fc..00000000
--- a/FrontEnd/src/views/common/dialog/Dialog.css
+++ /dev/null
@@ -1,55 +0,0 @@
-.cru-dialog-overlay {
- position: fixed;
- z-index: 1040;
- left: 0;
- top: 0;
- right: 0;
- bottom: 0;
- background-color: rgba(255, 255, 255, 0.92);
-
- display: flex;
- padding: 2em;
-
- overflow: auto;
-}
-
-.cru-dialog-container {
- max-width: 100%;
- min-width: 30vw;
-
- margin: auto;
-
- border: var(--cru-primary-color) 1px solid;
- border-radius: 5px;
- padding: 1.5em;
- background-color: white;
-}
-
-.cru-dialog-bottom-area {
- display: flex;
- justify-content: flex-end;
-}
-
-.cru-dialog-bottom-area > * {
- margin: 0 0.5em;
-}
-
-.cru-dialog-enter .cru-dialog-container {
- transform: scale(0, 0);
- opacity: 0;
- transform-origin: center;
-}
-
-.cru-dialog-enter-active .cru-dialog-container {
- transform: scale(1, 1);
- opacity: 1;
- transition: transform 0.3s, opacity 0.3s;
- transform-origin: center;
-}
-
-.cru-dialog-exit-active .cru-dialog-container {
- transition: transform 0.3s, opacity 0.3s;
- transform: scale(0, 0);
- opacity: 0;
- transform-origin: center;
-}
diff --git a/FrontEnd/src/views/common/dialog/Dialog.tsx b/FrontEnd/src/views/common/dialog/Dialog.tsx
deleted file mode 100644
index 923c636b..00000000
--- a/FrontEnd/src/views/common/dialog/Dialog.tsx
+++ /dev/null
@@ -1,51 +0,0 @@
-import { ReactNode } from "react";
-import ReactDOM from "react-dom";
-import { CSSTransition } from "react-transition-group";
-
-import "./Dialog.css";
-
-const optionalPortalElement = document.getElementById("portal");
-if (optionalPortalElement == null) {
- throw new Error("Portal element not found");
-}
-const portalElement = optionalPortalElement;
-
-interface DialogProps {
- onClose: () => void;
- open: boolean;
- children?: ReactNode;
- disableCloseOnClickOnOverlay?: boolean;
-}
-
-export default function Dialog(props: DialogProps) {
- const { open, onClose, children, disableCloseOnClickOnOverlay } = props;
-
- return ReactDOM.createPortal(
- <CSSTransition
- mountOnEnter
- unmountOnExit
- in={open}
- timeout={300}
- classNames="cru-dialog"
- >
- <div
- className="cru-dialog-overlay"
- onPointerDown={
- disableCloseOnClickOnOverlay
- ? undefined
- : () => {
- onClose();
- }
- }
- >
- <div
- className="cru-dialog-container"
- onPointerDown={(e) => e.stopPropagation()}
- >
- {children}
- </div>
- </div>
- </CSSTransition>,
- portalElement,
- );
-}
diff --git a/FrontEnd/src/views/common/dialog/FullPageDialog.css b/FrontEnd/src/views/common/dialog/FullPageDialog.css
deleted file mode 100644
index 2f1fc636..00000000
--- a/FrontEnd/src/views/common/dialog/FullPageDialog.css
+++ /dev/null
@@ -1,44 +0,0 @@
-.cru-full-page {
- position: fixed;
- z-index: 1030;
- left: 0;
- top: 0;
- right: 0;
- bottom: 0;
- background-color: white;
- padding-top: 56px;
-}
-
-.cru-full-page-top-bar {
- height: 56px;
- position: absolute;
- top: 0;
- left: 0;
- right: 0;
- z-index: 1;
- background-color: var(--cru-primary-color);
- display: flex;
- align-items: center;
-}
-
-.cru-full-page-content-container {
- overflow: scroll;
-}
-
-.cru-full-page-back-button {
- color: var(--cru-primary-t-color);
-}
-
-.cru-full-page-enter {
- transform: translate(100%, 0);
-}
-
-.cru-full-page-enter-active {
- transform: none;
- transition: transform 0.3s;
-}
-
-.cru-full-page-exit-active {
- transition: transform 0.3s;
- transform: translate(100%, 0);
-}
diff --git a/FrontEnd/src/views/common/dialog/FullPageDialog.tsx b/FrontEnd/src/views/common/dialog/FullPageDialog.tsx
deleted file mode 100644
index 6368fc0a..00000000
--- a/FrontEnd/src/views/common/dialog/FullPageDialog.tsx
+++ /dev/null
@@ -1,53 +0,0 @@
-import * as React from "react";
-import { createPortal } from "react-dom";
-import classnames from "classnames";
-import { CSSTransition } from "react-transition-group";
-
-import "./FullPageDialog.css";
-import IconButton from "../button/IconButton";
-
-export interface FullPageDialogProps {
- show: boolean;
- onBack: () => void;
- contentContainerClassName?: string;
- children: React.ReactNode;
-}
-
-const FullPageDialog: React.FC<FullPageDialogProps> = ({
- show,
- onBack,
- children,
- contentContainerClassName,
-}) => {
- return createPortal(
- <CSSTransition
- mountOnEnter
- unmountOnExit
- in={show}
- timeout={300}
- classNames="cru-full-page"
- >
- <div className="cru-full-page">
- <div className="cru-full-page-top-bar">
- <IconButton
- icon="arrow-left"
- className="ms-3 cru-full-page-back-button"
- onClick={onBack}
- />
- </div>
- <div
- className={classnames(
- "cru-full-page-content-container",
- contentContainerClassName
- )}
- >
- {children}
- </div>
- </div>
- </CSSTransition>,
- // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
- document.getElementById("portal")!
- );
-};
-
-export default FullPageDialog;
diff --git a/FrontEnd/src/views/common/dialog/OperationDialog.css b/FrontEnd/src/views/common/dialog/OperationDialog.css
deleted file mode 100644
index 2f7617d0..00000000
--- a/FrontEnd/src/views/common/dialog/OperationDialog.css
+++ /dev/null
@@ -1,25 +0,0 @@
-.cru-operation-dialog-group {
- display: block;
- margin: 0.4em 0;
-}
-
-.cru-operation-dialog-label {
- display: block;
- color: var(--cru-primary-color);
-}
-
-.cru-operation-dialog-inline-label {
- margin-inline-start: 0.5em;
-}
-
-.cru-operation-dialog-error-text {
- display: block;
- font-size: 0.8em;
- color: var(--cru-danger-color);
-}
-
-.cru-operation-dialog-helper-text {
- display: block;
- font-size: 0.8em;
- color: var(--cru-primary-color);
-}
diff --git a/FrontEnd/src/views/common/dialog/OperationDialog.tsx b/FrontEnd/src/views/common/dialog/OperationDialog.tsx
deleted file mode 100644
index 71be030a..00000000
--- a/FrontEnd/src/views/common/dialog/OperationDialog.tsx
+++ /dev/null
@@ -1,531 +0,0 @@
-import { useState } from "react";
-import * as React from "react";
-import { useTranslation } from "react-i18next";
-import { TwitterPicker } from "react-color";
-import classNames from "classnames";
-import moment from "moment";
-
-import { convertI18nText, I18nText, UiLogicError } from "@/common";
-
-import { PaletteColorType } from "@/palette";
-
-import Button from "../button/Button";
-import LoadingButton from "../button/LoadingButton";
-import Dialog from "./Dialog";
-
-import "./OperationDialog.css";
-
-interface DefaultErrorPromptProps {
- error?: string;
-}
-
-const DefaultErrorPrompt: React.FC<DefaultErrorPromptProps> = (props) => {
- const { t } = useTranslation();
-
- let result = <p className="cru-color-danger">{t("operationDialog.error")}</p>;
-
- if (props.error != null) {
- result = (
- <>
- {result}
- <p className="cru-color-danger">{props.error}</p>
- </>
- );
- }
-
- return result;
-};
-
-export interface OperationDialogTextInput {
- type: "text";
- label?: I18nText;
- password?: boolean;
- initValue?: string;
- textFieldProps?: Omit<
- React.InputHTMLAttributes<HTMLInputElement>,
- "type" | "value" | "onChange"
- >;
- helperText?: string;
-}
-
-export interface OperationDialogBoolInput {
- type: "bool";
- label: I18nText;
- initValue?: boolean;
- helperText?: string;
-}
-
-export interface OperationDialogSelectInputOption {
- value: string;
- label: I18nText;
- icon?: React.ReactElement;
-}
-
-export interface OperationDialogSelectInput {
- type: "select";
- label: I18nText;
- options: OperationDialogSelectInputOption[];
- initValue?: string;
-}
-
-export interface OperationDialogColorInput {
- type: "color";
- label?: I18nText;
- initValue?: string | null;
- canBeNull?: boolean;
-}
-
-export interface OperationDialogDateTimeInput {
- type: "datetime";
- label?: I18nText;
- initValue?: string;
- helperText?: string;
-}
-
-export type OperationDialogInput =
- | OperationDialogTextInput
- | OperationDialogBoolInput
- | OperationDialogSelectInput
- | OperationDialogColorInput
- | OperationDialogDateTimeInput;
-
-interface OperationInputTypeStringToValueTypeMap {
- text: string;
- bool: boolean;
- select: string;
- color: string | null;
- datetime: string;
-}
-
-type MapOperationInputTypeStringToValueType<Type> =
- Type extends keyof OperationInputTypeStringToValueTypeMap
- ? OperationInputTypeStringToValueTypeMap[Type]
- : never;
-
-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[]
-> = {
- [Index in keyof Tuple]: MapOperationInputInfoValueType<Tuple[Index]>;
-} & { length: Tuple["length"] };
-
-export type OperationInputError =
- | {
- [index: number]: I18nText | null | undefined;
- }
- | null
- | undefined;
-
-const isNoError = (error: OperationInputError): boolean => {
- if (error == null) return true;
- for (const key in error) {
- if (error[key] != null) return false;
- }
- return true;
-};
-
-export interface OperationDialogProps<
- TData,
- OperationInputInfoList extends readonly OperationDialogInput[]
-> {
- open: boolean;
- onClose: () => void;
- title: I18nText | (() => React.ReactNode);
- themeColor?: PaletteColorType;
- onProcess: (
- inputs: MapOperationInputInfoValueTypeList<OperationInputInfoList>
- ) => Promise<TData>;
- inputScheme?: OperationInputInfoList;
- inputValidator?: (
- inputs: MapOperationInputInfoValueTypeList<OperationInputInfoList>
- ) => OperationInputError;
- inputPrompt?: I18nText | (() => React.ReactNode);
- processPrompt?: () => React.ReactNode;
- successPrompt?: (data: TData) => React.ReactNode;
- failurePrompt?: (error: unknown) => React.ReactNode;
- onSuccessAndClose?: (data: TData) => void;
-}
-
-const OperationDialog = <
- TData,
- OperationInputInfoList extends readonly OperationDialogInput[]
->(
- props: OperationDialogProps<TData, OperationInputInfoList>
-): React.ReactElement => {
- const inputScheme = (props.inputScheme ??
- []) as readonly OperationDialogInput[];
-
- const { t } = useTranslation();
-
- type Step =
- | { type: "input" }
- | { type: "process" }
- | {
- type: "success";
- data: TData;
- }
- | {
- 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.");
- }
- })
- );
- const [dirtyList, setDirtyList] = useState<boolean[]>(() =>
- inputScheme.map(() => false)
- );
- const [inputError, setInputError] = useState<OperationInputError>();
-
- const close = (): void => {
- 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.");
- }
- };
-
- const onConfirm = (): void => {
- 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>
- )
- .then(
- (d) => {
- setStep({
- type: "success",
- data: d,
- });
- },
- (e: unknown) => {
- setStep({
- type: "failure",
- data: e,
- });
- }
- );
- };
-
- let body: React.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 { inputValidator } = props;
- if (inputValidator != null) {
- const result = inputValidator(
- values as unknown as MapOperationInputInfoValueTypeList<OperationInputInfoList>
- );
- setInputError(result);
- return isNoError(result);
- }
- return true;
- };
-
- const updateValue = (index: number, newValue: ValueType): void => {
- const oldValues = values;
- const newValues = oldValues.slice();
- newValues[index] = newValue;
- setValues(newValues);
- if (dirtyList[index] === false) {
- const newDirtyList = dirtyList.slice();
- newDirtyList[index] = true;
- setDirtyList(newDirtyList);
- }
- validate(newValues);
- };
-
- const canProcess = isNoError(inputError);
-
- body = (
- <>
- <div>
- {inputPrompt}
- {inputScheme.map((item, index) => {
- const value = values[index];
- const error: string | null =
- dirtyList[index] && inputError != null
- ? convertI18nText(inputError[index], t)
- : null;
-
- if (item.type === "text") {
- return (
- <div
- key={index}
- className={classNames(
- "cru-operation-dialog-group",
- error != null ? "error" : null
- )}
- >
- {item.label && (
- <label className="cru-operation-dialog-label">
- {convertI18nText(item.label, t)}
- </label>
- )}
- <input
- type={item.password === true ? "password" : "text"}
- value={value as string}
- onChange={(e) => {
- const v = e.target.value;
- updateValue(index, v);
- }}
- disabled={process}
- />
- {error != null && (
- <div className="cru-operation-dialog-error-text">
- {error}
- </div>
- )}
- {item.helperText && (
- <div className="cru-operation-dialog-helper-text">
- {t(item.helperText)}
- </div>
- )}
- </div>
- );
- } else if (item.type === "bool") {
- return (
- <div
- key={index}
- className={classNames(
- "cru-operation-dialog-group",
- error != null ? "error" : null
- )}
- >
- <input
- type="checkbox"
- checked={value as boolean}
- onChange={(event) => {
- updateValue(index, event.currentTarget.checked);
- }}
- disabled={process}
- />
- <label className="cru-operation-dialog-inline-label">
- {convertI18nText(item.label, t)}
- </label>
- {error != null && (
- <div className="cru-operation-dialog-error-text">
- {error}
- </div>
- )}
- {item.helperText && (
- <div className="cru-operation-dialog-helper-text">
- {t(item.helperText)}
- </div>
- )}
- </div>
- );
- } else if (item.type === "select") {
- return (
- <div
- key={index}
- className={classNames(
- "cru-operation-dialog-group",
- error != null ? "error" : null
- )}
- >
- <label className="cru-operation-dialog-label">
- {convertI18nText(item.label, t)}
- </label>
- <select
- value={value as string}
- onChange={(event) => {
- updateValue(index, event.target.value);
- }}
- disabled={process}
- >
- {item.options.map((option, i) => {
- return (
- <option value={option.value} key={i}>
- {option.icon}
- {convertI18nText(option.label, t)}
- </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
- )}
- >
- {item.label && (
- <label className="cru-operation-dialog-label">
- {convertI18nText(item.label, t)}
- </label>
- )}
- <input
- type="datetime-local"
- value={value as string}
- onChange={(e) => {
- const v = e.target.value;
- updateValue(index, v);
- }}
- disabled={process}
- />
- {error != null && <div>{error}</div>}
- </div>
- );
- }
- })}
- </div>
- <hr />
- <div className="cru-dialog-bottom-area">
- <Button
- text="operationDialog.cancel"
- color="secondary"
- outline
- onClick={close}
- disabled={process}
- />
- <LoadingButton
- color={props.themeColor}
- loading={process}
- disabled={!canProcess}
- onClick={() => {
- setDirtyList(inputScheme.map(() => true));
- if (validate(values)) {
- onConfirm();
- }
- }}
- >
- {t("operationDialog.confirm")}
- </LoadingButton>
- </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} />;
- }
- body = (
- <>
- <div>{content}</div>
- <hr />
- <div className="cru-dialog-bottom-area">
- <Button text="operationDialog.ok" color="primary" onClick={close} />
- </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"
- }
- >
- {title}
- </h3>
- <hr />
- {body}
- </Dialog>
- );
-};
-
-export default OperationDialog;