diff --git a/.changeset/quiet-kangaroos-give.md b/.changeset/quiet-kangaroos-give.md new file mode 100644 index 0000000000..8b74bf534e --- /dev/null +++ b/.changeset/quiet-kangaroos-give.md @@ -0,0 +1,7 @@ +--- +"@comet/admin": minor +--- + +Add possibility for uncontrolled (promise-based) behavior to `FeedbackButton` + +Previously the `FeedbackButton` was controlled by the props `loading` and `hasErrors`. To enable more use cases and easier usage, a promise-based way was added. If neither of the mentioned props are passed, the component uses the promise returned by `onClick` to evaluate the idle, loading and error state. diff --git a/packages/admin/admin/src/common/buttons/feedback/FeedbackButton.tsx b/packages/admin/admin/src/common/buttons/feedback/FeedbackButton.tsx index cee9878a2f..abe6848f4e 100644 --- a/packages/admin/admin/src/common/buttons/feedback/FeedbackButton.tsx +++ b/packages/admin/admin/src/common/buttons/feedback/FeedbackButton.tsx @@ -36,9 +36,10 @@ export interface FeedbackButtonProps root: typeof LoadingButton; tooltip: typeof CometTooltip; }>, - LoadingButtonProps { - loading?: boolean; + Omit { + onClick?: () => void | Promise; hasErrors?: boolean; + loading?: boolean; startIcon?: ReactNode; endIcon?: ReactNode; tooltipSuccessMessage?: ReactNode; @@ -49,6 +50,7 @@ type FeedbackButtonDisplayState = "idle" | "loading" | "success" | "error"; export function FeedbackButton(inProps: FeedbackButtonProps) { const { + onClick, loading, hasErrors, children, @@ -73,6 +75,8 @@ export function FeedbackButton(inProps: FeedbackButtonProps) { displayState, }; + const isUncontrolled = loading === undefined && hasErrors === undefined; + const resolveTooltipForDisplayState = (displayState: FeedbackButtonDisplayState) => { switch (displayState) { case "error": @@ -84,7 +88,26 @@ export function FeedbackButton(inProps: FeedbackButtonProps) { } }; + const handleOnClick = + isUncontrolled && onClick + ? async () => { + try { + setDisplayState("loading"); + await onClick(); + setDisplayState("success"); + } catch (_) { + setDisplayState("error"); + } finally { + setTimeout(() => { + setDisplayState("idle"); + }, 3000); + } + } + : onClick; + useEffect(() => { + if (isUncontrolled) return; + let timeoutId: number | undefined; let timeoutDuration: number | undefined; let newDisplayState: FeedbackButtonDisplayState; @@ -95,7 +118,7 @@ export function FeedbackButton(inProps: FeedbackButtonProps) { timeoutDuration = 0; newDisplayState = "error"; } else if (displayState === "loading" && !loading && !hasErrors) { - timeoutDuration = 500; + timeoutDuration = 50; newDisplayState = "success"; } else if (displayState === "error") { timeoutDuration = 5000; @@ -115,7 +138,7 @@ export function FeedbackButton(inProps: FeedbackButtonProps) { window.clearTimeout(timeoutId); } }; - }, [displayState, loading, hasErrors]); + }, [displayState, loading, hasErrors, isUncontrolled]); const tooltip = ( } startIcon={startIcon && tooltip} diff --git a/packages/admin/admin/src/dataGrid/CrudContextMenu.tsx b/packages/admin/admin/src/dataGrid/CrudContextMenu.tsx index 4172dcd6b9..b445b44c3c 100644 --- a/packages/admin/admin/src/dataGrid/CrudContextMenu.tsx +++ b/packages/admin/admin/src/dataGrid/CrudContextMenu.tsx @@ -14,14 +14,12 @@ import { RowActionsMenu } from "../rowActions/RowActionsMenu"; interface DeleteDialogProps { dialogOpen: boolean; - loading?: boolean; - hasErrors?: boolean; - onDelete: () => void; + onDelete: () => Promise; onCancel: () => void; } const DeleteDialog = (props: DeleteDialogProps) => { - const { dialogOpen, loading, hasErrors, onDelete, onCancel } = props; + const { dialogOpen, onDelete, onCancel } = props; return ( @@ -38,8 +36,6 @@ const DeleteDialog = (props: DeleteDialogProps) => { } onClick={onDelete} - loading={loading} - hasErrors={hasErrors} color="primary" variant="contained" tooltipErrorMessage={} @@ -68,13 +64,10 @@ export function CrudContextMenu({ url, onPaste, onDelete, refetchQueri const [deleteDialogOpen, setDeleteDialogOpen] = useState(false); const [copyLoading, setCopyLoading] = useState(false); const [pasting, setPasting] = useState(false); - const [deleteLoading, setDeleteLoading] = useState(false); - const [hasDeleteErrors, setHasDeleteErrors] = useState(false); const handleDeleteClick = async () => { if (!onDelete) return; - setHasDeleteErrors(false); - setDeleteLoading(true); + try { await onDelete({ client, @@ -82,10 +75,7 @@ export function CrudContextMenu({ url, onPaste, onDelete, refetchQueri if (refetchQueries) await client.refetchQueries({ include: refetchQueries }); setDeleteDialogOpen(false); } catch (_) { - setHasDeleteErrors(true); throw new Error("Delete failed"); - } finally { - setDeleteLoading(false); } }; @@ -186,13 +176,7 @@ export function CrudContextMenu({ url, onPaste, onDelete, refetchQueri )} - setDeleteDialogOpen(false)} - onDelete={handleDeleteClick} - /> + setDeleteDialogOpen(false)} onDelete={handleDeleteClick} /> ); } diff --git a/storybook/src/docs/components/Toolbar/stories/FeedbackButton.stories.tsx b/storybook/src/docs/components/Toolbar/stories/FeedbackButton.stories.tsx index 1743910cb1..fdc2cc3726 100644 --- a/storybook/src/docs/components/Toolbar/stories/FeedbackButton.stories.tsx +++ b/storybook/src/docs/components/Toolbar/stories/FeedbackButton.stories.tsx @@ -1,38 +1,116 @@ -import { FeedbackButton, Toolbar, ToolbarActions, ToolbarFillSpace, ToolbarTitleItem } from "@comet/admin"; -import { Assets } from "@comet/admin-icons"; +import { FeedbackButton } from "@comet/admin"; +import { Check, Close } from "@comet/admin-icons"; +import { Card, CardContent, Typography } from "@mui/material"; import { storiesOf } from "@storybook/react"; import * as React from "react"; import { storyRouterDecorator } from "../../../../story-router.decorator"; -import { toolbarDecorator } from "../toolbar.decorator"; storiesOf("stories/components/Toolbar/Feedback Button", module) - .addDecorator(toolbarDecorator()) .addDecorator(storyRouterDecorator()) - .add("Feedback", () => { + .add("Controlled", () => { const [loading, setLoading] = React.useState(false); + const [loadingError, setLoadingError] = React.useState(false); + const [hasErrors, setHasErrors] = React.useState(false); + + const onClick = () => { + setLoading(true); + + setTimeout(() => { + setLoading(false); + }, 2000); + }; + + const onClickError = () => { + setLoadingError(true); + + setTimeout(() => { + setHasErrors(true); + setLoadingError(false); + }, 2000); + + setTimeout(() => { + setHasErrors(false); + }, 4000); + }; + return ( - - Feedback Button - - + + + Controlled FeedbackButton + + This FeedbackButton is controlled by the props loading and hasErrors. A promise returned by onClick will be ignored if one of + the props is defined. + + + Success + { - setLoading(true); - setTimeout(() => { - setLoading(false); - }, 1000); - }} - startIcon={} + onClick={onClick} + startIcon={} + tooltipSuccessMessage="Saving was successful" + tooltipErrorMessage="Error while saving" + > + Click me + + + Error + + } + tooltipSuccessMessage="Saving was successful" + tooltipErrorMessage="Error while saving" + > + Click me + + + + ); + }) + .add("Uncontrolled", () => { + return ( + + + Uncontrolled FeedbackButton + + This FeedbackButton is controlled by the promise returned by the onClick function. Both the loading and hasError props have to + be undefined. + + + Success + + new Promise((resolve) => setTimeout(resolve, 2000))} + startIcon={} + tooltipSuccessMessage="Saving was successful" + tooltipErrorMessage="Error while saving" + > + Click me + + + Error + + new Promise((_, reject) => setTimeout(reject, 2000))} + startIcon={} tooltipSuccessMessage="Saving was successful" tooltipErrorMessage="Error while saving" > - Feedback + Click me - - + + ); });