forked from Github/frigate
Explore bulk actions (#15307)
* use id instead of index for object details and scrolling * long press package and hook * fix long press in review * search action group * multi select in explore * add bulk deletion to backend api * clean up * mimic behavior of review * don't open dialog on left click when mutli selecting * context menu on container ref * revert long press code * clean up
This commit is contained in:
@@ -1,4 +1,4 @@
|
||||
import { useCallback, useMemo } from "react";
|
||||
import { useMemo } from "react";
|
||||
import { useApiHost } from "@/api";
|
||||
import { getIconForLabel } from "@/utils/iconUtil";
|
||||
import useSWR from "swr";
|
||||
@@ -12,10 +12,11 @@ import { capitalizeFirstLetter } from "@/utils/stringUtil";
|
||||
import { SearchResult } from "@/types/search";
|
||||
import { cn } from "@/lib/utils";
|
||||
import { TooltipPortal } from "@radix-ui/react-tooltip";
|
||||
import useContextMenu from "@/hooks/use-contextmenu";
|
||||
|
||||
type SearchThumbnailProps = {
|
||||
searchResult: SearchResult;
|
||||
onClick: (searchResult: SearchResult) => void;
|
||||
onClick: (searchResult: SearchResult, ctrl: boolean, detail: boolean) => void;
|
||||
};
|
||||
|
||||
export default function SearchThumbnail({
|
||||
@@ -28,9 +29,9 @@ export default function SearchThumbnail({
|
||||
|
||||
// interactions
|
||||
|
||||
const handleOnClick = useCallback(() => {
|
||||
onClick(searchResult);
|
||||
}, [searchResult, onClick]);
|
||||
useContextMenu(imgRef, () => {
|
||||
onClick(searchResult, true, false);
|
||||
});
|
||||
|
||||
const objectLabel = useMemo(() => {
|
||||
if (
|
||||
@@ -45,7 +46,10 @@ export default function SearchThumbnail({
|
||||
}, [config, searchResult]);
|
||||
|
||||
return (
|
||||
<div className="relative size-full cursor-pointer" onClick={handleOnClick}>
|
||||
<div
|
||||
className="relative size-full cursor-pointer"
|
||||
onClick={() => onClick(searchResult, false, true)}
|
||||
>
|
||||
<ImageLoadingIndicator
|
||||
className="absolute inset-0"
|
||||
imgLoaded={imgLoaded}
|
||||
@@ -79,7 +83,7 @@ export default function SearchThumbnail({
|
||||
<div className="mx-3 pb-1 text-sm text-white">
|
||||
<Chip
|
||||
className={`z-0 flex items-center justify-between gap-1 space-x-1 bg-gray-500 bg-gradient-to-br from-gray-400 to-gray-500 text-xs`}
|
||||
onClick={() => onClick(searchResult)}
|
||||
onClick={() => onClick(searchResult, false, true)}
|
||||
>
|
||||
{getIconForLabel(objectLabel, "size-3 text-white")}
|
||||
{Math.round(
|
||||
|
||||
132
web/src/components/filter/SearchActionGroup.tsx
Normal file
132
web/src/components/filter/SearchActionGroup.tsx
Normal file
@@ -0,0 +1,132 @@
|
||||
import { useCallback, useState } from "react";
|
||||
import axios from "axios";
|
||||
import { Button, buttonVariants } from "../ui/button";
|
||||
import { isDesktop } from "react-device-detect";
|
||||
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";
|
||||
import { toast } from "sonner";
|
||||
|
||||
type SearchActionGroupProps = {
|
||||
selectedObjects: string[];
|
||||
setSelectedObjects: (ids: string[]) => void;
|
||||
pullLatestData: () => void;
|
||||
};
|
||||
export default function SearchActionGroup({
|
||||
selectedObjects,
|
||||
setSelectedObjects,
|
||||
pullLatestData,
|
||||
}: SearchActionGroupProps) {
|
||||
const onClearSelected = useCallback(() => {
|
||||
setSelectedObjects([]);
|
||||
}, [setSelectedObjects]);
|
||||
|
||||
const onDelete = useCallback(async () => {
|
||||
await axios
|
||||
.delete(`events/`, {
|
||||
data: { event_ids: selectedObjects },
|
||||
})
|
||||
.then((resp) => {
|
||||
if (resp.status == 200) {
|
||||
toast.success("Tracked objects deleted successfully.", {
|
||||
position: "top-center",
|
||||
});
|
||||
setSelectedObjects([]);
|
||||
pullLatestData();
|
||||
}
|
||||
})
|
||||
.catch(() => {
|
||||
toast.error("Failed to delete tracked objects.", {
|
||||
position: "top-center",
|
||||
});
|
||||
});
|
||||
}, [selectedObjects, setSelectedObjects, 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>
|
||||
Deleting these {selectedObjects.length} tracked objects removes the
|
||||
snapshot, any saved embeddings, and any associated object lifecycle
|
||||
entries. Recorded footage of these tracked objects in History view
|
||||
will <em>NOT</em> be deleted.
|
||||
<br />
|
||||
<br />
|
||||
Are you sure you want to proceed?
|
||||
<br />
|
||||
<br />
|
||||
Hold the <em>Shift</em> key to bypass this dialog in the future.
|
||||
</AlertDialogDescription>
|
||||
<AlertDialogFooter>
|
||||
<AlertDialogCancel>Cancel</AlertDialogCancel>
|
||||
<AlertDialogAction
|
||||
className={buttonVariants({ variant: "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">{`${selectedObjects.length} selected`}</div>
|
||||
<div className="p-1">{"|"}</div>
|
||||
<div
|
||||
className="cursor-pointer p-2 text-primary hover:rounded-lg hover:bg-secondary"
|
||||
onClick={onClearSelected}
|
||||
>
|
||||
Unselect
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex items-center gap-1 md:gap-2">
|
||||
<Button
|
||||
className="flex items-center gap-2 p-2"
|
||||
aria-label="Delete"
|
||||
size="sm"
|
||||
onClick={handleDelete}
|
||||
>
|
||||
<HiTrash className="text-secondary-foreground" />
|
||||
{isDesktop && (
|
||||
<div className="text-primary">
|
||||
{bypassDialog ? "Delete Now" : "Delete"}
|
||||
</div>
|
||||
)}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user