From c7934c59cb17a4266ea882cdb40be79f22043d10 Mon Sep 17 00:00:00 2001
From: crupest <crupest@outlook.com>
Date: Mon, 24 Jul 2023 00:31:02 +0800
Subject: ...

---
 FrontEnd/src/views/common/input/InputPanel.tsx | 7 +++----
 1 file changed, 3 insertions(+), 4 deletions(-)

(limited to 'FrontEnd/src/views/common/input/InputPanel.tsx')

diff --git a/FrontEnd/src/views/common/input/InputPanel.tsx b/FrontEnd/src/views/common/input/InputPanel.tsx
index 234ed267..27937a05 100644
--- a/FrontEnd/src/views/common/input/InputPanel.tsx
+++ b/FrontEnd/src/views/common/input/InputPanel.tsx
@@ -1,7 +1,6 @@
 import * as React from "react";
 import classNames from "classnames";
 import { useTranslation } from "react-i18next";
-import { TwitterPicker } from "react-color";
 
 import { convertI18nText, I18nText } from "@/common";
 
@@ -89,14 +88,14 @@ export interface InputPanelProps<InputList extends readonly Input[]> {
   values: MapInputListToValueTypeList<InputList>;
   onChange: (
     values: MapInputListToValueTypeList<InputList>,
-    index: number
+    index: number,
   ) => void;
   error?: InputPanelError;
   disable?: boolean;
 }
 
 const InputPanel = <InputList extends readonly Input[]>(
-  props: InputPanelProps<InputList>
+  props: InputPanelProps<InputList>,
 ): React.ReactElement => {
   const { values, onChange, scheme, error, disable } = props;
 
@@ -108,7 +107,7 @@ const InputPanel = <InputList extends readonly Input[]>(
     newValues[index] = newValue;
     onChange(
       newValues as unknown as MapInputListToValueTypeList<InputList>,
-      index
+      index,
     );
   };
 
-- 
cgit v1.2.3


From ae48ac24c5533653f1e1f1e6d2a33e0238222297 Mon Sep 17 00:00:00 2001
From: crupest <crupest@outlook.com>
Date: Mon, 24 Jul 2023 00:41:53 +0800
Subject: ...

---
 FrontEnd/package.json                                  |  1 +
 FrontEnd/pnpm-lock.yaml                                | 18 ++++++++++++++++++
 FrontEnd/src/views/common/input/InputPanel.tsx         | 12 +-----------
 .../views/timeline/TimelinePropertyChangeDialog.tsx    |  6 ------
 4 files changed, 20 insertions(+), 17 deletions(-)

(limited to 'FrontEnd/src/views/common/input/InputPanel.tsx')

diff --git a/FrontEnd/package.json b/FrontEnd/package.json
index 725b09dd..56af818c 100644
--- a/FrontEnd/package.json
+++ b/FrontEnd/package.json
@@ -51,6 +51,7 @@
     "@types/react-transition-group": "^4.4.6",
     "@typescript-eslint/eslint-plugin": "^5.61.0",
     "@typescript-eslint/parser": "^5.61.0",
+    "buffer": "^6.0.0",
     "eslint": "^8.44.0",
     "eslint-config-prettier": "^8.8.0",
     "eslint-plugin-prettier": "^4.2.1",
diff --git a/FrontEnd/pnpm-lock.yaml b/FrontEnd/pnpm-lock.yaml
index a31e5a2c..cad9a287 100644
--- a/FrontEnd/pnpm-lock.yaml
+++ b/FrontEnd/pnpm-lock.yaml
@@ -118,6 +118,9 @@ devDependencies:
   '@typescript-eslint/parser':
     specifier: ^5.61.0
     version: 5.61.0(eslint@8.44.0)(typescript@5.1.6)
+  buffer:
+    specifier: ^6.0.0
+    version: 6.0.0
   eslint:
     specifier: ^8.44.0
     version: 8.44.0
@@ -1748,6 +1751,10 @@ packages:
       safe-buffer: 5.2.1
     dev: true
 
+  /base64-js@1.5.1:
+    resolution: {integrity: sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==}
+    dev: true
+
   /boolbase@1.0.0:
     resolution: {integrity: sha512-JZOSA7Mo9sNGB8+UjSgzdLtokWAky1zbztM3WRLCbZ70/3cTANmQmOdR7y2g+J0e2WXywy1yS468tY+IruqEww==}
     dev: true
@@ -1789,6 +1796,13 @@ packages:
       update-browserslist-db: 1.0.11(browserslist@4.21.9)
     dev: true
 
+  /buffer@6.0.0:
+    resolution: {integrity: sha512-Sxdxq98A+Y9kRjO/3+mc2IAxIyTAKqzBiYKpeo5EluWnw9535rI4fN8DeMGsiQqpqqaWtFtTdxQgHnku6IEjCA==}
+    dependencies:
+      base64-js: 1.5.1
+      ieee754: 1.2.1
+    dev: true
+
   /call-bind@1.0.2:
     resolution: {integrity: sha512-7O+FbCihrB5WGbFYesctwmTKae6rOiIzmz1icreWJ+0aA7LJfuqhEso2T9ncpcFtzMQtzXf2QGGueWJGTYsqrA==}
     dependencies:
@@ -2648,6 +2662,10 @@ packages:
       '@babel/runtime': 7.22.6
     dev: false
 
+  /ieee754@1.2.1:
+    resolution: {integrity: sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==}
+    dev: true
+
   /ignore@5.2.4:
     resolution: {integrity: sha512-MAb38BcSbH0eHNBxn7ql2NH/kX33OkB3lZ1BNdh7ENeRChHTYsTvWrMubiIAMNS2llXEEgZ1MUOBtXChP3kaFQ==}
     engines: {node: '>= 4'}
diff --git a/FrontEnd/src/views/common/input/InputPanel.tsx b/FrontEnd/src/views/common/input/InputPanel.tsx
index 27937a05..e0db3787 100644
--- a/FrontEnd/src/views/common/input/InputPanel.tsx
+++ b/FrontEnd/src/views/common/input/InputPanel.tsx
@@ -31,23 +31,13 @@ export interface SelectInput {
   options: SelectInputOption[];
 }
 
-export interface ColorInput {
-  type: "color";
-  label?: I18nText;
-}
-
 export interface DateTimeInput {
   type: "datetime";
   label?: I18nText;
   helper?: I18nText;
 }
 
-export type Input =
-  | TextInput
-  | BoolInput
-  | SelectInput
-  | ColorInput
-  | DateTimeInput;
+export type Input = TextInput | BoolInput | SelectInput | DateTimeInput;
 
 interface InputTypeToValueTypeMap {
   text: string;
diff --git a/FrontEnd/src/views/timeline/TimelinePropertyChangeDialog.tsx b/FrontEnd/src/views/timeline/TimelinePropertyChangeDialog.tsx
index 63750445..bd5bef4c 100644
--- a/FrontEnd/src/views/timeline/TimelinePropertyChangeDialog.tsx
+++ b/FrontEnd/src/views/timeline/TimelinePropertyChangeDialog.tsx
@@ -52,12 +52,6 @@ const TimelinePropertyChangeDialog: React.FC<
             label: "timeline.dialogChangeProperty.description",
             initValue: timeline.description,
           },
-          {
-            type: "color",
-            label: "timeline.dialogChangeProperty.color",
-            initValue: timeline.color ?? null,
-            canBeNull: true,
-          },
         ] as const
       }
       open={props.open}
-- 
cgit v1.2.3


From 4f8d933994c576dc180fae23a3dca477d2354939 Mon Sep 17 00:00:00 2001
From: crupest <crupest@outlook.com>
Date: Mon, 24 Jul 2023 21:48:48 +0800
Subject: ...

---
 .../src/pages/setting/ChangePasswordDialog.tsx     |  11 +-
 .../src/views/common/dialog/OperationDialog.css    |  17 +-
 .../src/views/common/dialog/OperationDialog.tsx    | 409 ++++-----------------
 FrontEnd/src/views/common/input/InputGroup.css     |  25 ++
 FrontEnd/src/views/common/input/InputGroup.tsx     | 362 ++++++++++++++++++
 FrontEnd/src/views/common/input/InputPanel.css     |  25 --
 FrontEnd/src/views/common/input/InputPanel.tsx     | 246 -------------
 FrontEnd/src/views/register/index.tsx              |   9 +-
 8 files changed, 483 insertions(+), 621 deletions(-)
 create mode 100644 FrontEnd/src/views/common/input/InputGroup.css
 create mode 100644 FrontEnd/src/views/common/input/InputGroup.tsx
 delete mode 100644 FrontEnd/src/views/common/input/InputPanel.css
 delete mode 100644 FrontEnd/src/views/common/input/InputPanel.tsx

(limited to 'FrontEnd/src/views/common/input/InputPanel.tsx')

diff --git a/FrontEnd/src/pages/setting/ChangePasswordDialog.tsx b/FrontEnd/src/pages/setting/ChangePasswordDialog.tsx
index a523b454..5505137e 100644
--- a/FrontEnd/src/pages/setting/ChangePasswordDialog.tsx
+++ b/FrontEnd/src/pages/setting/ChangePasswordDialog.tsx
@@ -1,24 +1,25 @@
 import { useState } from "react";
-import * as React from "react";
 import { useNavigate } from "react-router-dom";
 
 import { userService } from "@/services/user";
 
 import OperationDialog from "@/views/common/dialog/OperationDialog";
 
-export interface ChangePasswordDialogProps {
+interface ChangePasswordDialogProps {
   open: boolean;
   close: () => void;
 }
 
-const ChangePasswordDialog: React.FC<ChangePasswordDialogProps> = (props) => {
+export function ChangePasswordDialog(props: ChangePasswordDialogProps) {
+  const { open, close } = props;
+
   const navigate = useNavigate();
 
   const [redirect, setRedirect] = useState<boolean>(false);
 
   return (
     <OperationDialog
-      open={props.open}
+      open={open}
       title="settings.dialogChangePassword.title"
       themeColor="danger"
       inputPrompt="settings.dialogChangePassword.prompt"
@@ -64,6 +65,6 @@ const ChangePasswordDialog: React.FC<ChangePasswordDialogProps> = (props) => {
       }}
     />
   );
-};
+}
 
 export default ChangePasswordDialog;
diff --git a/FrontEnd/src/views/common/dialog/OperationDialog.css b/FrontEnd/src/views/common/dialog/OperationDialog.css
index 2f7617d0..19c5d806 100644
--- a/FrontEnd/src/views/common/dialog/OperationDialog.css
+++ b/FrontEnd/src/views/common/dialog/OperationDialog.css
@@ -1,3 +1,18 @@
+.cru-operation-dialog-title {
+  font-size: 1.2em;
+  font-weight: bold;
+  color: var(--cru-key-color);
+  margin-bottom: 0.5em;
+}
+
+.cru-operation-dialog-prompt {
+  color: var(--cru-surface-on-color);
+}
+
+.cru-operation-dialog-main-area {
+  margin-top: 0.5em;
+}
+
 .cru-operation-dialog-group {
   display: block;
   margin: 0.4em 0;
@@ -22,4 +37,4 @@
   display: block;
   font-size: 0.8em;
   color: var(--cru-primary-color);
-}
+}
\ No newline at end of file
diff --git a/FrontEnd/src/views/common/dialog/OperationDialog.tsx b/FrontEnd/src/views/common/dialog/OperationDialog.tsx
index ad00c424..ad9bf5c1 100644
--- a/FrontEnd/src/views/common/dialog/OperationDialog.tsx
+++ b/FrontEnd/src/views/common/dialog/OperationDialog.tsx
@@ -1,180 +1,76 @@
-import { useState, ReactNode, ComponentPropsWithoutRef } from "react";
+import { useState, ReactNode } from "react";
 import classNames from "classnames";
-import moment from "moment";
 
 import { useC, Text, ThemeColor } from "../common";
 
 import Button from "../button/Button";
+import {
+  default as InputGroup,
+  InputErrors,
+  InputList,
+  Validator,
+  Values,
+  useDirties,
+} from "../input/InputGroup";
 import LoadingButton from "../button/LoadingButton";
 import Dialog from "./Dialog";
 
 import "./OperationDialog.css";
 
-interface DefaultPromptProps {
-  color?: ThemeColor;
+interface OperationDialogPromptProps {
   message?: Text;
   customMessage?: ReactNode;
   className?: string;
 }
 
-function DefaultPrompt(props: DefaultPromptProps) {
-  const { color, message, customMessage, className } = props;
+function OperationDialogPrompt(props: OperationDialogPromptProps) {
+  const { message, customMessage, className } = props;
 
   const c = useC();
 
   return (
-    <div className={classNames(className, `cru-${color ?? "primary"}`)}>
-      <p>{c(message)}</p>
+    <div className={classNames(className, "cru-operation-dialog-prompt")}>
+      {message && <p>{c(message)}</p>}
       {customMessage}
     </div>
   );
 }
 
-export interface OperationDialogTextInput {
-  type: "text";
-  label?: Text;
-  password?: boolean;
-  initValue?: string;
-  textFieldProps?: Omit<
-    ComponentPropsWithoutRef<"input">,
-    "type" | "value" | "onChange"
-  >;
-  helperText?: Text;
-}
-
-export interface OperationDialogBoolInput {
-  type: "bool";
-  label: Text;
-  initValue?: boolean;
-  helperText?: Text;
-}
-
-export interface OperationDialogSelectInputOption {
-  value: string;
-  label: Text;
-  icon?: ReactNode;
-}
-
-export interface OperationDialogSelectInput {
-  type: "select";
-  label: Text;
-  options: OperationDialogSelectInputOption[];
-  initValue?: string;
-}
-
-export interface OperationDialogDateTimeInput {
-  type: "datetime";
-  label?: Text;
-  initValue?: string;
-  helperText?: string;
-}
-
-export type OperationDialogInput =
-  | OperationDialogTextInput
-  | OperationDialogBoolInput
-  | OperationDialogSelectInput
-  | OperationDialogDateTimeInput;
-
-interface OperationInputTypeStringToValueTypeMap {
-  text: string;
-  bool: boolean;
-  select: string;
-  datetime: string;
-}
-
-type OperationInputValueType =
-  OperationInputTypeStringToValueTypeMap[keyof OperationInputTypeStringToValueTypeMap];
-
-type MapOperationInputTypeStringToValueType<Type> =
-  Type extends keyof OperationInputTypeStringToValueTypeMap
-    ? OperationInputTypeStringToValueTypeMap[Type]
-    : never;
-
-type MapOperationInputInfoValueType<T> = T extends OperationDialogInput
-  ? MapOperationInputTypeStringToValueType<T["type"]>
-  : T;
-
-type MapOperationInputInfoValueTypeList<
-  Tuple extends readonly OperationDialogInput[],
-> = {
-  [Index in keyof Tuple]: MapOperationInputInfoValueType<Tuple[Index]>;
-};
-
-export type OperationInputError =
-  | {
-      [index: number]: Text | 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;
-};
-
-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[],
-> {
+export interface OperationDialogProps<TData, Inputs extends InputList> {
   open: boolean;
   onClose: () => void;
 
-  themeColor?: ThemeColor;
+  color?: ThemeColor;
   title: Text;
   inputPrompt?: Text;
   processPrompt?: Text;
   successPrompt?: (data: TData) => ReactNode;
   failurePrompt?: (error: unknown) => ReactNode;
 
-  inputScheme?: OperationInputInfoList;
-  inputValidator?: (
-    inputs: MapOperationInputInfoValueTypeList<OperationInputInfoList>,
-  ) => OperationInputError;
+  inputs: Inputs;
+  validator?: Validator<Inputs>;
 
-  onProcess: (
-    inputs: MapOperationInputInfoValueTypeList<OperationInputInfoList>,
-  ) => Promise<TData>;
+  onProcess: (inputs: Values<Inputs>) => Promise<TData>;
   onSuccessAndClose?: (data: TData) => void;
 }
 
-function OperationDialog<
-  TData,
-  OperationInputInfoList extends readonly OperationDialogInput[],
->(props: OperationDialogProps<TData, OperationInputInfoList>) {
-  const inputScheme = props.inputScheme ?? ([] as const);
+function OperationDialog<TData, Inputs extends InputList>(
+  props: OperationDialogProps<TData, Inputs>,
+) {
+  const {
+    open,
+    onClose,
+    color,
+    title,
+    inputPrompt,
+    processPrompt,
+    successPrompt,
+    failurePrompt,
+    inputs,
+    validator,
+    onProcess,
+    onSuccessAndClose,
+  } = props;
 
   const c = useC();
 
@@ -191,21 +87,9 @@ function OperationDialog<
       };
 
   const [step, setStep] = useState<Step>({ type: "input" });
-
-  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),
-  );
-
-  const [inputError, setInputError] = useState<OperationInputError>();
+  const [values, setValues] = useState<Values<Inputs>>();
+  const [errors, setErrors] = useState<InputErrors>();
+  const [dirties, setDirties, dirtyAll] = useDirties();
 
   function close() {
     if (step.type !== "process") {
@@ -244,178 +128,28 @@ function OperationDialog<
 
   let body: ReactNode;
   if (step.type === "input" || step.type === "process") {
-    const process = step.type === "process";
-
-    const validate = (values: Values): boolean => {
-      const { inputValidator } = props;
-      if (inputValidator != null) {
-        const result = inputValidator(values);
-        setInputError(result);
-        return isNoError(result);
-      }
-      return true;
-    };
-
-    const updateValue = (
-      index: number,
-      newValue: OperationInputValueType,
-    ): void => {
-      const oldValues = values;
-      const newValues = oldValues.slice();
-      newValues[index] = newValue;
-      setValues(newValues as Values);
-      if (dirtyList[index] === false) {
-        const newDirtyList = dirtyList.slice();
-        newDirtyList[index] = true;
-        setDirtyList(newDirtyList);
-      }
-      validate(newValues as Values);
-    };
-
-    const canProcess = isNoError(inputError);
+    const isProcessing = step.type === "process";
+    const hasError = errors.length > 0;
 
     body = (
       <div className="cru-operation-dialog-main-area">
         <div>
-          <div>{c(props.inputPrompt)}</div>
-          {inputScheme.map((item: OperationDialogInput, index: number) => {
-            const value = values[index];
-            const error: string | null =
-              dirtyList[index] && inputError != null
-                ? c(inputError[index])
-                : null;
-
-            if (item.type === "text") {
-              return (
-                <div
-                  key={index}
-                  className={classNames(
-                    "cru-operation-dialog-group",
-                    error && "error",
-                  )}
-                >
-                  {item.label && (
-                    <label className="cru-operation-dialog-label">
-                      {c(item.label)}
-                    </label>
-                  )}
-                  <input
-                    type={item.password === true ? "password" : "text"}
-                    value={value as string}
-                    onChange={(event) => {
-                      const v = event.target.value;
-                      updateValue(index, v);
-                    }}
-                    disabled={process}
-                  />
-                  {error && (
-                    <div className="cru-operation-dialog-error-text">
-                      {error}
-                    </div>
-                  )}
-                  {item.helperText && (
-                    <div className="cru-operation-dialog-helper-text">
-                      {c(item.helperText)}
-                    </div>
-                  )}
-                </div>
-              );
-            } else if (item.type === "bool") {
-              return (
-                <div
-                  key={index}
-                  className={classNames(
-                    "cru-operation-dialog-group",
-                    error && "error",
-                  )}
-                >
-                  <input
-                    type="checkbox"
-                    checked={value as boolean}
-                    onChange={(event) => {
-                      const v = event.currentTarget.checked;
-                      updateValue(index, v);
-                    }}
-                    disabled={process}
-                  />
-                  <label className="cru-operation-dialog-inline-label">
-                    {c(item.label)}
-                  </label>
-                  {error && (
-                    <div className="cru-operation-dialog-error-text">
-                      {error}
-                    </div>
-                  )}
-                  {item.helperText && (
-                    <div className="cru-operation-dialog-helper-text">
-                      {c(item.helperText)}
-                    </div>
-                  )}
-                </div>
-              );
-            } else if (item.type === "select") {
-              return (
-                <div
-                  key={index}
-                  className={classNames(
-                    "cru-operation-dialog-group",
-                    error && "error",
-                  )}
-                >
-                  <label className="cru-operation-dialog-label">
-                    {c(item.label)}
-                  </label>
-                  <select
-                    value={value as string}
-                    onChange={(event) => {
-                      const e = event.target.value;
-                      updateValue(index, e);
-                    }}
-                    disabled={process}
-                  >
-                    {item.options.map((option, i) => {
-                      return (
-                        <option value={option.value} key={i}>
-                          {option.icon}
-                          {c(option.label)}
-                        </option>
-                      );
-                    })}
-                  </select>
-                </div>
-              );
-            } else if (item.type === "datetime") {
-              return (
-                <div
-                  key={index}
-                  className={classNames(
-                    "cru-operation-dialog-group",
-                    error && "error",
-                  )}
-                >
-                  {item.label && (
-                    <label className="cru-operation-dialog-label">
-                      {c(item.label)}
-                    </label>
-                  )}
-                  <input
-                    type="datetime-local"
-                    value={value as string}
-                    onChange={(event) => {
-                      const v = event.target.value;
-                      updateValue(index, v);
-                    }}
-                    disabled={process}
-                  />
-                  {error && (
-                    <div className="cru-operation-dialog-error-text">
-                      {error}
-                    </div>
-                  )}
-                </div>
-              );
-            }
-          })}
+          <OperationDialogPrompt customMessage={c(props.inputPrompt)} />
+          <InputGroup
+            className="cru-operation-dialog-input-group"
+            color={color}
+            inputs={inputs}
+            validator={validator}
+            values={values}
+            errors={errors}
+            disabled={isProcessing}
+            onChange={(values, errors) => {
+              setValues(values);
+              setErrors(errors);
+            }}
+            dirties={dirties}
+            onDirty={setDirties}
+          />
         </div>
         <hr />
         <div className="cru-dialog-bottom-area">
@@ -424,14 +158,14 @@ function OperationDialog<
             color="secondary"
             outline
             onClick={close}
-            disabled={process}
+            disabled={isProcessing}
           />
           <LoadingButton
-            color={props.themeColor}
-            loading={process}
-            disabled={!canProcess}
+            color={color}
+            loading={isProcessing}
+            disabled={hasError}
             onClick={() => {
-              setDirtyList(inputScheme.map(() => true));
+              dirtyAll();
               if (validate(values)) {
                 onConfirm();
               }
@@ -445,21 +179,19 @@ function OperationDialog<
   } else {
     const result = step;
 
-    const promptProps: DefaultPromptProps =
+    const promptProps: OperationDialogPromptProps =
       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 className="cru-operation-dialog-main-area">
-        <DefaultPrompt {...promptProps} />
+        <OperationDialogPrompt {...promptProps} />
         <hr />
         <div className="cru-dialog-bottom-area">
           <Button text="operationDialog.ok" color="primary" onClick={close} />
@@ -471,14 +203,15 @@ function OperationDialog<
   return (
     <Dialog open={props.open} onClose={close}>
       <div
-        className={`cru-operation-dialog-title cru-${
-          props.themeColor ?? "primary"
-        }`}
+        className={classNames(
+          "cru-operation-dialog-container",
+          `cru-${props.themeColor ?? "primary"}`,
+        )}
       >
-        {c(props.title)}
+        <div className="cru-operation-dialog-title">{c(props.title)}</div>
+        <hr />
+        {body}
       </div>
-      <hr />
-      {body}
     </Dialog>
   );
 }
diff --git a/FrontEnd/src/views/common/input/InputGroup.css b/FrontEnd/src/views/common/input/InputGroup.css
new file mode 100644
index 00000000..f9d6ac8b
--- /dev/null
+++ b/FrontEnd/src/views/common/input/InputGroup.css
@@ -0,0 +1,25 @@
+.cru-input-panel-group {
+  display: block;
+  margin: 0.4em 0;
+}
+
+.cru-input-panel-label {
+  display: block;
+  color: var(--cru-primary-color);
+}
+
+.cru-input-panel-inline-label {
+  margin-inline-start: 0.5em;
+}
+
+.cru-input-panel-error-text {
+  display: block;
+  font-size: 0.8em;
+  color: var(--cru-danger-color);
+}
+
+.cru-input-panel-helper-text {
+  display: block;
+  font-size: 0.8em;
+  color: var(--cru-primary-color);
+}
diff --git a/FrontEnd/src/views/common/input/InputGroup.tsx b/FrontEnd/src/views/common/input/InputGroup.tsx
new file mode 100644
index 00000000..1b137fd8
--- /dev/null
+++ b/FrontEnd/src/views/common/input/InputGroup.tsx
@@ -0,0 +1,362 @@
+import {
+  useState,
+  useEffect,
+  ReactNode,
+  ComponentPropsWithoutRef,
+  Ref,
+} from "react";
+import classNames from "classnames";
+
+import { useC, Text, ThemeColor } from "../common";
+
+import "./InputGroup.css";
+
+export interface TextInput {
+  type: "text";
+  label?: Text;
+  password?: boolean;
+  textFieldProps?: Omit<
+    ComponentPropsWithoutRef<"input">,
+    "type" | "value" | "onChange"
+  >;
+  helperText?: Text;
+}
+
+export interface BoolInput {
+  type: "bool";
+  label: Text;
+  helperText?: Text;
+}
+
+export interface SelectInputOption {
+  value: string;
+  label: Text;
+  icon?: ReactNode;
+}
+
+export interface SelectInput {
+  type: "select";
+  label: Text;
+  options: SelectInputOption[];
+}
+
+export interface DateTimeInput {
+  type: "datetime";
+  label?: Text;
+  helperText?: string;
+}
+
+export type Input = TextInput | BoolInput | SelectInput | DateTimeInput;
+
+export type InputType = Input["type"];
+
+export type InputTypeToInputMap = {
+  [I in Input as I["type"]]: I;
+};
+
+export interface InputTypeToValueMap {
+  text: string;
+  bool: boolean;
+  select: string;
+  datetime: Date;
+}
+
+export type InputValue = InputTypeToValueMap[keyof InputTypeToValueMap];
+
+export type MapInputToValue<I extends Input> = InputTypeToValueMap[I["type"]];
+
+export type MapInputListToValueList<Tuple extends Input[]> = {
+  [Index in keyof Tuple]: MapInputToValue<Tuple[Index]>;
+};
+
+export type MapInputListTo<Tuple extends Input[], T> = {
+  [Index in keyof Tuple]: T;
+};
+
+export interface InputTypeToInitialValueMap {
+  text: string | null | undefined;
+  bool: boolean | null | undefined;
+  select: string | null | undefined;
+  datetime: Date | string | null | undefined;
+}
+
+export type MapInputToInitialValue<I extends Input> =
+  InputTypeToInitialValueMap[I["type"]];
+
+export type InputValueTransformers = {
+  [I in Input as I["type"]]: (
+    input: I,
+    value: MapInputToInitialValue<I>,
+  ) => MapInputToValue<I>;
+};
+
+const initialValueTransformers: InputValueTransformers = {
+  text: (input, value) => value ?? "",
+  bool: (input, value) => value ?? false,
+  select: (input, value) => value ?? input.options[0].value,
+  datetime: (input, value) => {
+    if (value == null) return new Date();
+    if (typeof value === "string") {
+      return new Date(value);
+    }
+    return value;
+  },
+};
+
+// No use currently
+//
+// export type ValueValueTransformers = {
+//   [I in Input as I["type"]]: (input: MapInputToValue<I>) => MapInputToValue<I>;
+// };
+//
+// const finalValueMapperMap: ValueValueMapper = {
+//   bool: (value) => value,
+//   datetime: (value) => new Date(value).toISOString(),
+//   select: (value) => value,
+//   text: (value) => value,
+// };
+
+export type InputErrors = {
+  index: number;
+  message: Text;
+}[];
+
+export type InputList = Input[];
+export type Validator<Inputs extends InputList> = (
+  inputs: MapInputListToValueList<Inputs>,
+) => InputErrors;
+export type Values<Inputs extends InputList> = MapInputListToValueList<Inputs>;
+export type Dirties<Inputs extends InputList> = MapInputListTo<Inputs, boolean>;
+
+export function useInputs<Inputs extends InputList>(
+  inputs: Inputs,
+  validator?: Validator<Inputs>,
+): {
+  inputs: Inputs;
+  validator?: Validator<Inputs>;
+  dirties: Dirties<Inputs> | undefined;
+  setDirties: (dirties: Dirties<Inputs>) => void;
+  dirtyAll: () => void;
+} {
+  const [dirties, setDirties] = useState<Dirties<Inputs>>();
+
+  return {
+    inputs,
+    validator,
+    values,
+    dirties,
+    setDirties,
+    dirtyAll: () => {
+      if (dirties != null) {
+        setDirties(new Array(dirties.length).fill(true) as Dirties<Inputs>);
+      }
+    },
+  };
+}
+
+export interface InputGroupProps<Inputs extends InputList> {
+  inputs: Inputs;
+  validator?: Validator<Inputs>;
+
+  values?: Values<Inputs>;
+  onChange: (
+    values: Values<Inputs>,
+    errors: InputErrors,
+    trigger: number, // May be -1, which means I don't know who trigger this change.
+  ) => void;
+  errors: InputErrors;
+  dirties: Dirties<Inputs>;
+  onDirty: (dirties: Dirties<Inputs>) => void;
+  disabled?: boolean;
+
+  color?: ThemeColor;
+  className?: string;
+  containerRef?: Ref<HTMLDivElement>;
+}
+
+export default function InputGroup<Inputs extends Input[]>({
+  color,
+  inputs,
+  validator,
+  values,
+  errors,
+  disabled,
+  onChange,
+  dirties,
+  onDirty,
+  containerRef,
+  className,
+}: InputGroupProps<Inputs>) {
+  const c = useC();
+
+  type Values = MapInputListToValueList<Inputs>;
+  type Dirties = MapInputListTo<Inputs, boolean>;
+
+  useEffect(() => {
+    if (values == null) {
+      const values = inputs.map((input) => {
+        return initialValueTransformers[input.type](input as never);
+      }) as Values;
+      const errors = validator?.(values) ?? [];
+      onChange(values, errors, -1);
+      onDirty?.(inputs.map(() => false) as Dirties);
+    }
+  }, [values, inputs, validator, onChange, onDirty]);
+
+  if (values == null) {
+    return null;
+  }
+
+  const updateValue = (index: number, newValue: InputValue): void => {
+    const oldValues = values;
+    const newValues = oldValues.slice() as Values;
+    newValues[index] = newValue;
+    const error = validator?.(newValues) ?? [];
+    onChange(newValues, error, index);
+    if (dirties != null && onDirty != null && dirties[index] === false) {
+      const newDirties = dirties.slice() as Dirties;
+      newDirties[index] = true;
+      onDirty(newDirties);
+    }
+  };
+
+  return (
+    <div
+      ref={containerRef}
+      className={classNames(
+        "cru-input-group",
+        `cru-${color ?? "primary"}`,
+        className,
+      )}
+    >
+      {inputs.map((item: Input, index: number) => {
+        const value = values[index];
+        const error =
+          dirties &&
+          dirties[index] &&
+          errors &&
+          errors.find((e) => e.index === index)?.message;
+
+        if (item.type === "text") {
+          return (
+            <div
+              key={index}
+              className={classNames(
+                "cru-input-container cru-input-text",
+                item.password && "password",
+                error && "error",
+              )}
+            >
+              {item.label && (
+                <label className="cru-input-label">{c(item.label)}</label>
+              )}
+              <input
+                type={item.password === true ? "password" : "text"}
+                value={value as string}
+                onChange={(event) => {
+                  const v = event.target.value;
+                  updateValue(index, v);
+                }}
+                disabled={disabled}
+              />
+              {error && <div className="cru-input-error-text">{c(error)}</div>}
+              {item.helperText && (
+                <div className="cru-input-helper-text">
+                  {c(item.helperText)}
+                </div>
+              )}
+            </div>
+          );
+        } else if (item.type === "bool") {
+          return (
+            <div
+              key={index}
+              className={classNames(
+                "cru-input-container cru-input-bool",
+                error && "error",
+              )}
+            >
+              <input
+                type="checkbox"
+                checked={value as boolean}
+                onChange={(event) => {
+                  const v = event.currentTarget.checked;
+                  updateValue(index, v);
+                }}
+                disabled={disabled}
+              />
+              <label className="cru-input-label-inline">{c(item.label)}</label>
+              {error && <div className="cru-input-error-text">{c(error)}</div>}
+              {item.helperText && (
+                <div className="cru-operation-dialog-helper-text">
+                  {c(item.helperText)}
+                </div>
+              )}
+            </div>
+          );
+        } else if (item.type === "select") {
+          return (
+            <div
+              key={index}
+              className={classNames(
+                "cru-input-container cru-input-select",
+                error && "error",
+              )}
+            >
+              <label className="cru-input-label">{c(item.label)}</label>
+              <select
+                value={value as string}
+                onChange={(event) => {
+                  const e = event.target.value;
+                  updateValue(index, e);
+                }}
+                disabled={disabled}
+              >
+                {item.options.map((option, i) => {
+                  return (
+                    <option value={option.value} key={i}>
+                      {option.icon}
+                      {c(option.label)}
+                    </option>
+                  );
+                })}
+              </select>
+            </div>
+          );
+        } else if (item.type === "datetime") {
+          return (
+            <div
+              key={index}
+              className={classNames(
+                "cru-input-container cru-input-datetime",
+                error && "error",
+              )}
+            >
+              {item.label && (
+                <label className="cru-input-label">{c(item.label)}</label>
+              )}
+              <input
+                type="datetime-local"
+                value={(value as Date).toLocaleString()}
+                onChange={(event) => {
+                  const v = event.target.valueAsDate;
+                  if (v == null) {
+                    if (process.env.NODE_ENV === "development") {
+                      console.log(
+                        "Looks like user input date is null. We do nothing. But you might want to check why.",
+                      );
+                    }
+                    return;
+                  }
+                  updateValue(index, v);
+                }}
+                disabled={disabled}
+              />
+              {error && <div className="cru-input-error-text">{c(error)}</div>}
+            </div>
+          );
+        }
+      })}
+    </div>
+  );
+}
diff --git a/FrontEnd/src/views/common/input/InputPanel.css b/FrontEnd/src/views/common/input/InputPanel.css
deleted file mode 100644
index f9d6ac8b..00000000
--- a/FrontEnd/src/views/common/input/InputPanel.css
+++ /dev/null
@@ -1,25 +0,0 @@
-.cru-input-panel-group {
-  display: block;
-  margin: 0.4em 0;
-}
-
-.cru-input-panel-label {
-  display: block;
-  color: var(--cru-primary-color);
-}
-
-.cru-input-panel-inline-label {
-  margin-inline-start: 0.5em;
-}
-
-.cru-input-panel-error-text {
-  display: block;
-  font-size: 0.8em;
-  color: var(--cru-danger-color);
-}
-
-.cru-input-panel-helper-text {
-  display: block;
-  font-size: 0.8em;
-  color: var(--cru-primary-color);
-}
diff --git a/FrontEnd/src/views/common/input/InputPanel.tsx b/FrontEnd/src/views/common/input/InputPanel.tsx
deleted file mode 100644
index e0db3787..00000000
--- a/FrontEnd/src/views/common/input/InputPanel.tsx
+++ /dev/null
@@ -1,246 +0,0 @@
-import * as React from "react";
-import classNames from "classnames";
-import { useTranslation } from "react-i18next";
-
-import { convertI18nText, I18nText } from "@/common";
-
-import "./InputPanel.css";
-
-export interface TextInput {
-  type: "text";
-  label?: I18nText;
-  helper?: I18nText;
-  password?: boolean;
-}
-
-export interface BoolInput {
-  type: "bool";
-  label: I18nText;
-  helper?: I18nText;
-}
-
-export interface SelectInputOption {
-  value: string;
-  label: I18nText;
-  icon?: React.ReactElement;
-}
-
-export interface SelectInput {
-  type: "select";
-  label: I18nText;
-  options: SelectInputOption[];
-}
-
-export interface DateTimeInput {
-  type: "datetime";
-  label?: I18nText;
-  helper?: I18nText;
-}
-
-export type Input = TextInput | BoolInput | SelectInput | DateTimeInput;
-
-interface InputTypeToValueTypeMap {
-  text: string;
-  bool: boolean;
-  select: string;
-  color: string;
-  datetime: string;
-}
-
-type ValueTypes = InputTypeToValueTypeMap[keyof InputTypeToValueTypeMap];
-
-type MapInputTypeToValueType<Type> = Type extends keyof InputTypeToValueTypeMap
-  ? InputTypeToValueTypeMap[Type]
-  : never;
-
-type MapInputToValueType<T> = T extends Input
-  ? MapInputTypeToValueType<T["type"]>
-  : T;
-
-type MapInputListToValueTypeList<Tuple extends readonly Input[]> = {
-  [Index in keyof Tuple]: MapInputToValueType<Tuple[Index]>;
-} & { length: Tuple["length"] };
-
-export type InputPanelError = {
-  [index: number]: I18nText | null | undefined;
-};
-
-export function hasError(e: InputPanelError | null | undefined): boolean {
-  if (e == null) return false;
-  for (const key of Object.keys(e)) {
-    if (e[key as unknown as number] != null) return true;
-  }
-  return false;
-}
-
-export interface InputPanelProps<InputList extends readonly Input[]> {
-  scheme: InputList;
-  values: MapInputListToValueTypeList<InputList>;
-  onChange: (
-    values: MapInputListToValueTypeList<InputList>,
-    index: number,
-  ) => void;
-  error?: InputPanelError;
-  disable?: boolean;
-}
-
-const InputPanel = <InputList extends readonly Input[]>(
-  props: InputPanelProps<InputList>,
-): React.ReactElement => {
-  const { values, onChange, scheme, error, disable } = props;
-
-  const { t } = useTranslation();
-
-  const updateValue = (index: number, newValue: ValueTypes): void => {
-    const oldValues = values;
-    const newValues = oldValues.slice();
-    newValues[index] = newValue;
-    onChange(
-      newValues as unknown as MapInputListToValueTypeList<InputList>,
-      index,
-    );
-  };
-
-  return (
-    <div>
-      {scheme.map((item, index) => {
-        const v = values[index];
-        const e: string | null = convertI18nText(error?.[index], t);
-
-        if (item.type === "text") {
-          return (
-            <div
-              key={index}
-              className={classNames("cru-input-panel-group", e && "error")}
-            >
-              {item.label && (
-                <label className="cru-input-panel-label">
-                  {convertI18nText(item.label, t)}
-                </label>
-              )}
-              <input
-                type={item.password === true ? "password" : "text"}
-                value={v as string}
-                onChange={(e) => {
-                  const v = e.target.value;
-                  updateValue(index, v);
-                }}
-                disabled={disable}
-              />
-              {e && <div className="cru-input-panel-error-text">{e}</div>}
-              {item.helper && (
-                <div className="cru-input-panel-helper-text">
-                  {convertI18nText(item.helper, t)}
-                </div>
-              )}
-            </div>
-          );
-        } else if (item.type === "bool") {
-          return (
-            <div
-              key={index}
-              className={classNames("cru-input-panel-group", e && "error")}
-            >
-              <input
-                type="checkbox"
-                checked={v as boolean}
-                onChange={(event) => {
-                  const value = event.currentTarget.checked;
-                  updateValue(index, value);
-                }}
-                disabled={disable}
-              />
-              <label className="cru-input-panel-inline-label">
-                {convertI18nText(item.label, t)}
-              </label>
-              {e != null && (
-                <div className="cru-input-panel-error-text">{e}</div>
-              )}
-              {item.helper && (
-                <div className="cru-input-panel-helper-text">
-                  {convertI18nText(item.helper, t)}
-                </div>
-              )}
-            </div>
-          );
-        } else if (item.type === "select") {
-          return (
-            <div
-              key={index}
-              className={classNames("cru-input-panel-group", e && "error")}
-            >
-              <label className="cru-input-panel-label">
-                {convertI18nText(item.label, t)}
-              </label>
-              <select
-                value={v as string}
-                onChange={(event) => {
-                  const value = event.target.value;
-                  updateValue(index, value);
-                }}
-                disabled={disable}
-              >
-                {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-input-panel-group", e && "error")}
-            >
-              <label className="cru-input-panel-inline-label">
-                {convertI18nText(item.label, t)}
-              </label>
-              <TwitterPicker
-                color={v as string}
-                triangle="hide"
-                onChange={(result) => updateValue(index, result.hex)}
-              />
-            </div>
-          );
-        } else if (item.type === "datetime") {
-          return (
-            <div
-              key={index}
-              className={classNames("cru-input-panel-group", e && "error")}
-            >
-              {item.label && (
-                <label className="cru-input-panel-label">
-                  {convertI18nText(item.label, t)}
-                </label>
-              )}
-              <input
-                type="datetime-local"
-                value={v as string}
-                onChange={(e) => {
-                  const v = e.target.value;
-                  updateValue(index, v);
-                }}
-                disabled={disable}
-              />
-              {e != null && (
-                <div className="cru-input-panel-error-text">{e}</div>
-              )}
-              {item.helper && (
-                <div className="cru-input-panel-helper-text">
-                  {convertI18nText(item.helper, t)}
-                </div>
-              )}
-            </div>
-          );
-        }
-      })}
-    </div>
-  );
-};
-
-export default InputPanel;
diff --git a/FrontEnd/src/views/register/index.tsx b/FrontEnd/src/views/register/index.tsx
index c1b95ff7..3ecbca63 100644
--- a/FrontEnd/src/views/register/index.tsx
+++ b/FrontEnd/src/views/register/index.tsx
@@ -7,10 +7,7 @@ import { getHttpTokenClient } from "@/http/token";
 import { userService, useUser } from "@/services/user";
 
 import { LoadingButton } from "../common/button";
-import InputPanel, {
-  hasError,
-  InputPanelError,
-} from "../common/input/InputPanel";
+import { hasError, InputPanelError } from "../common/input/InputGroup";
 
 import "./index.css";
 
@@ -101,7 +98,7 @@ const RegisterPage: React.FC = () => {
           setDirty(newDirty);
           const e = validate(
             [username, password, confirmPassword, registerCode],
-            newDirty
+            newDirty,
           );
           if (hasError(e)) {
             setInputError(e);
@@ -128,7 +125,7 @@ const RegisterPage: React.FC = () => {
                     setResultError("error.network");
                   }
                   setProcess(false);
-                }
+                },
               );
           }
         }}
-- 
cgit v1.2.3

