aboutsummaryrefslogtreecommitdiff
path: root/FrontEnd/src/views
diff options
context:
space:
mode:
Diffstat (limited to 'FrontEnd/src/views')
-rw-r--r--FrontEnd/src/views/common/dialog/OperationDialog.css17
-rw-r--r--FrontEnd/src/views/common/dialog/OperationDialog.tsx409
-rw-r--r--FrontEnd/src/views/common/input/InputGroup.css (renamed from FrontEnd/src/views/common/input/InputPanel.css)0
-rw-r--r--FrontEnd/src/views/common/input/InputGroup.tsx362
-rw-r--r--FrontEnd/src/views/common/input/InputPanel.tsx246
-rw-r--r--FrontEnd/src/views/register/index.tsx9
6 files changed, 452 insertions, 591 deletions
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/InputPanel.css b/FrontEnd/src/views/common/input/InputGroup.css
index f9d6ac8b..f9d6ac8b 100644
--- a/FrontEnd/src/views/common/input/InputPanel.css
+++ b/FrontEnd/src/views/common/input/InputGroup.css
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.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);
- }
+ },
);
}
}}