From cc0cc154b9506d1961d08cb29fbc29ad815bad69 Mon Sep 17 00:00:00 2001 From: crupest Date: Mon, 24 Aug 2020 22:59:45 +0800 Subject: ... --- .../ClientApp/src/app/common/OperationDialog.tsx | 762 ++++++++++----------- 1 file changed, 381 insertions(+), 381 deletions(-) (limited to 'Timeline/ClientApp/src/app/common/OperationDialog.tsx') diff --git a/Timeline/ClientApp/src/app/common/OperationDialog.tsx b/Timeline/ClientApp/src/app/common/OperationDialog.tsx index 501a353e..bca4580c 100644 --- a/Timeline/ClientApp/src/app/common/OperationDialog.tsx +++ b/Timeline/ClientApp/src/app/common/OperationDialog.tsx @@ -1,381 +1,381 @@ -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; +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; -- cgit v1.2.3