diff options
author | crupest <crupest@outlook.com> | 2023-07-28 00:40:58 +0800 |
---|---|---|
committer | crupest <crupest@outlook.com> | 2023-07-28 00:41:30 +0800 |
commit | a9dc6b16d6730d8d1dc1ea2fab8ab3830fe56ce4 (patch) | |
tree | 98b1c06da608f52df10e79064237c659b0550d10 /FrontEnd/src/views/common | |
parent | dba8216b13a9473fd25674905e3048084794941e (diff) | |
download | timeline-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.tsx | 108 | ||||
-rw-r--r-- | FrontEnd/src/views/common/input/InputGroup.tsx | 184 |
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), + }, + }); + }, }; } |