mirror of
				https://github.com/blakeblackshear/frigate.git
				synced 2025-10-27 10:52:11 +01:00 
			
		
		
		
	Add confirmation dialog before deleting review items (#12950)
This commit is contained in:
		
							parent
							
								
									d3259c4782
								
							
						
					
					
						commit
						9832831c5e
					
				@ -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 = (
 | 
			
		||||
    <div
 | 
			
		||||
      className="relative flex w-full cursor-pointer flex-col gap-1.5"
 | 
			
		||||
@ -158,7 +183,33 @@ export default function ReviewCard({
 | 
			
		||||
 | 
			
		||||
  if (isDesktop) {
 | 
			
		||||
    return (
 | 
			
		||||
      <ContextMenu>
 | 
			
		||||
      <>
 | 
			
		||||
        <AlertDialog
 | 
			
		||||
          open={deleteDialogOpen}
 | 
			
		||||
          onOpenChange={() => setDeleteDialogOpen(!deleteDialogOpen)}
 | 
			
		||||
        >
 | 
			
		||||
          <AlertDialogContent>
 | 
			
		||||
            <AlertDialogHeader>
 | 
			
		||||
              <AlertDialogTitle>Confirm Delete</AlertDialogTitle>
 | 
			
		||||
            </AlertDialogHeader>
 | 
			
		||||
            <AlertDialogDescription>
 | 
			
		||||
              Are you sure you want to delete all recorded video associated with
 | 
			
		||||
              this review item?
 | 
			
		||||
              <br />
 | 
			
		||||
              <br />
 | 
			
		||||
              Hold the <em>Shift</em> key to bypass this dialog in the future.
 | 
			
		||||
            </AlertDialogDescription>
 | 
			
		||||
            <AlertDialogFooter>
 | 
			
		||||
              <AlertDialogCancel onClick={() => setOptionsOpen(false)}>
 | 
			
		||||
                Cancel
 | 
			
		||||
              </AlertDialogCancel>
 | 
			
		||||
              <AlertDialogAction className="bg-destructive" onClick={onDelete}>
 | 
			
		||||
                Delete
 | 
			
		||||
              </AlertDialogAction>
 | 
			
		||||
            </AlertDialogFooter>
 | 
			
		||||
          </AlertDialogContent>
 | 
			
		||||
        </AlertDialog>
 | 
			
		||||
        <ContextMenu key={event.id}>
 | 
			
		||||
          <ContextMenuTrigger asChild>{content}</ContextMenuTrigger>
 | 
			
		||||
          <ContextMenuContent>
 | 
			
		||||
            <ContextMenuItem>
 | 
			
		||||
@ -184,18 +235,47 @@ export default function ReviewCard({
 | 
			
		||||
            <ContextMenuItem>
 | 
			
		||||
              <div
 | 
			
		||||
                className="flex w-full cursor-pointer items-center justify-start gap-2 p-2"
 | 
			
		||||
              onClick={onDelete}
 | 
			
		||||
                onClick={handleDelete}
 | 
			
		||||
              >
 | 
			
		||||
                <HiTrash className="text-secondary-foreground" />
 | 
			
		||||
              <div className="text-primary">Delete</div>
 | 
			
		||||
                <div className="text-primary">
 | 
			
		||||
                  {bypassDialogRef.current ? "Delete Now" : "Delete"}
 | 
			
		||||
                </div>
 | 
			
		||||
              </div>
 | 
			
		||||
            </ContextMenuItem>
 | 
			
		||||
          </ContextMenuContent>
 | 
			
		||||
        </ContextMenu>
 | 
			
		||||
      </>
 | 
			
		||||
    );
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  return (
 | 
			
		||||
    <>
 | 
			
		||||
      <AlertDialog
 | 
			
		||||
        open={deleteDialogOpen}
 | 
			
		||||
        onOpenChange={() => setDeleteDialogOpen(!deleteDialogOpen)}
 | 
			
		||||
      >
 | 
			
		||||
        <AlertDialogContent>
 | 
			
		||||
          <AlertDialogHeader>
 | 
			
		||||
            <AlertDialogTitle>Confirm Delete</AlertDialogTitle>
 | 
			
		||||
          </AlertDialogHeader>
 | 
			
		||||
          <AlertDialogDescription>
 | 
			
		||||
            Are you sure you want to delete all recorded video associated with
 | 
			
		||||
            this review item?
 | 
			
		||||
            <br />
 | 
			
		||||
            <br />
 | 
			
		||||
            Hold the <em>Shift</em> key to bypass this dialog in the future.
 | 
			
		||||
          </AlertDialogDescription>
 | 
			
		||||
          <AlertDialogFooter>
 | 
			
		||||
            <AlertDialogCancel onClick={() => setOptionsOpen(false)}>
 | 
			
		||||
              Cancel
 | 
			
		||||
            </AlertDialogCancel>
 | 
			
		||||
            <AlertDialogAction className="bg-destructive" onClick={onDelete}>
 | 
			
		||||
              Delete
 | 
			
		||||
            </AlertDialogAction>
 | 
			
		||||
          </AlertDialogFooter>
 | 
			
		||||
        </AlertDialogContent>
 | 
			
		||||
      </AlertDialog>
 | 
			
		||||
      <Drawer open={optionsOpen} onOpenChange={setOptionsOpen}>
 | 
			
		||||
        {content}
 | 
			
		||||
        <DrawerContent>
 | 
			
		||||
@ -217,12 +297,15 @@ export default function ReviewCard({
 | 
			
		||||
          )}
 | 
			
		||||
          <div
 | 
			
		||||
            className="flex w-full items-center justify-start gap-2 p-2"
 | 
			
		||||
          onClick={onDelete}
 | 
			
		||||
            onClick={handleDelete}
 | 
			
		||||
          >
 | 
			
		||||
            <HiTrash className="text-secondary-foreground" />
 | 
			
		||||
          <div className="text-primary">Delete</div>
 | 
			
		||||
            <div className="text-primary">
 | 
			
		||||
              {bypassDialogRef.current ? "Delete Now" : "Delete"}
 | 
			
		||||
            </div>
 | 
			
		||||
          </div>
 | 
			
		||||
        </DrawerContent>
 | 
			
		||||
      </Drawer>
 | 
			
		||||
    </>
 | 
			
		||||
  );
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@ -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,7 +45,47 @@ 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 (
 | 
			
		||||
    <>
 | 
			
		||||
      <AlertDialog
 | 
			
		||||
        open={deleteDialogOpen}
 | 
			
		||||
        onOpenChange={() => setDeleteDialogOpen(!deleteDialogOpen)}
 | 
			
		||||
      >
 | 
			
		||||
        <AlertDialogContent>
 | 
			
		||||
          <AlertDialogHeader>
 | 
			
		||||
            <AlertDialogTitle>Confirm Delete</AlertDialogTitle>
 | 
			
		||||
          </AlertDialogHeader>
 | 
			
		||||
          <AlertDialogDescription>
 | 
			
		||||
            Are you sure you want to delete all recorded video associated with
 | 
			
		||||
            the selected review items?
 | 
			
		||||
            <br />
 | 
			
		||||
            <br />
 | 
			
		||||
            Hold the <em>Shift</em> key to bypass this dialog in the future.
 | 
			
		||||
          </AlertDialogDescription>
 | 
			
		||||
          <AlertDialogFooter>
 | 
			
		||||
            <AlertDialogCancel>Cancel</AlertDialogCancel>
 | 
			
		||||
            <AlertDialogAction className="bg-destructive" onClick={onDelete}>
 | 
			
		||||
              Delete
 | 
			
		||||
            </AlertDialogAction>
 | 
			
		||||
          </AlertDialogFooter>
 | 
			
		||||
        </AlertDialogContent>
 | 
			
		||||
      </AlertDialog>
 | 
			
		||||
 | 
			
		||||
      <div className="absolute inset-x-2 inset-y-0 flex items-center justify-between gap-2 bg-background py-2 md:left-auto">
 | 
			
		||||
        <div className="mx-1 flex items-center justify-center text-sm text-muted-foreground">
 | 
			
		||||
          <div className="p-1">{`${selectedReviews.length} selected`}</div>
 | 
			
		||||
@ -71,12 +122,17 @@ export default function ReviewActionGroup({
 | 
			
		||||
          <Button
 | 
			
		||||
            className="flex items-center gap-2 p-2"
 | 
			
		||||
            size="sm"
 | 
			
		||||
          onClick={onDelete}
 | 
			
		||||
            onClick={handleDelete}
 | 
			
		||||
          >
 | 
			
		||||
            <HiTrash className="text-secondary-foreground" />
 | 
			
		||||
          {isDesktop && <div className="text-primary">Delete</div>}
 | 
			
		||||
            {isDesktop && (
 | 
			
		||||
              <div className="text-primary">
 | 
			
		||||
                {bypassDialog ? "Delete Now" : "Delete"}
 | 
			
		||||
              </div>
 | 
			
		||||
            )}
 | 
			
		||||
          </Button>
 | 
			
		||||
        </div>
 | 
			
		||||
      </div>
 | 
			
		||||
    </>
 | 
			
		||||
  );
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@ -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;
 | 
			
		||||
      }
 | 
			
		||||
 | 
			
		||||
@ -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;
 | 
			
		||||
      }
 | 
			
		||||
 | 
			
		||||
      if (keys.includes(e.key)) {
 | 
			
		||||
        e.preventDefault();
 | 
			
		||||
        listener(e.key, {
 | 
			
		||||
      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, 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],
 | 
			
		||||
 | 
			
		||||
@ -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,8 +248,7 @@ export default function LiveDashboardView({
 | 
			
		||||
    [setPreferredLiveModes],
 | 
			
		||||
  );
 | 
			
		||||
 | 
			
		||||
  const onKeyboardShortcut = useCallback(
 | 
			
		||||
    (key: string, modifiers: KeyModifiers) => {
 | 
			
		||||
  useKeyboardListener(["f"], (key, modifiers) => {
 | 
			
		||||
    if (!modifiers.down) {
 | 
			
		||||
      return;
 | 
			
		||||
    }
 | 
			
		||||
@ -261,11 +258,7 @@ export default function LiveDashboardView({
 | 
			
		||||
        toggleFullscreen();
 | 
			
		||||
        break;
 | 
			
		||||
    }
 | 
			
		||||
    },
 | 
			
		||||
    [toggleFullscreen],
 | 
			
		||||
  );
 | 
			
		||||
 | 
			
		||||
  useKeyboardListener(["f"], onKeyboardShortcut);
 | 
			
		||||
  });
 | 
			
		||||
 | 
			
		||||
  return (
 | 
			
		||||
    <div
 | 
			
		||||
 | 
			
		||||
		Loading…
	
		Reference in New Issue
	
	Block a user