import { useState, ReactNode, ComponentPropsWithoutRef } from "react"; import classNames from "classnames"; import moment from "moment"; import { useC, Text, ThemeColor } from "../common"; import Button from "../button/Button"; import LoadingButton from "../button/LoadingButton"; import Dialog from "./Dialog"; import "./OperationDialog.css"; interface DefaultPromptProps { color?: ThemeColor; message?: Text; customMessage?: ReactNode; className?: string; } function DefaultPrompt(props: DefaultPromptProps) { const { color, message, customMessage, className } = props; const c = useC(); return (

{c(message)}

{customMessage}
); } 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 extends keyof OperationInputTypeStringToValueTypeMap ? OperationInputTypeStringToValueTypeMap[Type] : never; type MapOperationInputInfoValueType = T extends OperationDialogInput ? MapOperationInputTypeStringToValueType : T; type MapOperationInputInfoValueTypeList< Tuple extends readonly OperationDialogInput[], > = { [Index in keyof Tuple]: MapOperationInputInfoValueType; }; 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; }; type ValueValueMapper = { [T in OperationDialogInput as T["type"]]: ( item: MapOperationInputInfoValueType, ) => MapOperationInputInfoValueType; }; 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[], > { open: boolean; onClose: () => void; themeColor?: ThemeColor; title: Text; inputPrompt?: Text; processPrompt?: Text; successPrompt?: (data: TData) => ReactNode; failurePrompt?: (error: unknown) => ReactNode; inputScheme?: OperationInputInfoList; inputValidator?: ( inputs: MapOperationInputInfoValueTypeList, ) => OperationInputError; onProcess: ( inputs: MapOperationInputInfoValueTypeList, ) => Promise; onSuccessAndClose?: (data: TData) => void; } function OperationDialog< TData, OperationInputInfoList extends readonly OperationDialogInput[], >(props: OperationDialogProps) { const inputScheme = props.inputScheme ?? ([] as const); const c = useC(); type Step = | { type: "input" } | { type: "process" } | { type: "success"; data: TData; } | { type: "failure"; data: unknown; }; const [step, setStep] = useState({ type: "input" }); type Values = MapOperationInputInfoValueTypeList; const [values, setValues] = useState( () => inputScheme.map((item) => initValueMapperMap[item.type](item as never), ) as Values, ); const [dirtyList, setDirtyList] = useState(() => inputScheme.map(() => false), ); const [inputError, setInputError] = useState(); function close() { if (step.type !== "process") { props.onClose(); if (step.type === "success" && props.onSuccessAndClose) { props.onSuccessAndClose(step.data); } } else { console.log("Attempt to close modal dialog when processing."); } } function onConfirm() { setStep({ type: "process" }); props .onProcess( values.map((value, index) => finalValueMapperMap[inputScheme[index].type](value as never), ) as Values, ) .then( (d) => { setStep({ type: "success", data: d, }); }, (e: unknown) => { setStep({ type: "failure", data: e, }); }, ); } 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); body = (
{c(props.inputPrompt)}
{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 (
{item.label && ( )} { const v = event.target.value; updateValue(index, v); }} disabled={process} /> {error && (
{error}
)} {item.helperText && (
{c(item.helperText)}
)}
); } else if (item.type === "bool") { return (
{ const v = event.currentTarget.checked; updateValue(index, v); }} disabled={process} /> {error && (
{error}
)} {item.helperText && (
{c(item.helperText)}
)}
); } else if (item.type === "select") { return (
); } else if (item.type === "datetime") { return (
{item.label && ( )} { const v = event.target.value; updateValue(index, v); }} disabled={process} /> {error && (
{error}
)}
); } })}

); } else { const result = step; const promptProps: DefaultPromptProps = result.type === "success" ? { color: "success", message: "operationDialog.success", customMessage: props.successPrompt?.(result.data), } : { color: "danger", message: "operationDialog.error", customMessage: props.failurePrompt?.(result.data), }; body = (

); } return (
{c(props.title)}

{body}
); } export default OperationDialog;