import React, { useState } from "react"; import { useTranslation } from "react-i18next"; import { Form, Button, Modal } from "react-bootstrap"; import { UiLogicError } from "@/common"; import LoadingButton from "./LoadingButton"; interface DefaultErrorPromptProps { error?: string; } const DefaultErrorPrompt: React.FC = (props) => { const { t } = useTranslation(); let result =

{t("operationDialog.error")}

; if (props.error != null) { result = ( <> {result}

{props.error}

); } return result; }; export type OperationInputOptionalError = undefined | null | string; export interface OperationInputErrorInfo { [index: number]: OperationInputOptionalError; } export type OperationInputValidator = ( value: TValue, values: (string | boolean)[] ) => OperationInputOptionalError | OperationInputErrorInfo; export interface OperationTextInputInfo { type: "text"; password?: boolean; label?: string; initValue?: string; textFieldProps?: Omit< React.InputHTMLAttributes, "type" | "value" | "onChange" | "aria-relevant" >; helperText?: string; validator?: OperationInputValidator; } export interface OperationBoolInputInfo { type: "bool"; label: string; initValue?: boolean; } export interface OperationSelectInputInfoOption { value: string; label: string; icon?: React.ReactElement; } export interface OperationSelectInputInfo { type: "select"; label: string; options: OperationSelectInputInfoOption[]; initValue?: string; } export type OperationInputInfo = | OperationTextInputInfo | OperationBoolInputInfo | OperationSelectInputInfo; interface OperationResult { type: "success" | "failure"; data: unknown; } interface OperationDialogProps { open: boolean; close: () => void; title: React.ReactNode; titleColor?: "default" | "dangerous" | "create" | string; onProcess: (inputs: (string | boolean)[]) => Promise; inputScheme?: OperationInputInfo[]; inputPrompt?: string | (() => React.ReactNode); processPrompt?: () => React.ReactNode; successPrompt?: (data: unknown) => React.ReactNode; failurePrompt?: (error: unknown) => React.ReactNode; onSuccessAndClose?: () => void; } const OperationDialog: React.FC = (props) => { const inputScheme = props.inputScheme ?? []; const { t } = useTranslation(); type Step = "input" | "process" | OperationResult; const [step, setStep] = useState("input"); const [values, setValues] = useState<(boolean | string)[]>( inputScheme.map((i) => { if (i.type === "bool") { return i.initValue ?? false; } else if (i.type === "text" || i.type === "select") { return i.initValue ?? ""; } else { throw new UiLogicError("Unknown input scheme."); } }) ); const [inputError, setInputError] = useState({}); const close = (): void => { if (step !== "process") { props.close(); if ( typeof step === "object" && step.type === "success" && props.onSuccessAndClose ) { props.onSuccessAndClose(); } } else { console.log("Attempt to close modal when processing."); } }; const onConfirm = (): void => { setStep("process"); props.onProcess(values).then( (d: unknown) => { setStep({ type: "success", data: d, }); }, (e: unknown) => { setStep({ type: "failure", data: e, }); } ); }; let body: React.ReactNode; if (step === "input" || step === "process") { const process = step === "process"; let inputPrompt = typeof props.inputPrompt === "function" ? props.inputPrompt() : props.inputPrompt; inputPrompt =
{inputPrompt}
; const updateValue = ( index: number, newValue: string | boolean ): (string | boolean)[] => { const oldValues = values; const newValues = oldValues.slice(); newValues[index] = newValue; setValues(newValues); return newValues; }; const testErrorInfo = (errorInfo: OperationInputErrorInfo): boolean => { for (let i = 0; i < inputScheme.length; i++) { if (inputScheme[i].type === "text" && errorInfo[i] != null) { return true; } } return false; }; const calculateError = ( oldError: OperationInputErrorInfo, index: number, newError: OperationInputOptionalError | OperationInputErrorInfo ): OperationInputErrorInfo => { if (newError === undefined) { return oldError; } else if (newError === null || typeof newError === "string") { return { ...oldError, [index]: newError }; } else { const newInputError: OperationInputErrorInfo = { ...oldError }; for (const [index, error] of Object.entries(newError)) { if (error !== undefined) { newInputError[+index] = error as OperationInputOptionalError; } } return newInputError; } }; const validateAll = (): boolean => { let newInputError = inputError; for (let i = 0; i < inputScheme.length; i++) { const item = inputScheme[i]; if (item.type === "text") { newInputError = calculateError( newInputError, i, item.validator?.(values[i] as string, values) ); } } const result = !testErrorInfo(newInputError); setInputError(newInputError); return result; }; body = ( <> {inputPrompt} {inputScheme.map((item, index) => { const value = values[index]; const error: string | undefined = ((e) => typeof e === "string" ? t(e) : undefined)(inputError?.[index]); if (item.type === "text") { return ( {item.label && {t(item.label)}} { const v = e.target.value; const newValues = updateValue(index, v); setInputError( calculateError( inputError, index, item.validator?.(v, newValues) ) ); }} isInvalid={error != null} disabled={process} /> {error != null && ( {error} )} {item.helperText && ( {t(item.helperText)} )} ); } else if (item.type === "bool") { return ( type="checkbox" checked={value as boolean} onChange={(event) => { updateValue(index, event.currentTarget.checked); }} label={t(item.label)} disabled={process} /> ); } else if (item.type === "select") { return ( {t(item.label)} { updateValue(index, event.target.value); }} disabled={process} > {item.options.map((option, i) => { return ( ); })} ); } })} { if (validateAll()) { onConfirm(); } }} > {t("operationDialog.confirm")} ); } else { let content: React.ReactNode; const result = step; if (result.type === "success") { content = props.successPrompt?.(result.data) ?? t("operationDialog.success"); if (typeof content === "string") content =

{content}

; } else { content = props.failurePrompt?.(result.data) ?? ; if (typeof content === "string") content = ; } body = ( <> {content} ); } const title = typeof props.title === "string" ? t(props.title) : props.title; return ( {title} {body} ); }; export default OperationDialog;