import { faCheck, faCircleNotch, faExclamationTriangle, faTimes, } from "@fortawesome/free-solid-svg-icons"; import { FontAwesomeIcon } from "@fortawesome/react-fontawesome"; import React, { FunctionComponent, PropsWithChildren, useCallback, useEffect, useMemo, useState, } from "react"; import { Alert, Button, ButtonProps, Container } from "react-bootstrap"; import { LoadingIndicator } from "."; import { useNotification } from "../@redux/hooks/site"; import { Reload } from "../utilites"; import { Selector, SelectorProps } from "./inputs"; enum RequestState { Success, Error, Invalid, } interface ChildProps { data: NonNullable>; error?: Error; } interface AsyncStateOverlayProps { state: AsyncState; exist?: (item: T) => boolean; children?: FunctionComponent>; } function defaultExist(item: any) { if (item instanceof Array) { return item.length !== 0; } else { return item !== null && item !== undefined; } } export function AsyncStateOverlay(props: AsyncStateOverlayProps) { const { exist, state, children } = props; const missing = exist ? !exist(state.data) : !defaultExist(state.data); const onError = useNotification(); useEffect(() => { if (!state.updating && state.error !== undefined && !missing) { onError({ type: "error", message: state.error.message, }); } }, [state, onError, missing]); if (state.updating) { if (missing) { return ; } } else { if (state.error && missing) { return ( Ouch! You got an error

{state.error.message}


); } } return children ? children({ data: state.data!, error: state.error }) : null; } interface PromiseProps { promise: () => Promise; children: FunctionComponent; } export function PromiseOverlay({ promise, children }: PromiseProps) { const [item, setItem] = useState(null); useEffect(() => { promise() .then((result) => setItem(result)) .catch(() => {}); }, [promise]); if (item === null) { return ; } else { return children(item); } } type ExtractAS> = Unpacked>; type AsyncSelectorProps> = { state: T; label: (item: ExtractAS) => string; }; type RemovedSelectorProps = Omit< SelectorProps, "loading" | "options" >; export function AsyncSelector< T extends AsyncState, M extends boolean = false >( props: Override, RemovedSelectorProps, M>> ) { const { label, state, ...selector } = props; const options = useMemo>[]>( () => state.data.map((v) => ({ label: label(v), value: v, })), [state, label] ); return ( ); } interface AsyncButtonProps { as?: ButtonProps["as"]; variant?: ButtonProps["variant"]; size?: ButtonProps["size"]; className?: string; disabled?: boolean; onChange?: (v: boolean) => void; noReset?: boolean; animation?: boolean; promise: () => Promise | null; onSuccess?: (result: T) => void; error?: () => void; } export function AsyncButton( props: PropsWithChildren> ): JSX.Element { const { children: propChildren, className, promise, onSuccess, noReset, animation, error, onChange, disabled, ...button } = props; const [loading, setLoading] = useState(false); const [state, setState] = useState(RequestState.Invalid); const [, setHandle] = useState>(null); useEffect(() => { if (noReset) { return; } if (state === RequestState.Error || state === RequestState.Success) { const handle = setTimeout(() => setState(RequestState.Invalid), 2 * 1000); setHandle(handle); } // Clear timeout handle so we wont leak memory return () => { setHandle((handle) => { if (handle) { clearTimeout(handle); } return null; }); }; }, [state, noReset]); const click = useCallback(() => { if (state !== RequestState.Invalid) { return; } const result = promise(); if (result) { setLoading(true); onChange && onChange(true); result .then((res) => { setState(RequestState.Success); onSuccess && onSuccess(res); }) .catch(() => { setState(RequestState.Error); error && error(); }) .finally(() => { setLoading(false); onChange && onChange(false); }); } }, [error, onChange, promise, onSuccess, state]); const showAnimation = animation ?? true; let children = propChildren; if (showAnimation) { if (loading) { children = ; } if (state === RequestState.Success) { children = ; } else if (state === RequestState.Error) { children = ; } } return ( ); }