mirror of
https://github.com/blakeblackshear/frigate.git
synced 2025-07-26 13:47:03 +02:00
Edit sub labels from the UI (#16764)
* Add ability to edit sub labels from tracked object detail dialog * add allowEmpty prop * use TextEntryDialog * clean up * text consistency
This commit is contained in:
parent
04a718dda8
commit
9414e001f3
@ -71,6 +71,8 @@ import {
|
||||
} from "@/components/ui/popover";
|
||||
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";
|
||||
|
||||
const SEARCH_TABS = [
|
||||
"details",
|
||||
@ -288,6 +290,7 @@ function ObjectDetailsTab({
|
||||
// data
|
||||
|
||||
const [desc, setDesc] = useState(search?.data.description);
|
||||
const [isSubLabelDialogOpen, setIsSubLabelDialogOpen] = useState(false);
|
||||
|
||||
const handleDescriptionFocus = useCallback(() => {
|
||||
setInputFocused(true);
|
||||
@ -430,6 +433,74 @@ function ObjectDetailsTab({
|
||||
[search, config],
|
||||
);
|
||||
|
||||
const handleSubLabelSave = useCallback(
|
||||
(text: string) => {
|
||||
if (!search) return;
|
||||
|
||||
// set score to 1.0 if we're manually entering a sub label
|
||||
const subLabelScore =
|
||||
text === "" ? undefined : search.data?.sub_label_score || 1.0;
|
||||
|
||||
axios
|
||||
.post(`${apiHost}api/events/${search.id}/sub_label`, {
|
||||
camera: search.camera,
|
||||
subLabel: text,
|
||||
subLabelScore: subLabelScore,
|
||||
})
|
||||
.then((response) => {
|
||||
if (response.status === 200) {
|
||||
toast.success("Successfully updated sub label.", {
|
||||
position: "top-center",
|
||||
});
|
||||
|
||||
mutate(
|
||||
(key) =>
|
||||
typeof key === "string" &&
|
||||
(key.includes("events") ||
|
||||
key.includes("events/search") ||
|
||||
key.includes("events/explore")),
|
||||
(currentData: SearchResult[][] | SearchResult[] | undefined) => {
|
||||
if (!currentData) return currentData;
|
||||
return currentData.flat().map((event) =>
|
||||
event.id === search.id
|
||||
? {
|
||||
...event,
|
||||
sub_label: text,
|
||||
data: {
|
||||
...event.data,
|
||||
sub_label_score: subLabelScore,
|
||||
},
|
||||
}
|
||||
: event,
|
||||
);
|
||||
},
|
||||
{
|
||||
optimisticData: true,
|
||||
rollbackOnError: true,
|
||||
revalidate: false,
|
||||
},
|
||||
);
|
||||
|
||||
setSearch({
|
||||
...search,
|
||||
sub_label: text,
|
||||
data: {
|
||||
...search.data,
|
||||
sub_label_score: subLabelScore,
|
||||
},
|
||||
});
|
||||
setIsSubLabelDialogOpen(false);
|
||||
}
|
||||
})
|
||||
.catch(() => {
|
||||
toast.error("Failed to update sub label.", {
|
||||
position: "top-center",
|
||||
});
|
||||
});
|
||||
},
|
||||
[search, apiHost, mutate, setSearch],
|
||||
);
|
||||
|
||||
return (
|
||||
<div className="flex flex-col gap-5">
|
||||
<div className="flex w-full flex-row">
|
||||
@ -440,6 +511,21 @@ function ObjectDetailsTab({
|
||||
{getIconForLabel(search.label, "size-4 text-primary")}
|
||||
{search.label}
|
||||
{search.sub_label && ` (${search.sub_label})`}
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<span>
|
||||
<FaPencilAlt
|
||||
className="size-4 cursor-pointer text-primary/40 hover:text-primary/80"
|
||||
onClick={() => {
|
||||
setIsSubLabelDialogOpen(true);
|
||||
}}
|
||||
/>
|
||||
</span>
|
||||
</TooltipTrigger>
|
||||
<TooltipPortal>
|
||||
<TooltipContent>Edit sub label</TooltipContent>
|
||||
</TooltipPortal>
|
||||
</Tooltip>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex flex-col gap-1.5">
|
||||
@ -616,6 +702,15 @@ function ObjectDetailsTab({
|
||||
Save
|
||||
</Button>
|
||||
)}
|
||||
<TextEntryDialog
|
||||
open={isSubLabelDialogOpen}
|
||||
setOpen={setIsSubLabelDialogOpen}
|
||||
title="Edit Sub Label"
|
||||
description={`Enter a new sub label for this ${search.label ?? "tracked object"}.`}
|
||||
onSave={handleSubLabelSave}
|
||||
defaultValue={search?.sub_label || ""}
|
||||
allowEmpty={true}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -10,7 +10,7 @@ import {
|
||||
import { Form, FormControl, FormField, FormItem } from "@/components/ui/form";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import { zodResolver } from "@hookform/resolvers/zod";
|
||||
import { useCallback } from "react";
|
||||
import { useCallback, useEffect } from "react";
|
||||
import { useForm } from "react-hook-form";
|
||||
import { z } from "zod";
|
||||
|
||||
@ -20,13 +20,18 @@ type TextEntryDialogProps = {
|
||||
description?: string;
|
||||
setOpen: (open: boolean) => void;
|
||||
onSave: (text: string) => void;
|
||||
defaultValue?: string;
|
||||
allowEmpty?: boolean;
|
||||
};
|
||||
|
||||
export default function TextEntryDialog({
|
||||
open,
|
||||
title,
|
||||
description,
|
||||
setOpen,
|
||||
onSave,
|
||||
defaultValue = "",
|
||||
allowEmpty = false,
|
||||
}: TextEntryDialogProps) {
|
||||
const formSchema = z.object({
|
||||
text: z.string(),
|
||||
@ -34,6 +39,7 @@ export default function TextEntryDialog({
|
||||
|
||||
const form = useForm<z.infer<typeof formSchema>>({
|
||||
resolver: zodResolver(formSchema),
|
||||
defaultValues: { text: defaultValue },
|
||||
});
|
||||
const fileRef = form.register("text");
|
||||
|
||||
@ -41,15 +47,20 @@ export default function TextEntryDialog({
|
||||
|
||||
const onSubmit = useCallback(
|
||||
(data: z.infer<typeof formSchema>) => {
|
||||
if (!data["text"]) {
|
||||
if (!allowEmpty && !data["text"]) {
|
||||
return;
|
||||
}
|
||||
|
||||
onSave(data["text"]);
|
||||
},
|
||||
[onSave],
|
||||
[onSave, allowEmpty],
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
if (open) {
|
||||
form.reset({ text: defaultValue });
|
||||
}
|
||||
}, [open, defaultValue, form]);
|
||||
|
||||
return (
|
||||
<Dialog open={open} defaultOpen={false} onOpenChange={setOpen}>
|
||||
<DialogContent>
|
||||
|
Loading…
Reference in New Issue
Block a user