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" })}
-
-
-
-
-
+
>
);
}