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:
Josh Hawkins 2025-02-23 17:56:48 -06:00 committed by GitHub
parent 04a718dda8
commit 9414e001f3
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
2 changed files with 110 additions and 4 deletions

View File

@ -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>

View File

@ -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>