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,
)
old = list(
Trigger.select(Trigger.camera, Trigger.name, Trigger.data)
.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,
)
# Check if trigger exists for upsert
trigger = Trigger.get_or_none(Trigger.camera == camera, Trigger.name == name)
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(
data=body.data,
model=request.app.frigate_config.semantic_search.model,
@ -1672,18 +1662,19 @@ def update_trigger_embedding(
threshold=body.threshold,
triggering_event_id="",
last_triggered=None,
)
.where(Trigger.camera == camera, Trigger.name == name)
.execute()
)
if updated == 0:
return JSONResponse(
content={
"success": False,
"message": f"Trigger {camera}:{name} not found",
},
status_code=404,
).where(Trigger.camera == camera, Trigger.name == name).execute()
else:
# Create new trigger (for rename case)
Trigger.create(
camera=camera,
name=name,
type=body.type,
data=body.data,
threshold=body.threshold,
model=request.app.frigate_config.semantic_search.model,
embedding=np.array(embedding, dtype=np.float32).tobytes(),
triggering_event_id="",
last_triggered=None,
)
if body.type == "thumbnail":

View File

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

View File

@ -81,7 +81,9 @@ class SemanticTriggerProcessor(PostProcessorApi):
for trigger in triggers:
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"]]
.enabled
):

View File

@ -681,7 +681,7 @@
},
"deleteTrigger": {
"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": {
"name": {

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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