From 4f8d933994c576dc180fae23a3dca477d2354939 Mon Sep 17 00:00:00 2001 From: crupest Date: Mon, 24 Jul 2023 21:48:48 +0800 Subject: ... --- 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 ----------------- 4 files changed, 387 insertions(+), 271 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') 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 = InputTypeToValueMap[I["type"]]; + +export type MapInputListToValueList = { + [Index in keyof Tuple]: MapInputToValue; +}; + +export type MapInputListTo = { + [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 = + InputTypeToInitialValueMap[I["type"]]; + +export type InputValueTransformers = { + [I in Input as I["type"]]: ( + input: I, + value: MapInputToInitialValue, + ) => MapInputToValue; +}; + +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) => MapInputToValue; +// }; +// +// 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: MapInputListToValueList, +) => InputErrors; +export type Values = MapInputListToValueList; +export type Dirties = MapInputListTo; + +export function useInputs( + inputs: Inputs, + validator?: Validator, +): { + inputs: Inputs; + validator?: Validator; + dirties: Dirties | undefined; + setDirties: (dirties: Dirties) => void; + dirtyAll: () => void; +} { + const [dirties, setDirties] = useState>(); + + return { + inputs, + validator, + values, + dirties, + setDirties, + dirtyAll: () => { + if (dirties != null) { + setDirties(new Array(dirties.length).fill(true) as Dirties); + } + }, + }; +} + +export interface InputGroupProps { + inputs: Inputs; + validator?: Validator; + + values?: Values; + onChange: ( + values: Values, + errors: InputErrors, + trigger: number, // May be -1, which means I don't know who trigger this change. + ) => void; + errors: InputErrors; + dirties: Dirties; + onDirty: (dirties: Dirties) => void; + disabled?: boolean; + + color?: ThemeColor; + className?: string; + containerRef?: Ref; +} + +export default function InputGroup({ + color, + inputs, + validator, + values, + errors, + disabled, + onChange, + dirties, + onDirty, + containerRef, + className, +}: InputGroupProps) { + const c = useC(); + + type Values = MapInputListToValueList; + type Dirties = MapInputListTo; + + 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 ( +
+ {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 ( +
+ {item.label && ( + + )} + { + const v = event.target.value; + updateValue(index, v); + }} + disabled={disabled} + /> + {error &&
{c(error)}
} + {item.helperText && ( +
+ {c(item.helperText)} +
+ )} +
+ ); + } else if (item.type === "bool") { + return ( +
+ { + const v = event.currentTarget.checked; + updateValue(index, v); + }} + disabled={disabled} + /> + + {error &&
{c(error)}
} + {item.helperText && ( +
+ {c(item.helperText)} +
+ )} +
+ ); + } else if (item.type === "select") { + return ( +
+ + +
+ ); + } else if (item.type === "datetime") { + return ( +
+ {item.label && ( + + )} + { + 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 &&
{c(error)}
} +
+ ); + } + })} +
+ ); +} 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 extends keyof InputTypeToValueTypeMap - ? InputTypeToValueTypeMap[Type] - : never; - -type MapInputToValueType = T extends Input - ? MapInputTypeToValueType - : T; - -type MapInputListToValueTypeList = { - [Index in keyof Tuple]: MapInputToValueType; -} & { 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 { - scheme: InputList; - values: MapInputListToValueTypeList; - onChange: ( - values: MapInputListToValueTypeList, - index: number, - ) => void; - error?: InputPanelError; - disable?: boolean; -} - -const InputPanel = ( - props: InputPanelProps, -): 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, - index, - ); - }; - - return ( -
- {scheme.map((item, index) => { - const v = values[index]; - const e: string | null = convertI18nText(error?.[index], t); - - if (item.type === "text") { - return ( -
- {item.label && ( - - )} - { - const v = e.target.value; - updateValue(index, v); - }} - disabled={disable} - /> - {e &&
{e}
} - {item.helper && ( -
- {convertI18nText(item.helper, t)} -
- )} -
- ); - } else if (item.type === "bool") { - return ( -
- { - const value = event.currentTarget.checked; - updateValue(index, value); - }} - disabled={disable} - /> - - {e != null && ( -
{e}
- )} - {item.helper && ( -
- {convertI18nText(item.helper, t)} -
- )} -
- ); - } else if (item.type === "select") { - return ( -
- - -
- ); - } else if (item.type === "color") { - return ( -
- - updateValue(index, result.hex)} - /> -
- ); - } else if (item.type === "datetime") { - return ( -
- {item.label && ( - - )} - { - const v = e.target.value; - updateValue(index, v); - }} - disabled={disable} - /> - {e != null && ( -
{e}
- )} - {item.helper && ( -
- {convertI18nText(item.helper, t)} -
- )} -
- ); - } - })} -
- ); -}; - -export default InputPanel; -- cgit v1.2.3