From 9832831c5ee2030b61fdaa4af6d9fbd51d81e75f Mon Sep 17 00:00:00 2001 From: Josh Hawkins <32435876+hawkeye217@users.noreply.github.com> Date: Sun, 11 Aug 2024 07:25:09 -0500 Subject: [PATCH] Add confirmation dialog before deleting review items (#12950) --- web/src/components/card/ReviewCard.tsx | 189 +++++++++++++----- .../components/filter/ReviewActionGroup.tsx | 130 ++++++++---- web/src/components/player/VideoControls.tsx | 2 +- web/src/hooks/use-keyboard-listener.tsx | 29 ++- web/src/views/live/LiveDashboardView.tsx | 29 +-- 5 files changed, 263 insertions(+), 116 deletions(-) diff --git a/web/src/components/card/ReviewCard.tsx b/web/src/components/card/ReviewCard.tsx index 64f52bd1b..33032d2b8 100644 --- a/web/src/components/card/ReviewCard.tsx +++ b/web/src/components/card/ReviewCard.tsx @@ -6,7 +6,7 @@ import { getIconForLabel } from "@/utils/iconUtil"; import { isDesktop, isIOS, isSafari } from "react-device-detect"; import useSWR from "swr"; import TimeAgo from "../dynamic/TimeAgo"; -import { useCallback, useMemo, useState } from "react"; +import { useCallback, useMemo, useRef, useState } from "react"; import useImageLoaded from "@/hooks/use-image-loaded"; import ImageLoadingIndicator from "../indicators/ImageLoadingIndicator"; import { FaCompactDisc } from "react-icons/fa"; @@ -18,9 +18,20 @@ import { ContextMenuItem, ContextMenuTrigger, } from "../ui/context-menu"; +import { + AlertDialog, + AlertDialogAction, + AlertDialogCancel, + AlertDialogContent, + AlertDialogDescription, + AlertDialogFooter, + AlertDialogHeader, + AlertDialogTitle, +} from "../ui/alert-dialog"; import { Drawer, DrawerContent } from "../ui/drawer"; import axios from "axios"; import { toast } from "sonner"; +import useKeyboardListener from "@/hooks/use-keyboard-listener"; type ReviewCardProps = { event: ReviewSegment; @@ -46,6 +57,8 @@ export default function ReviewCard({ ); const [optionsOpen, setOptionsOpen] = useState(false); + const [deleteDialogOpen, setDeleteDialogOpen] = useState(false); + const bypassDialogRef = useRef(false); const onMarkAsReviewed = useCallback(async () => { await axios.post(`reviews/viewed`, { ids: [event.id] }); @@ -92,6 +105,18 @@ export default function ReviewCard({ setOptionsOpen(false); }, [event]); + useKeyboardListener(["Shift"], (_, modifiers) => { + bypassDialogRef.current = modifiers.shift; + }); + + const handleDelete = useCallback(() => { + if (bypassDialogRef.current) { + onDelete(); + } else { + setDeleteDialogOpen(true); + } + }, [bypassDialogRef, onDelete]); + const content = (
- {content} - - -
- -
Export
-
-
- {!event.has_been_reviewed && ( + <> + setDeleteDialogOpen(!deleteDialogOpen)} + > + + + Confirm Delete + + + Are you sure you want to delete all recorded video associated with + this review item? +
+
+ Hold the Shift key to bypass this dialog in the future. +
+ + setOptionsOpen(false)}> + Cancel + + + Delete + + +
+
+ + {content} +
- -
Mark as reviewed
+ +
Export
- )} - -
- -
Delete
-
-
-
-
+ {!event.has_been_reviewed && ( + +
+ +
Mark as reviewed
+
+
+ )} + +
+ +
+ {bypassDialogRef.current ? "Delete Now" : "Delete"} +
+
+
+
+ + ); } return ( - - {content} - -
- -
Export
-
- {!event.has_been_reviewed && ( + <> + setDeleteDialogOpen(!deleteDialogOpen)} + > + + + Confirm Delete + + + Are you sure you want to delete all recorded video associated with + this review item? +
+
+ Hold the Shift key to bypass this dialog in the future. +
+ + setOptionsOpen(false)}> + Cancel + + + Delete + + +
+
+ + {content} +
- -
Mark as reviewed
+ +
Export
- )} -
- -
Delete
-
-
-
+ {!event.has_been_reviewed && ( +
+ +
Mark as reviewed
+
+ )} +
+ +
+ {bypassDialogRef.current ? "Delete Now" : "Delete"} +
+
+
+
+ ); } diff --git a/web/src/components/filter/ReviewActionGroup.tsx b/web/src/components/filter/ReviewActionGroup.tsx index 49e9f561a..c637b1e35 100644 --- a/web/src/components/filter/ReviewActionGroup.tsx +++ b/web/src/components/filter/ReviewActionGroup.tsx @@ -1,10 +1,21 @@ import { FaCircleCheck } from "react-icons/fa6"; -import { useCallback } from "react"; +import { useCallback, useState } from "react"; import axios from "axios"; import { Button } from "../ui/button"; import { isDesktop } from "react-device-detect"; import { FaCompactDisc } from "react-icons/fa"; import { HiTrash } from "react-icons/hi"; +import { + AlertDialog, + AlertDialogAction, + AlertDialogCancel, + AlertDialogContent, + AlertDialogDescription, + AlertDialogFooter, + AlertDialogHeader, + AlertDialogTitle, +} from "../ui/alert-dialog"; +import useKeyboardListener from "@/hooks/use-keyboard-listener"; type ReviewActionGroupProps = { selectedReviews: string[]; @@ -34,49 +45,94 @@ export default function ReviewActionGroup({ pullLatestData(); }, [selectedReviews, setSelectedReviews, pullLatestData]); + const [deleteDialogOpen, setDeleteDialogOpen] = useState(false); + const [bypassDialog, setBypassDialog] = useState(false); + + useKeyboardListener(["Shift"], (_, modifiers) => { + setBypassDialog(modifiers.shift); + }); + + const handleDelete = useCallback(() => { + if (bypassDialog) { + onDelete(); + } else { + setDeleteDialogOpen(true); + } + }, [bypassDialog, onDelete]); + return ( -
-
-
{`${selectedReviews.length} selected`}
-
{"|"}
-
- Unselect + <> + setDeleteDialogOpen(!deleteDialogOpen)} + > + + + Confirm Delete + + + Are you sure you want to delete all recorded video associated with + the selected review items? +
+
+ Hold the Shift key to bypass this dialog in the future. +
+ + Cancel + + Delete + + +
+
+ +
+
+
{`${selectedReviews.length} selected`}
+
{"|"}
+
+ Unselect +
-
-
- {selectedReviews.length == 1 && ( +
+ {selectedReviews.length == 1 && ( + + )} - )} - - + +
-
+ ); } diff --git a/web/src/components/player/VideoControls.tsx b/web/src/components/player/VideoControls.tsx index 70d9a4be8..50b2cc045 100644 --- a/web/src/components/player/VideoControls.tsx +++ b/web/src/components/player/VideoControls.tsx @@ -141,7 +141,7 @@ export default function VideoControls({ }, [volume, muted]); const onKeyboardShortcut = useCallback( - (key: string, modifiers: KeyModifiers) => { + (key: string | null, modifiers: KeyModifiers) => { if (!modifiers.down) { return; } diff --git a/web/src/hooks/use-keyboard-listener.tsx b/web/src/hooks/use-keyboard-listener.tsx index f127cf0d8..ad9462a05 100644 --- a/web/src/hooks/use-keyboard-listener.tsx +++ b/web/src/hooks/use-keyboard-listener.tsx @@ -4,11 +4,12 @@ export type KeyModifiers = { down: boolean; repeat: boolean; ctrl: boolean; + shift: boolean; }; export default function useKeyboardListener( keys: string[], - listener: (key: string, modifiers: KeyModifiers) => void, + listener: (key: string | null, modifiers: KeyModifiers) => void, ) { const keyDownListener = useCallback( (e: KeyboardEvent) => { @@ -16,13 +17,18 @@ export default function useKeyboardListener( return; } + const modifiers = { + down: true, + repeat: e.repeat, + ctrl: e.ctrlKey || e.metaKey, + shift: e.shiftKey, + }; + if (keys.includes(e.key)) { e.preventDefault(); - listener(e.key, { - down: true, - repeat: e.repeat, - ctrl: e.ctrlKey || e.metaKey, - }); + listener(e.key, modifiers); + } else if (e.key === "Shift" || e.key === "Control" || e.key === "Meta") { + listener(null, modifiers); } }, [keys, listener], @@ -34,9 +40,18 @@ export default function useKeyboardListener( return; } + const modifiers = { + down: false, + repeat: false, + ctrl: false, + shift: false, + }; + if (keys.includes(e.key)) { e.preventDefault(); - listener(e.key, { down: false, repeat: false, ctrl: false }); + listener(e.key, modifiers); + } else if (e.key === "Shift" || e.key === "Control" || e.key === "Meta") { + listener(null, modifiers); } }, [keys, listener], diff --git a/web/src/views/live/LiveDashboardView.tsx b/web/src/views/live/LiveDashboardView.tsx index 51f46d2f2..a91afb356 100644 --- a/web/src/views/live/LiveDashboardView.tsx +++ b/web/src/views/live/LiveDashboardView.tsx @@ -31,9 +31,7 @@ import { cn } from "@/lib/utils"; import { LivePlayerError, LivePlayerMode } from "@/types/live"; import { FaCompress, FaExpand } from "react-icons/fa"; import { useResizeObserver } from "@/hooks/resize-observer"; -import useKeyboardListener, { - KeyModifiers, -} from "@/hooks/use-keyboard-listener"; +import useKeyboardListener from "@/hooks/use-keyboard-listener"; type LiveDashboardViewProps = { cameras: CameraConfig[]; @@ -250,22 +248,17 @@ export default function LiveDashboardView({ [setPreferredLiveModes], ); - const onKeyboardShortcut = useCallback( - (key: string, modifiers: KeyModifiers) => { - if (!modifiers.down) { - return; - } + useKeyboardListener(["f"], (key, modifiers) => { + if (!modifiers.down) { + return; + } - switch (key) { - case "f": - toggleFullscreen(); - break; - } - }, - [toggleFullscreen], - ); - - useKeyboardListener(["f"], onKeyboardShortcut); + switch (key) { + case "f": + toggleFullscreen(); + break; + } + }); return (