diff --git a/src-gui/src/renderer/components/PromiseInvokeButton.tsx b/src-gui/src/renderer/components/PromiseInvokeButton.tsx new file mode 100644 index 00000000..cc9c4989 --- /dev/null +++ b/src-gui/src/renderer/components/PromiseInvokeButton.tsx @@ -0,0 +1,87 @@ +import { Button, ButtonProps, IconButton, Tooltip } from "@material-ui/core"; +import CircularProgress from "@material-ui/core/CircularProgress"; +import { useSnackbar } from "notistack"; +import { ReactNode, useEffect, useState } from "react"; + +interface IpcInvokeButtonProps { + onSuccess?: (data: T) => void; + onClick: () => Promise; + isLoadingOverride?: boolean; + isIconButton?: boolean; + loadIcon?: ReactNode; + disabled?: boolean; + displayErrorSnackbar?: boolean; + tooltipTitle?: string; +} + +export default function PromiseInvokeButton({ + disabled, + onSuccess, + onClick, + endIcon, + loadIcon, + isLoadingOverride, + isIconButton, + displayErrorSnackbar, + tooltipTitle, + ...rest +}: IpcInvokeButtonProps & ButtonProps) { + const { enqueueSnackbar } = useSnackbar(); + + const [isPending, setIsPending] = useState(false); + const [hasMinLoadingTimePassed, setHasMinLoadingTimePassed] = useState(false); + + const isLoading = (isPending && hasMinLoadingTimePassed) || isLoadingOverride; + const actualEndIcon = isLoading + ? loadIcon || + : endIcon; + + useEffect(() => { + setHasMinLoadingTimePassed(false); + setTimeout(() => setHasMinLoadingTimePassed(true), 100); + }, [isPending]); + + async function handleClick(event: React.MouseEvent) { + if (!isPending) { + try { + setIsPending(true); + let result = await onClick(); + onSuccess?.(result); + } catch (e: unknown) { + if (displayErrorSnackbar) { + enqueueSnackbar((e as Error).message, { + autoHideDuration: 60 * 1000, + variant: "error", + }); + } + } finally { + setIsPending(false); + } + } + } + + const isDisabled = disabled || isLoading; + + return ( + + + {isIconButton ? ( + + {actualEndIcon} + + ) : ( +