renaming and deletion fixes

This commit is contained in:
Josh Hawkins 2025-07-02 18:53:10 -05:00
parent efe5864ebc
commit c9e320c313
10 changed files with 76 additions and 45 deletions

View File

@ -1647,24 +1647,14 @@ def update_trigger_embedding(
status_code=400, status_code=400,
) )
old = list( # Check if trigger exists for upsert
Trigger.select(Trigger.camera, Trigger.name, Trigger.data) trigger = Trigger.get_or_none(Trigger.camera == camera, Trigger.name == name)
.where(Trigger.camera == camera, Trigger.name == name)
.dicts()
.iterator()
)
if not old:
return JSONResponse(
content={
"success": False,
"message": f"Trigger {camera}:{name} not found",
},
status_code=404,
)
context.delete_trigger_thumbnail(camera, old[0]["data"]) if trigger:
# Update existing trigger
if trigger.data != body.data: # Delete old thumbnail only if data changes
context.delete_trigger_thumbnail(camera, trigger.data)
updated = (
Trigger.update( Trigger.update(
data=body.data, data=body.data,
model=request.app.frigate_config.semantic_search.model, model=request.app.frigate_config.semantic_search.model,
@ -1672,18 +1662,19 @@ def update_trigger_embedding(
threshold=body.threshold, threshold=body.threshold,
triggering_event_id="", triggering_event_id="",
last_triggered=None, last_triggered=None,
) ).where(Trigger.camera == camera, Trigger.name == name).execute()
.where(Trigger.camera == camera, Trigger.name == name) else:
.execute() # Create new trigger (for rename case)
) Trigger.create(
camera=camera,
if updated == 0: name=name,
return JSONResponse( type=body.type,
content={ data=body.data,
"success": False, threshold=body.threshold,
"message": f"Trigger {camera}:{name} not found", model=request.app.frigate_config.semantic_search.model,
}, embedding=np.array(embedding, dtype=np.float32).tobytes(),
status_code=404, triggering_event_id="",
last_triggered=None,
) )
if body.type == "thumbnail": if body.type == "thumbnail":

View File

@ -196,6 +196,7 @@ class WebPushClient(Communicator): # type: ignore[misc]
# notification action enabled # notification action enabled
if ( if (
not self.config.cameras[camera].notifications.enabled not self.config.cameras[camera].notifications.enabled
or name not in self.config.cameras[camera].semantic_search.triggers
or "notification" or "notification"
not in self.config.cameras[camera] not in self.config.cameras[camera]
.semantic_search.triggers[name] .semantic_search.triggers[name]

View File

@ -81,7 +81,9 @@ class SemanticTriggerProcessor(PostProcessorApi):
for trigger in triggers: for trigger in triggers:
if ( if (
not self.config.cameras[camera] trigger["name"]
not in self.config.cameras[camera].semantic_search.triggers
or not self.config.cameras[camera]
.semantic_search.triggers[trigger["name"]] .semantic_search.triggers[trigger["name"]]
.enabled .enabled
): ):

View File

@ -681,7 +681,7 @@
}, },
"deleteTrigger": { "deleteTrigger": {
"title": "Delete Trigger", "title": "Delete Trigger",
"desc": "Are you sure you want to delete the trigger <strong>{{triggerName}}</strong> from camera {{camera}}? This action cannot be undone." "desc": "Are you sure you want to delete the trigger <strong>{{triggerName}}</strong>? This action cannot be undone."
}, },
"form": { "form": {
"name": { "name": {

View File

@ -1,4 +1,4 @@
import { useEffect, useState, useMemo } from "react"; import { useEffect, useMemo } from "react";
import { useTranslation } from "react-i18next"; import { useTranslation } from "react-i18next";
import { useForm } from "react-hook-form"; import { useForm } from "react-hook-form";
import { zodResolver } from "@hookform/resolvers/zod"; import { zodResolver } from "@hookform/resolvers/zod";
@ -40,6 +40,7 @@ type CreateTriggerDialogProps = {
show: boolean; show: boolean;
trigger: Trigger | null; trigger: Trigger | null;
selectedCamera: string; selectedCamera: string;
isLoading: boolean;
onCreate: ( onCreate: (
enabled: boolean, enabled: boolean,
name: string, name: string,
@ -56,12 +57,12 @@ export default function CreateTriggerDialog({
show, show,
trigger, trigger,
selectedCamera, selectedCamera,
isLoading,
onCreate, onCreate,
onEdit, onEdit,
onCancel, onCancel,
}: CreateTriggerDialogProps) { }: CreateTriggerDialogProps) {
const { t } = useTranslation("views/settings"); const { t } = useTranslation("views/settings");
const [isLoading, setIsLoading] = useState(false);
const { data: config } = useSWR<FrigateConfig>("config"); const { data: config } = useSWR<FrigateConfig>("config");
const existingTriggerNames = useMemo(() => { const existingTriggerNames = useMemo(() => {
@ -112,7 +113,6 @@ export default function CreateTriggerDialog({
}); });
const onSubmit = async (values: z.infer<typeof formSchema>) => { const onSubmit = async (values: z.infer<typeof formSchema>) => {
setIsLoading(true);
if (trigger) { if (trigger) {
onEdit({ ...values }); onEdit({ ...values });
} else { } else {
@ -125,7 +125,6 @@ export default function CreateTriggerDialog({
values.actions, values.actions,
); );
} }
setIsLoading(false);
}; };
useEffect(() => { useEffect(() => {

View File

@ -9,10 +9,12 @@ import {
} from "@/components/ui/dialog"; } from "@/components/ui/dialog";
import { Button } from "@/components/ui/button"; import { Button } from "@/components/ui/button";
import { Trans } from "react-i18next"; import { Trans } from "react-i18next";
import ActivityIndicator from "@/components/indicators/activity-indicator";
type DeleteTriggerDialogProps = { type DeleteTriggerDialogProps = {
show: boolean; show: boolean;
triggerName: string; triggerName: string;
isLoading: boolean;
onCancel: () => void; onCancel: () => void;
onDelete: () => void; onDelete: () => void;
}; };
@ -20,6 +22,7 @@ type DeleteTriggerDialogProps = {
export default function DeleteTriggerDialog({ export default function DeleteTriggerDialog({
show, show,
triggerName, triggerName,
isLoading,
onCancel, onCancel,
onDelete, onDelete,
}: DeleteTriggerDialogProps) { }: DeleteTriggerDialogProps) {
@ -48,16 +51,25 @@ export default function DeleteTriggerDialog({
aria-label={t("button.cancel", { ns: "common" })} aria-label={t("button.cancel", { ns: "common" })}
onClick={onCancel} onClick={onCancel}
type="button" type="button"
disabled={isLoading}
> >
{t("button.cancel", { ns: "common" })} {t("button.cancel", { ns: "common" })}
</Button> </Button>
<Button <Button
variant="destructive" variant="destructive"
aria-label={t("button.delete", { ns: "common" })} aria-label={t("button.delete", { ns: "common" })}
className="flex flex-1" className="flex flex-1 text-white"
onClick={onDelete} onClick={onDelete}
disabled={isLoading}
> >
{t("button.delete", { ns: "common" })} {isLoading ? (
<div className="flex flex-row items-center gap-2">
<ActivityIndicator />
<span>{t("button.delete", { ns: "common" })}</span>
</div>
) : (
t("button.delete", { ns: "common" })
)}
</Button> </Button>
</div> </div>
</div> </div>

View File

@ -60,7 +60,7 @@ export default function DeleteUserDialog({
<Button <Button
variant="destructive" variant="destructive"
aria-label={t("button.delete", { ns: "common" })} aria-label={t("button.delete", { ns: "common" })}
className="flex flex-1" className="flex flex-1 text-white"
onClick={onDelete} onClick={onDelete}
> >
{t("button.delete", { ns: "common" })} {t("button.delete", { ns: "common" })}

View File

@ -476,6 +476,7 @@ function LibrarySelector({
</Button> </Button>
<Button <Button
variant="destructive" variant="destructive"
className="text-white"
onClick={() => { onClick={() => {
if (confirmDelete) { if (confirmDelete) {
handleDeleteFace(confirmDelete); handleDeleteFace(confirmDelete);

View File

@ -431,6 +431,7 @@ function LibrarySelector({
</Button> </Button>
<Button <Button
variant="destructive" variant="destructive"
className="text-white"
onClick={() => { onClick={() => {
if (confirmDelete) { if (confirmDelete) {
handleDeleteFace(confirmDelete); handleDeleteFace(confirmDelete);

View File

@ -169,6 +169,7 @@ export default function TriggerView({
}) })
.finally(() => { .finally(() => {
setIsLoading(false); setIsLoading(false);
setShowCreate(false);
}); });
}, },
[t, updateConfig, selectedCamera, setUnsavedChanges], [t, updateConfig, selectedCamera, setUnsavedChanges],
@ -185,7 +186,6 @@ export default function TriggerView({
) => { ) => {
setUnsavedChanges(true); setUnsavedChanges(true);
saveToConfig({ enabled, name, type, data, threshold, actions }, false); saveToConfig({ enabled, name, type, data, threshold, actions }, false);
setShowCreate(false);
}, },
[saveToConfig, setUnsavedChanges], [saveToConfig, setUnsavedChanges],
); );
@ -193,18 +193,40 @@ export default function TriggerView({
const onEdit = useCallback( const onEdit = useCallback(
(trigger: Trigger) => { (trigger: Trigger) => {
setUnsavedChanges(true); setUnsavedChanges(true);
setIsLoading(true);
if (selectedTrigger?.name && selectedTrigger.name !== trigger.name) { if (selectedTrigger?.name && selectedTrigger.name !== trigger.name) {
// Handle rename by deleting old trigger // Handle rename: delete old trigger, update config, then save new trigger
axios axios
.delete( .delete(
`/trigger/embedding/${selectedCamera}/${selectedTrigger.name}`, `/trigger/embedding/${selectedCamera}/${selectedTrigger.name}`,
) )
.then((embeddingResponse) => { .then((embeddingResponse) => {
if (embeddingResponse.data.success) { if (!embeddingResponse.data.success) {
return saveToConfig(trigger, true);
} else {
throw new Error(embeddingResponse.data.message); throw new Error(embeddingResponse.data.message);
} }
const deleteConfigBody: ConfigSetBody = {
requires_restart: 0,
config_data: {
cameras: {
[selectedCamera]: {
semantic_search: {
triggers: {
[selectedTrigger.name]: "",
},
},
},
},
},
update_topic: `config/cameras/${selectedCamera}/semantic_search`,
};
return axios.put("config/set", deleteConfigBody);
})
.then((configResponse) => {
if (configResponse.status !== 200) {
throw new Error(configResponse.statusText);
}
// Save new trigger
saveToConfig(trigger, false);
}) })
.catch((error) => { .catch((error) => {
const errorMessage = const errorMessage =
@ -218,9 +240,9 @@ export default function TriggerView({
setIsLoading(false); setIsLoading(false);
}); });
} else { } else {
// Regular update without rename
saveToConfig(trigger, true); saveToConfig(trigger, true);
} }
setShowCreate(false);
setSelectedTrigger(null); setSelectedTrigger(null);
}, },
[t, saveToConfig, selectedCamera, selectedTrigger, setUnsavedChanges], [t, saveToConfig, selectedCamera, selectedTrigger, setUnsavedChanges],
@ -254,7 +276,6 @@ export default function TriggerView({
.put("config/set", configBody) .put("config/set", configBody)
.then((configResponse) => { .then((configResponse) => {
if (configResponse.status === 200) { if (configResponse.status === 200) {
setShowDelete(false);
updateConfig(); updateConfig();
toast.success( toast.success(
t("triggers.toast.success.deleteTrigger", { name }), t("triggers.toast.success.deleteTrigger", { name }),
@ -282,6 +303,7 @@ export default function TriggerView({
); );
}) })
.finally(() => { .finally(() => {
setShowDelete(false);
setIsLoading(false); setIsLoading(false);
}); });
}, },
@ -439,7 +461,7 @@ export default function TriggerView({
<Button <Button
size="sm" size="sm"
variant="destructive" variant="destructive"
className="h-8 px-2" className="h-8 px-2 text-white"
onClick={() => { onClick={() => {
setSelectedTrigger(trigger); setSelectedTrigger(trigger);
setShowDelete(true); setShowDelete(true);
@ -472,6 +494,7 @@ export default function TriggerView({
show={showCreate} show={showCreate}
trigger={selectedTrigger} trigger={selectedTrigger}
selectedCamera={selectedCamera} selectedCamera={selectedCamera}
isLoading={isLoading}
onCreate={onCreate} onCreate={onCreate}
onEdit={onEdit} onEdit={onEdit}
onCancel={() => { onCancel={() => {
@ -483,6 +506,7 @@ export default function TriggerView({
<DeleteTriggerDialog <DeleteTriggerDialog
show={showDelete} show={showDelete}
triggerName={selectedTrigger?.name ?? ""} triggerName={selectedTrigger?.name ?? ""}
isLoading={isLoading}
onCancel={() => { onCancel={() => {
setShowDelete(false); setShowDelete(false);
setSelectedTrigger(null); setSelectedTrigger(null);