aboutsummaryrefslogtreecommitdiff
path: root/FrontEnd/src/views/common/input
diff options
context:
space:
mode:
Diffstat (limited to 'FrontEnd/src/views/common/input')
-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
3 files changed, 362 insertions, 246 deletions
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;