import React, { useState } from 'react'; import { useTranslation } from 'react-i18next'; import { Spinner, Container, ModalBody, Label, Input, FormGroup, FormFeedback, ModalFooter, Button, Modal, ModalHeader, FormText, } from 'reactstrap'; import { UiLogicError } from '../common'; const DefaultProcessPrompt: React.FC = (_) => { return ( ); }; 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' >; 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') { 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 && } { const v = e.target.value; const newValues = updateValue(index, v); setInputError( calculateError( inputError, index, item.validator?.(v, newValues) ) ); }} invalid={error != null} {...item.textFieldProps} /> {error != null && {error}} {item.helperText && {t(item.helperText)}} ); } else if (item.type === 'bool') { return ( { updateValue( index, (e.target as HTMLInputElement).checked ); }} /> ); } else if (item.type === 'select') { return ( { updateValue(index, event.target.value); }} > {item.options.map((option, i) => { return ( ); })} ); } })} ); } else if (step === 'process') { body = ( {props.processPrompt?.() ?? } ); } 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;