diff options
author | crupest <crupest@outlook.com> | 2023-07-24 21:48:48 +0800 |
---|---|---|
committer | crupest <crupest@outlook.com> | 2023-07-24 21:48:48 +0800 |
commit | 4f8d933994c576dc180fae23a3dca477d2354939 (patch) | |
tree | 654763f143b3508af2d2c263ace6469268107899 /FrontEnd/src/views/common | |
parent | ae48ac24c5533653f1e1f1e6d2a33e0238222297 (diff) | |
download | timeline-4f8d933994c576dc180fae23a3dca477d2354939.tar.gz timeline-4f8d933994c576dc180fae23a3dca477d2354939.tar.bz2 timeline-4f8d933994c576dc180fae23a3dca477d2354939.zip |
...
Diffstat (limited to 'FrontEnd/src/views/common')
-rw-r--r-- | FrontEnd/src/views/common/dialog/OperationDialog.css | 17 | ||||
-rw-r--r-- | FrontEnd/src/views/common/dialog/OperationDialog.tsx | 409 | ||||
-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.tsx | 362 | ||||
-rw-r--r-- | FrontEnd/src/views/common/input/InputPanel.tsx | 246 |
5 files changed, 449 insertions, 585 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; |