From 4f8d933994c576dc180fae23a3dca477d2354939 Mon Sep 17 00:00:00 2001 From: crupest 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') 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 = (props) => { +export function ChangePasswordDialog(props: ChangePasswordDialogProps) { + const { open, close } = props; + const navigate = useNavigate(); const [redirect, setRedirect] = useState(false); return ( = (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 ( -
-

{c(message)}

+
+ {message &&

{c(message)}

} {customMessage}
); } -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 extends keyof OperationInputTypeStringToValueTypeMap - ? OperationInputTypeStringToValueTypeMap[Type] - : never; - -type MapOperationInputInfoValueType = T extends OperationDialogInput - ? MapOperationInputTypeStringToValueType - : T; - -type MapOperationInputInfoValueTypeList< - Tuple extends readonly OperationDialogInput[], -> = { - [Index in keyof Tuple]: MapOperationInputInfoValueType; -}; - -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; -}; - -type ValueValueMapper = { - [T in OperationDialogInput as T["type"]]: ( - item: MapOperationInputInfoValueType, - ) => MapOperationInputInfoValueType; -}; - -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 { 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, - ) => OperationInputError; + inputs: Inputs; + validator?: Validator; - onProcess: ( - inputs: MapOperationInputInfoValueTypeList, - ) => Promise; + onProcess: (inputs: Values) => Promise; onSuccessAndClose?: (data: TData) => void; } -function OperationDialog< - TData, - OperationInputInfoList extends readonly OperationDialogInput[], ->(props: OperationDialogProps) { - const inputScheme = props.inputScheme ?? ([] as const); +function OperationDialog( + props: OperationDialogProps, +) { + 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({ type: "input" }); - - type Values = MapOperationInputInfoValueTypeList; - - const [values, setValues] = useState( - () => - inputScheme.map((item) => - initValueMapperMap[item.type](item as never), - ) as Values, - ); - - const [dirtyList, setDirtyList] = useState(() => - inputScheme.map(() => false), - ); - - const [inputError, setInputError] = useState(); + const [values, setValues] = useState>(); + const [errors, setErrors] = useState(); + 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 = (
-
{c(props.inputPrompt)}
- {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 ( -
- {item.label && ( - - )} - { - const v = event.target.value; - updateValue(index, v); - }} - disabled={process} - /> - {error && ( -
- {error} -
- )} - {item.helperText && ( -
- {c(item.helperText)} -
- )} -
- ); - } else if (item.type === "bool") { - return ( -
- { - const v = event.currentTarget.checked; - updateValue(index, v); - }} - disabled={process} - /> - - {error && ( -
- {error} -
- )} - {item.helperText && ( -
- {c(item.helperText)} -
- )} -
- ); - } else if (item.type === "select") { - return ( -
- - -
- ); - } else if (item.type === "datetime") { - return ( -
- {item.label && ( - - )} - { - const v = event.target.value; - updateValue(index, v); - }} - disabled={process} - /> - {error && ( -
- {error} -
- )} -
- ); - } - })} + + { + setValues(values); + setErrors(errors); + }} + dirties={dirties} + onDirty={setDirties} + />

@@ -424,14 +158,14 @@ function OperationDialog< color="secondary" outline onClick={close} - disabled={process} + disabled={isProcessing} /> { - 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 = (
- +