aboutsummaryrefslogtreecommitdiff
path: root/FrontEnd/src/views/common/input/InputPanel.tsx
diff options
context:
space:
mode:
Diffstat (limited to 'FrontEnd/src/views/common/input/InputPanel.tsx')
-rw-r--r--FrontEnd/src/views/common/input/InputPanel.tsx247
1 files changed, 247 insertions, 0 deletions
diff --git a/FrontEnd/src/views/common/input/InputPanel.tsx b/FrontEnd/src/views/common/input/InputPanel.tsx
new file mode 100644
index 00000000..1270cc53
--- /dev/null
+++ b/FrontEnd/src/views/common/input/InputPanel.tsx
@@ -0,0 +1,247 @@
+import React from "react";
+import classNames from "classnames";
+import { useTranslation } from "react-i18next";
+import { TwitterPicker } from "react-color";
+
+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 ColorInput {
+ type: "color";
+ label?: I18nText;
+}
+
+export interface DateTimeInput {
+ type: "datetime";
+ label?: I18nText;
+ helper?: I18nText;
+}
+
+export type Input =
+ | TextInput
+ | BoolInput
+ | SelectInput
+ | ColorInput
+ | 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 OperationInputError = (I18nText | null | undefined)[];
+
+export interface InputPanelProps<InputList extends readonly Input[]> {
+ scheme: InputList;
+ values: MapInputListToValueTypeList<InputList>;
+ onChange: (
+ values: MapInputListToValueTypeList<InputList>,
+ index: number
+ ) => void;
+ error?: OperationInputError;
+ 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;