diff --git a/web/public/locales/en/views/explore.json b/web/public/locales/en/views/explore.json index 8a61dcf58..d754fee77 100644 --- a/web/public/locales/en/views/explore.json +++ b/web/public/locales/en/views/explore.json @@ -175,6 +175,10 @@ "label": "Find similar", "aria": "Find similar tracked objects" }, + "addTrigger": { + "label": "Add trigger", + "aria": "Add a trigger for this tracked object" + }, "audioTranscription": { "label": "Transcribe", "aria": "Request audio transcription" diff --git a/web/public/locales/en/views/settings.json b/web/public/locales/en/views/settings.json index 9061aa74b..6713186df 100644 --- a/web/public/locales/en/views/settings.json +++ b/web/public/locales/en/views/settings.json @@ -673,15 +673,15 @@ "dialog": { "createTrigger": { "title": "Create Trigger", - "desc": "Create a new trigger for camera '{{camera}}'." + "desc": "Create a trigger for camera {{camera}}" }, "editTrigger": { "title": "Edit Trigger", - "desc": "Edit the settings for an existing trigger on camera '{{camera}}'." + "desc": "Edit the settings for trigger on camera {{camera}}" }, "deleteTrigger": { "title": "Delete Trigger", - "desc": "Are you sure you want to delete the trigger {{triggerName}} from camera '{{camera}}'? This action cannot be undone." + "desc": "Are you sure you want to delete the trigger {{triggerName}} from camera {{camera}}? This action cannot be undone." }, "form": { "name": { @@ -725,9 +725,9 @@ }, "toast": { "success": { - "createTrigger": "Trigger '{{name}}' created successfully.", - "updateTrigger": "Trigger '{{name}}' updated successfully.", - "deleteTrigger": "Trigger '{{name}}' deleted successfully." + "createTrigger": "Trigger {{name}} created successfully.", + "updateTrigger": "Trigger {{name}} updated successfully.", + "deleteTrigger": "Trigger {{name}} deleted successfully." }, "error": { "createTriggerFailed": "Failed to create trigger: {{errorMessage}}", diff --git a/web/src/components/card/SearchThumbnailFooter.tsx b/web/src/components/card/SearchThumbnailFooter.tsx index c86e9c3c6..e23d1c3f6 100644 --- a/web/src/components/card/SearchThumbnailFooter.tsx +++ b/web/src/components/card/SearchThumbnailFooter.tsx @@ -15,6 +15,7 @@ type SearchThumbnailProps = { refreshResults: () => void; showObjectLifecycle: () => void; showSnapshot: () => void; + addTrigger: () => void; }; export default function SearchThumbnailFooter({ @@ -24,6 +25,7 @@ export default function SearchThumbnailFooter({ refreshResults, showObjectLifecycle, showSnapshot, + addTrigger, }: SearchThumbnailProps) { const { t } = useTranslation(["views/search"]); const { data: config } = useSWR("config"); @@ -61,6 +63,7 @@ export default function SearchThumbnailFooter({ refreshResults={refreshResults} showObjectLifecycle={showObjectLifecycle} showSnapshot={showSnapshot} + addTrigger={addTrigger} /> diff --git a/web/src/components/menu/SearchResultActions.tsx b/web/src/components/menu/SearchResultActions.tsx index 1779430f0..2c928becf 100644 --- a/web/src/components/menu/SearchResultActions.tsx +++ b/web/src/components/menu/SearchResultActions.tsx @@ -41,6 +41,7 @@ import { import useSWR from "swr"; import { Trans, useTranslation } from "react-i18next"; +import { BsFillLightningFill } from "react-icons/bs"; type SearchResultActionsProps = { searchResult: SearchResult; @@ -48,6 +49,7 @@ type SearchResultActionsProps = { refreshResults: () => void; showObjectLifecycle: () => void; showSnapshot: () => void; + addTrigger: () => void; isContextMenu?: boolean; children?: ReactNode; }; @@ -58,6 +60,7 @@ export default function SearchResultActions({ refreshResults, showObjectLifecycle, showSnapshot, + addTrigger, isContextMenu = false, children, }: SearchResultActionsProps) { @@ -138,6 +141,16 @@ export default function SearchResultActions({ {t("itemMenu.findSimilar.label")} )} + {config?.semantic_search?.enabled && + searchResult.data.type == "object" && ( + + + {t("itemMenu.addTrigger.label")} + + )} {isMobileOnly && config?.plus?.enabled && searchResult.has_snapshot && diff --git a/web/src/components/overlay/CreateTriggerDialog.tsx b/web/src/components/overlay/CreateTriggerDialog.tsx index ec8572281..b74da75f6 100644 --- a/web/src/components/overlay/CreateTriggerDialog.tsx +++ b/web/src/components/overlay/CreateTriggerDialog.tsx @@ -95,9 +95,7 @@ export default function CreateTriggerDialog({ .number() .min(0, t("triggers.dialog.form.threshold.error.min")) .max(1, t("triggers.dialog.form.threshold.error.max")), - actions: z - .array(z.enum(["alert", "notification"])) - .min(1, t("triggers.dialog.form.actions.error.min")), + actions: z.array(z.enum(["alert", "notification"])), }); const form = useForm>({ @@ -109,7 +107,7 @@ export default function CreateTriggerDialog({ type: trigger?.type ?? "description", data: trigger?.data ?? "", threshold: trigger?.threshold ?? 0.5, - actions: trigger?.actions ?? ["alert"], + actions: trigger?.actions ?? [], }, }); @@ -138,17 +136,22 @@ export default function CreateTriggerDialog({ type: "description", data: "", threshold: 0.5, - actions: ["alert"], + actions: [], }); } else if (trigger) { - form.reset({ - enabled: trigger.enabled, - name: trigger.name, - type: trigger.type, - data: trigger.data, - threshold: trigger.threshold, - actions: trigger.actions, - }); + form.reset( + { + enabled: trigger.enabled, + name: trigger.name, + type: trigger.type, + data: trigger.data, + threshold: trigger.threshold, + actions: trigger.actions, + }, + { keepDirty: false, keepTouched: false }, // Reset validation state + ); + // Trigger validation to ensure isValid updates + // form.trigger(); } }, [show, trigger, form]); diff --git a/web/src/pages/Settings.tsx b/web/src/pages/Settings.tsx index 0f3eb52fd..f29cec400 100644 --- a/web/src/pages/Settings.tsx +++ b/web/src/pages/Settings.tsx @@ -173,7 +173,7 @@ export default function Settings() { } } // don't clear url params if we're creating a new object mask - return !searchParams.has("object_mask"); + return !(searchParams.has("object_mask") || searchParams.has("event_id")); }); useSearchEffect("camera", (camera: string) => { @@ -181,8 +181,8 @@ export default function Settings() { if (cameraNames.includes(camera)) { setSelectedCamera(camera); } - // don't clear url params if we're creating a new object mask - return !searchParams.has("object_mask"); + // don't clear url params if we're creating a new object mask or trigger + return !(searchParams.has("object_mask") || searchParams.has("event_id")); }); useEffect(() => { diff --git a/web/src/views/explore/ExploreView.tsx b/web/src/views/explore/ExploreView.tsx index f5cdb220d..06bb800c7 100644 --- a/web/src/views/explore/ExploreView.tsx +++ b/web/src/views/explore/ExploreView.tsx @@ -218,6 +218,7 @@ function ExploreThumbnailImage({ const apiHost = useApiHost(); const { data: config } = useSWR("config"); const [imgRef, imgLoaded, onImgLoad] = useImageLoaded(); + const navigate = useNavigate(); const handleFindSimilar = () => { if (config?.semantic_search.enabled) { @@ -233,6 +234,12 @@ function ExploreThumbnailImage({ onSelectSearch(event, false, "snapshot"); }; + const handleAddTrigger = () => { + navigate( + `/settings?page=triggers&camera=${event.camera}&event_id=${event.id}`, + ); + }; + return (
diff --git a/web/src/views/search/SearchView.tsx b/web/src/views/search/SearchView.tsx index d72d3dbf4..6ca2a0f6d 100644 --- a/web/src/views/search/SearchView.tsx +++ b/web/src/views/search/SearchView.tsx @@ -32,6 +32,7 @@ import Chip from "@/components/indicators/Chip"; import { TooltipPortal } from "@radix-ui/react-tooltip"; import SearchActionGroup from "@/components/filter/SearchActionGroup"; import { Trans, useTranslation } from "react-i18next"; +import { useNavigate } from "react-router-dom"; type SearchViewProps = { search: string; @@ -76,6 +77,7 @@ export default function SearchView({ const { data: config } = useSWR("config", { revalidateOnFocus: false, }); + const navigate = useNavigate(); // grid @@ -648,6 +650,16 @@ export default function SearchView({ showSnapshot={() => onSelectSearch(value, false, "snapshot") } + addTrigger={() => { + if ( + config?.semantic_search.enabled && + value.data.type == "object" + ) { + navigate( + `/settings?page=triggers&camera=${value.camera}&event_id=${value.id}`, + ); + } + }} />
diff --git a/web/src/views/settings/TriggerView.tsx b/web/src/views/settings/TriggerView.tsx index 6feca924d..ba13fc6fe 100644 --- a/web/src/views/settings/TriggerView.tsx +++ b/web/src/views/settings/TriggerView.tsx @@ -26,6 +26,7 @@ import CreateTriggerDialog from "@/components/overlay/CreateTriggerDialog"; import DeleteTriggerDialog from "@/components/overlay/DeleteTriggerDialog"; import { FrigateConfig } from "@/types/frigateConfig"; import { Trigger, TriggerAction, TriggerType } from "@/types/trigger"; +import { useSearchEffect } from "@/hooks/use-overlay-state"; type ConfigSetBody = { requires_restart: number; @@ -296,6 +297,24 @@ export default function TriggerView({ } }, [selectedCamera, setUnsavedChanges]); + // for adding a trigger with event id via explore context menu + + useSearchEffect("event_id", (eventId: string) => { + if (!config || isLoading) { + return false; + } + setShowCreate(true); + setSelectedTrigger({ + enabled: true, + name: "", + type: "thumbnail", + data: eventId, + threshold: 0.5, + actions: [], + }); + return true; + }); + if (!config || !selectedCamera) { return (