From c9d20fa1ee02e8a3ae6f1a9c9fc1336d50361dac Mon Sep 17 00:00:00 2001 From: Josh Hawkins <32435876+hawkeye217@users.noreply.github.com> Date: Wed, 5 Nov 2025 14:39:18 -0600 Subject: [PATCH] add actions to dots menu --- .../overlay/detail/SearchDetailDialog.tsx | 245 ++++++++---------- 1 file changed, 111 insertions(+), 134 deletions(-) diff --git a/web/src/components/overlay/detail/SearchDetailDialog.tsx b/web/src/components/overlay/detail/SearchDetailDialog.tsx index 29e824332..f195230a2 100644 --- a/web/src/components/overlay/detail/SearchDetailDialog.tsx +++ b/web/src/components/overlay/detail/SearchDetailDialog.tsx @@ -30,8 +30,6 @@ import { FaChevronDown, FaChevronLeft, FaChevronRight, - FaDownload, - FaHistory, } from "react-icons/fa"; import { TrackingDetails } from "./TrackingDetails"; import { DetailStreamProvider } from "@/context/detail-stream-context"; @@ -49,14 +47,16 @@ import { } from "@/components/ui/tooltip"; import { REVIEW_PADDING, ReviewSegment } from "@/types/review"; import { useNavigate } from "react-router-dom"; -import Chip from "@/components/indicators/Chip"; +// Chip removed from VideoTab - kept import commented out previously import { capitalizeAll } from "@/utils/stringUtil"; import useGlobalMutation from "@/hooks/use-global-mutate"; +import { HiDotsHorizontal } from "react-icons/hi"; import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuTrigger, + DropdownMenuPortal, } from "@/components/ui/dropdown-menu"; import { TransformComponent, TransformWrapper } from "react-zoom-pan-pinch"; import useImageLoaded from "@/hooks/use-image-loaded"; @@ -67,16 +67,14 @@ import { PopoverContent, PopoverTrigger, } from "@/components/ui/popover"; -import { LuInfo, LuSearch } from "react-icons/lu"; +import { LuInfo } from "react-icons/lu"; import { TooltipPortal } from "@radix-ui/react-tooltip"; import { FaPencilAlt } from "react-icons/fa"; import TextEntryDialog from "@/components/overlay/dialog/TextEntryDialog"; import { Trans, useTranslation } from "react-i18next"; -import { TbFaceId } from "react-icons/tb"; import { useIsAdmin } from "@/hooks/use-is-admin"; import FaceSelectionDialog from "../FaceSelectionDialog"; import { getTranslatedLabel } from "@/utils/i18n"; -import { CgTranscript } from "react-icons/cg"; import { CameraNameLabel } from "@/components/camera/CameraNameLabel"; import Heading from "@/components/ui/heading"; import { DialogPortal } from "@radix-ui/react-dialog"; @@ -346,7 +344,6 @@ export default function SearchDetailDialog({ )}
- {tabsComponent}
{page == "snapshot" && ( )}
@@ -456,6 +454,7 @@ type ObjectDetailsTabProps = { setSimilarity?: () => void; setInputFocused: React.Dispatch>; showThumbnail?: boolean; + tabs?: React.ReactNode; }; function ObjectDetailsTab({ search, @@ -464,6 +463,7 @@ function ObjectDetailsTab({ setSimilarity, setInputFocused, showThumbnail = true, + tabs, }: ObjectDetailsTabProps) { const { t, i18n } = useTranslation([ "views/explore", @@ -583,6 +583,12 @@ function ObjectDetailsTab({ } }, [search]); + const clipTimeRange = useMemo(() => { + const startTime = (search.start_time ?? 0) - REVIEW_PADDING; + const endTime = (search.end_time ?? Date.now() / 1000) + REVIEW_PADDING; + return `start/${startTime}/end/${endTime}`; + }, [search]); + const updateDescription = useCallback(() => { if (!search) { return; @@ -853,6 +859,11 @@ function ObjectDetailsTab({ [faceData], ); + const { data: reviewItem } = useSWR([ + `review/event/${search.id}`, + ]); + const navigate = useNavigate(); + const onTrainFace = useCallback( (trainName: string) => { axios @@ -948,9 +959,90 @@ function ObjectDetailsTab({ ); const popoverContainerRef = useRef(null); - return (
+ {tabs && ( +
+
{tabs}
+
+ + +
+ +
+
+ + + + +
+ {t("itemMenu.downloadSnapshot.label")} +
+
+
+ + + +
+ {t("itemMenu.downloadVideo.label")} +
+
+
+ {config?.semantic_search.enabled && + setSimilarity != undefined && + search.data.type == "object" && ( + { + setSearch(undefined); + setSimilarity(); + }} + > +
+ {t("itemMenu.findSimilar.label")} +
+
+ )} + {reviewItem && reviewItem.id && ( + { + navigate(`/review?id=${reviewItem.id}`); + }} + > +
+ {t("itemMenu.viewInHistory.label")} +
+
+ )} + + {hasFace && ( + + +
+ + {t("trainFace", { ns: "views/faceLibrary" })} + +
+
+
+ )} +
+
+
+
+
+ )} +
@@ -1214,54 +1306,6 @@ function ObjectDetailsTab({ draggable={false} src={`${apiHost}api/events/${search.id}/thumbnail.webp`} /> -
- {config?.semantic_search.enabled && - setSimilarity != undefined && - search.data.type == "object" && ( - - )} - {hasFace && ( - - - - )} - {config?.cameras[search?.camera].audio_transcription.enabled && - search?.label == "speech" && - search?.end_time && ( - - )} -
)}
@@ -1305,6 +1349,15 @@ function ObjectDetailsTab({ )}
+ {config?.cameras[search?.camera].audio_transcription.enabled && + search?.label == "speech" && + search?.end_time && ( + + )} {config?.cameras[search.camera].objects.genai.enabled && search.end_time && (
@@ -1356,6 +1409,7 @@ function ObjectDetailsTab({ {t("button.save", { ns: "common" })} )} + -
- - - - - - - - - - - {t("button.download", { ns: "common" })} - - - -
)} @@ -1470,12 +1499,6 @@ type VideoTabProps = { }; export function VideoTab({ search }: VideoTabProps) { - const { t } = useTranslation(["views/explore"]); - const navigate = useNavigate(); - const { data: reviewItem } = useSWR([ - `review/event/${search.id}`, - ]); - const clipTimeRange = useMemo(() => { const startTime = search.start_time - REVIEW_PADDING; const endTime = (search.end_time ?? Date.now() / 1000) + REVIEW_PADDING; @@ -1487,53 +1510,7 @@ export function VideoTab({ search }: VideoTabProps) { return ( <> - -
- {reviewItem && ( - - - { - if (reviewItem?.id) { - const params = new URLSearchParams({ - id: reviewItem.id, - }).toString(); - navigate(`/review?${params}`); - } - }} - > - - - - - - {t("itemMenu.viewInHistory.label")} - - - - )} - - - - - - - - - - - {t("button.download", { ns: "common" })} - - - -
-
+ ); }