aboutsummaryrefslogtreecommitdiff
path: root/FrontEnd/src/views/common
diff options
context:
space:
mode:
authorcrupest <crupest@outlook.com>2023-07-28 00:40:58 +0800
committercrupest <crupest@outlook.com>2023-07-28 00:41:30 +0800
commita9dc6b16d6730d8d1dc1ea2fab8ab3830fe56ce4 (patch)
tree98b1c06da608f52df10e79064237c659b0550d10 /FrontEnd/src/views/common
parentdba8216b13a9473fd25674905e3048084794941e (diff)
downloadtimeline-a9dc6b16d6730d8d1dc1ea2fab8ab3830fe56ce4.tar.gz
timeline-a9dc6b16d6730d8d1dc1ea2fab8ab3830fe56ce4.tar.bz2
timeline-a9dc6b16d6730d8d1dc1ea2fab8ab3830fe56ce4.zip
...
Diffstat (limited to 'FrontEnd/src/views/common')
-rw-r--r--FrontEnd/src/views/common/dialog/OperationDialog.tsx108
-rw-r--r--FrontEnd/src/views/common/input/InputGroup.tsx184
2 files changed, 183 insertions, 109 deletions
diff --git a/FrontEnd/src/views/common/dialog/OperationDialog.tsx b/FrontEnd/src/views/common/dialog/OperationDialog.tsx
index ad9bf5c1..97d135e9 100644
--- a/FrontEnd/src/views/common/dialog/OperationDialog.tsx
+++ b/FrontEnd/src/views/common/dialog/OperationDialog.tsx
@@ -5,12 +5,11 @@ import { useC, Text, ThemeColor } from "../common";
import Button from "../button/Button";
import {
- default as InputGroup,
- InputErrors,
- InputList,
- Validator,
- Values,
- useDirties,
+ useInputs,
+ InputGroup,
+ InitializeInfo as InputInitializer,
+ InputValueDict,
+ InputScheme,
} from "../input/InputGroup";
import LoadingButton from "../button/LoadingButton";
import Dialog from "./Dialog";
@@ -36,42 +35,47 @@ function OperationDialogPrompt(props: OperationDialogPromptProps) {
);
}
-export interface OperationDialogProps<TData, Inputs extends InputList> {
+export interface OperationDialogProps<TData> {
open: boolean;
- onClose: () => void;
+ close: () => void;
color?: ThemeColor;
title: Text;
inputPrompt?: Text;
- processPrompt?: Text;
successPrompt?: (data: TData) => ReactNode;
failurePrompt?: (error: unknown) => ReactNode;
- inputs: Inputs;
- validator?: Validator<Inputs>;
+ inputInit?: InputInitializer;
+ inputScheme?: InputScheme;
- onProcess: (inputs: Values<Inputs>) => Promise<TData>;
+ onProcess: (inputs: InputValueDict) => Promise<TData>;
onSuccessAndClose?: (data: TData) => void;
}
-function OperationDialog<TData, Inputs extends InputList>(
- props: OperationDialogProps<TData, Inputs>,
-) {
+function OperationDialog<TData>(props: OperationDialogProps<TData>) {
const {
open,
- onClose,
+ close,
color,
title,
inputPrompt,
- processPrompt,
successPrompt,
failurePrompt,
- inputs,
- validator,
+ inputInit,
+ inputScheme,
onProcess,
onSuccessAndClose,
} = props;
+ if (process.env.NODE_ENV === "development") {
+ if (inputScheme == null && inputInit == null) {
+ throw Error("Scheme or Init? Choose one and create one.");
+ }
+ if (inputScheme != null && inputInit != null) {
+ throw Error("Scheme or Init? Choose one and drop one");
+ }
+ }
+
const c = useC();
type Step =
@@ -87,15 +91,17 @@ function OperationDialog<TData, Inputs extends InputList>(
};
const [step, setStep] = useState<Step>({ type: "input" });
- const [values, setValues] = useState<Values<Inputs>>();
- const [errors, setErrors] = useState<InputErrors>();
- const [dirties, setDirties, dirtyAll] = useDirties();
- function close() {
+ const { inputGroupProps, hasError, setAllDisabled, confirm } = useInputs({
+ /* eslint-disable-next-line @typescript-eslint/no-non-null-assertion */
+ init: inputInit ?? { scheme: inputScheme! },
+ });
+
+ function onClose() {
if (step.type !== "process") {
- props.onClose();
+ close();
if (step.type === "success" && props.onSuccessAndClose) {
- props.onSuccessAndClose(step.data);
+ onSuccessAndClose?.(step.data);
}
} else {
console.log("Attempt to close modal dialog when processing.");
@@ -103,14 +109,11 @@ function OperationDialog<TData, Inputs extends InputList>(
}
function onConfirm() {
- setStep({ type: "process" });
- props
- .onProcess(
- values.map((value, index) =>
- finalValueMapperMap[inputScheme[index].type](value as never),
- ) as Values,
- )
- .then(
+ const result = confirm();
+ if (result.type === "ok") {
+ setStep({ type: "process" });
+ setAllDisabled(true);
+ onProcess(result.values).then(
(d) => {
setStep({
type: "success",
@@ -124,31 +127,21 @@ function OperationDialog<TData, Inputs extends InputList>(
});
},
);
+ }
}
let body: ReactNode;
if (step.type === "input" || step.type === "process") {
const isProcessing = step.type === "process";
- const hasError = errors.length > 0;
body = (
<div className="cru-operation-dialog-main-area">
<div>
- <OperationDialogPrompt customMessage={c(props.inputPrompt)} />
+ <OperationDialogPrompt customMessage={c(inputPrompt)} />
<InputGroup
- className="cru-operation-dialog-input-group"
+ containerClassName="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}
+ {...inputGroupProps}
/>
</div>
<hr />
@@ -157,19 +150,14 @@ function OperationDialog<TData, Inputs extends InputList>(
text="operationDialog.cancel"
color="secondary"
outline
- onClick={close}
+ onClick={onClose}
disabled={isProcessing}
/>
<LoadingButton
color={color}
loading={isProcessing}
disabled={hasError}
- onClick={() => {
- dirtyAll();
- if (validate(values)) {
- onConfirm();
- }
- }}
+ onClick={onConfirm}
>
{c("operationDialog.confirm")}
</LoadingButton>
@@ -183,32 +171,32 @@ function OperationDialog<TData, Inputs extends InputList>(
result.type === "success"
? {
message: "operationDialog.success",
- customMessage: props.successPrompt?.(result.data),
+ customMessage: successPrompt?.(result.data),
}
: {
message: "operationDialog.error",
- customMessage: props.failurePrompt?.(result.data),
+ customMessage: failurePrompt?.(result.data),
};
body = (
<div className="cru-operation-dialog-main-area">
<OperationDialogPrompt {...promptProps} />
<hr />
<div className="cru-dialog-bottom-area">
- <Button text="operationDialog.ok" color="primary" onClick={close} />
+ <Button text="operationDialog.ok" color="primary" onClick={onClose} />
</div>
</div>
);
}
return (
- <Dialog open={props.open} onClose={close}>
+ <Dialog open={open} onClose={onClose}>
<div
className={classNames(
"cru-operation-dialog-container",
- `cru-${props.themeColor ?? "primary"}`,
+ `cru-${color ?? "primary"}`,
)}
>
- <div className="cru-operation-dialog-title">{c(props.title)}</div>
+ <div className="cru-operation-dialog-title">{c(title)}</div>
<hr />
{body}
</div>
diff --git a/FrontEnd/src/views/common/input/InputGroup.tsx b/FrontEnd/src/views/common/input/InputGroup.tsx
index 7c33def7..232edfc9 100644
--- a/FrontEnd/src/views/common/input/InputGroup.tsx
+++ b/FrontEnd/src/views/common/input/InputGroup.tsx
@@ -23,7 +23,7 @@
* `useInputs` hook takes care of logic and generate props for `InputGroup`.
*/
-import { useState, useRef, Ref } from "react";
+import { useState, Ref } from "react";
import classNames from "classnames";
import { useC, Text, ThemeColor } from "../common";
@@ -86,7 +86,7 @@ export type InputScheme = {
validator?: Validator;
};
-export type InputState = {
+export type InputData = {
values: InputValueDict;
errors: InputErrorDict;
disabled: InputDisabledDict;
@@ -95,16 +95,18 @@ export type InputState = {
export type State = {
scheme: InputScheme;
- state: InputState;
+ data: InputData;
};
-export type StateInitializer = Partial<InputState>;
+export type DataInitializeInfo = Partial<InputData>;
-export type Initializer = {
+export type InitializeInfo = {
scheme: InputScheme;
- stateInit?: Partial<InputState>;
+ dataInit?: DataInitializeInfo;
};
+export type Initialize
+
export interface InputGroupProps {
color?: ThemeColor;
containerClassName?: string;
@@ -114,7 +116,7 @@ export interface InputGroupProps {
onChange: (index: number, value: Input["value"]) => void;
}
-function cleanObject<O extends Record<string, unknown>>(o: O): O {
+function cleanObject<V>(o: Record<string, V>): Record<string, V> {
const result = { ...o };
for (const key of Object.keys(result)) {
if (result[key] == null) {
@@ -124,8 +126,23 @@ function cleanObject<O extends Record<string, unknown>>(o: O): O {
return result;
}
-export function useInputs(options: { init?: () => Initializer }): {
+export type ConfirmResult =
+ | {
+ type: "ok";
+ values: InputValueDict;
+ }
+ | {
+ type: "error";
+ errors: InputErrorDict;
+ };
+
+export function useInputs(options: {
+ init: InitializeInfo | (() => InitializeInfo);
+}): {
inputGroupProps: InputGroupProps;
+ hasError: boolean;
+ confirm: () => ConfirmResult;
+ setAllDisabled: (disabled: boolean) => void;
} {
function initializeValue(
input: InputInfo,
@@ -141,54 +158,59 @@ export function useInputs(options: { init?: () => Initializer }): {
throw new Error("Unknown input type");
}
- function initialize(initializer: Initializer): State {
- const { scheme, stateInit } = initializer;
+ function initialize(info: InitializeInfo): State {
+ const { scheme, dataInit } = info;
const { inputs, validator } = scheme;
const keys = inputs.map((input) => input.key);
if (process.env.NODE_ENV === "development") {
- const checkKeys = (dict: Record<string, unknown>) => {
- for (const key of Object.keys(dict)) {
- if (!keys.includes(key)) {
- console.warn("");
+ const checkKeys = (dict: Record<string, unknown> | undefined) => {
+ if (dict != null) {
+ for (const key of Object.keys(dict)) {
+ if (!keys.includes(key)) {
+ console.warn("");
+ }
}
}
};
- checkKeys(stateInit?.values ?? {});
- checkKeys(stateInit?.errors ?? {});
- checkKeys(stateInit?.disabled ?? {});
- checkKeys(stateInit?.dirties ?? {});
+ checkKeys(dataInit?.values);
+ checkKeys(dataInit?.errors);
+ checkKeys(dataInit?.disabled);
+ checkKeys(dataInit?.dirties);
+ }
+
+ function clean<V>(dict: Record<string, V> | undefined): Record<string, V> {
+ return dict != null ? cleanObject(dict) : {};
}
const values: InputValueDict = {};
- let errors: InputErrorDict = cleanObject(
- initializer.stateInit?.errors ?? {},
- );
- const disabled: InputDisabledDict = cleanObject(
- initializer.stateInit?.disabled ?? {},
- );
- const dirties: InputDirtyDict = cleanObject(
- initializer.stateInit?.dirties ?? {},
- );
+ const disabled: InputDisabledDict = clean(info.dataInit?.disabled);
+ const dirties: InputDirtyDict = clean(info.dataInit?.dirties);
for (let i = 0; i < inputs.length; i++) {
const input = inputs[i];
const { key } = input;
- values[key] = initializeValue(input, stateInit?.values?.[key]);
- if (!(key in dirties)) {
- dirties[key] = false;
- }
+ values[key] = initializeValue(input, dataInit?.values?.[key]);
}
- if (Object.keys(errors).length === 0 && validator != null) {
- errors = validator(values, inputs);
+ let errors = info.dataInit?.errors;
+
+ if (errors != null) {
+ if (process.env.NODE_ENV === "development") {
+ console.log(
+ "You explicitly set errors (not undefined) in initializer, so validator won't run.",
+ );
+ }
+ errors = cleanObject(errors);
+ } else {
+ errors = validator?.(values, inputs) ?? {};
}
return {
scheme,
- state: {
+ data: {
values,
errors,
disabled,
@@ -198,31 +220,95 @@ export function useInputs(options: { init?: () => Initializer }): {
}
const { init } = options;
+ const initializer = typeof init === "function" ? init : () => init;
+
+ const [state, setState] = useState<State>(() => initialize(initializer()));
+
+ const { scheme, data } = state;
+ const { validator } = scheme;
+
+ function createAllBooleanDict(value: boolean): Record<string, boolean> {
+ const result: InputDirtyDict = {};
+ for (const key of scheme.inputs.map((input) => input.key)) {
+ result[key] = value;
+ }
+ return result;
+ }
+
+ const createAllDirties = () => createAllBooleanDict(true);
const componentInputs: Input[] = [];
- for (let i = 0; i < inputs.length; i++) {
- const input = { ...inputs[i] };
- const error = dirties[i]
- ? errors.find((e) => e.index === i)?.message
- : undefined;
- const componentInput: ExtendInputForComponent<Input> = {
+ for (let i = 0; i < scheme.inputs.length; i++) {
+ const input = scheme.inputs[i];
+ const value = data.values[input.key];
+ const error = data.errors[input.key];
+ const disabled = data.disabled[input.key] ?? false;
+ const dirty = data.dirties[input.key] ?? false;
+ const componentInput: Input = {
...input,
- value: values[i],
+ value: value as never,
disabled,
- error,
+ error: dirty ? error : undefined,
};
componentInputs.push(componentInput);
}
- const dirtyAll = () => {
- if (dirties != null) {
- setDirties(new Array(dirties.length).fill(true) as Dirties<Inputs>);
- }
- };
-
return {
- inputGroupProps: {},
+ inputGroupProps: {
+ inputs: componentInputs,
+ onChange: (index, value) => {
+ const input = scheme.inputs[index];
+ const { key } = input;
+ const newValues = { ...data.values, [key]: value };
+ const newDirties = { ...data.dirties, [key]: true };
+ const newErrors = validator?.(newValues, scheme.inputs) ?? {};
+ setState({
+ scheme,
+ data: {
+ ...data,
+ values: newValues,
+ errors: newErrors,
+ dirties: newDirties,
+ },
+ });
+ },
+ },
+ hasError: Object.keys(data.errors).length > 0,
+ confirm() {
+ const newDirties = createAllDirties();
+ const newErrors = validator?.(data.values, scheme.inputs) ?? {};
+
+ setState({
+ scheme,
+ data: {
+ ...data,
+ dirties: newDirties,
+ errors: newErrors,
+ },
+ });
+
+ if (Object.keys(newErrors).length === 0) {
+ return {
+ type: "error",
+ errors: newErrors,
+ };
+ } else {
+ return {
+ type: "ok",
+ values: data.values,
+ };
+ }
+ },
+ setAllDisabled(disabled: boolean) {
+ setState({
+ scheme,
+ data: {
+ ...data,
+ disabled: createAllBooleanDict(disabled),
+ },
+ });
+ },
};
}