= (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;