Add zones friend name (#20761)

* feat: add zones friendly name

* fix: fix the issue where the input field was empty when there was no friendly_name

* chore: fix the issue where the friendly name would replace spaces with underscores

* docs: update zones docs

* Update web/src/components/settings/ZoneEditPane.tsx

Co-authored-by: Josh Hawkins <32435876+hawkeye217@users.noreply.github.com>

* Add friendly_name option for zone configuration

Added optional friendly name for zones in configuration.

* fix: fix the logical error in the null/empty check for the polygons parameter

* fix: remove the toast name for zones will use the friendly_name instead

* docs: remove emoji tips

* revert: revert zones doc ui tips

* Update docs/docs/configuration/zones.md

Co-authored-by: Josh Hawkins <32435876+hawkeye217@users.noreply.github.com>

* Update docs/docs/configuration/zones.md

Co-authored-by: Josh Hawkins <32435876+hawkeye217@users.noreply.github.com>

* Update docs/docs/configuration/zones.md

Co-authored-by: Josh Hawkins <32435876+hawkeye217@users.noreply.github.com>

* feat: add friendly zone names to tracking details and lifecycle item descriptions

* chore: lint fix

* refactor: add friendly zone names to timeline entries and clean up unused code

* refactor: add formatList

---------

Co-authored-by: Josh Hawkins <32435876+hawkeye217@users.noreply.github.com>
This commit is contained in:
GuoQing Liu
2025-11-07 22:02:06 +08:00
committed by GitHub
parent 530b69b877
commit ef19332fe5
37 changed files with 314 additions and 126 deletions

View File

@@ -262,13 +262,17 @@ export function PolygonCanvas({
};
useEffect(() => {
if (activePolygonIndex === undefined || !polygons) {
if (activePolygonIndex === undefined || !polygons?.length) {
return;
}
const updatedPolygons = [...polygons];
const activePolygon = updatedPolygons[activePolygonIndex];
if (!activePolygon) {
return;
}
// add default points order for already completed polygons
if (!activePolygon.pointsOrder && activePolygon.isFinished) {
updatedPolygons[activePolygonIndex] = {

View File

@@ -179,7 +179,7 @@ export default function PolygonItem({
if (res.status === 200) {
toast.success(
t("masksAndZones.form.polygonDrawing.delete.success", {
name: polygon?.name,
name: polygon?.friendly_name ?? polygon?.name,
}),
{
position: "top-center",
@@ -261,7 +261,9 @@ export default function PolygonItem({
}}
/>
)}
<p className="cursor-default">{polygon.name}</p>
<p className="cursor-default">
{polygon.friendly_name ?? polygon.name}
</p>
</div>
<AlertDialog
open={deleteDialogOpen}
@@ -278,7 +280,7 @@ export default function PolygonItem({
ns="views/settings"
values={{
type: polygon.type.replace("_", " "),
name: polygon.name,
name: polygon.friendly_name ?? polygon.name,
}}
>
masksAndZones.form.polygonDrawing.delete.desc

View File

@@ -34,6 +34,7 @@ import { Link } from "react-router-dom";
import { LuExternalLink } from "react-icons/lu";
import { useDocDomain } from "@/hooks/use-doc-domain";
import { getTranslatedLabel } from "@/utils/i18n";
import NameAndIdFields from "../input/NameAndIdFields";
type ZoneEditPaneProps = {
polygons?: Polygon[];
@@ -146,15 +147,37 @@ export default function ZoneEditPane({
"masksAndZones.form.zoneName.error.mustNotContainPeriod",
),
},
)
.refine((value: string) => /^[a-zA-Z0-9_-]+$/.test(value), {
message: t("masksAndZones.form.zoneName.error.hasIllegalCharacter"),
})
.refine((value: string) => /[a-zA-Z]/.test(value), {
),
friendly_name: z
.string()
.min(2, {
message: t(
"masksAndZones.form.zoneName.error.mustHaveAtLeastOneLetter",
"masksAndZones.form.zoneName.error.mustBeAtLeastTwoCharacters",
),
}),
})
.refine(
(value: string) => {
return !cameras.map((cam) => cam.name).includes(value);
},
{
message: t(
"masksAndZones.form.zoneName.error.mustNotBeSameWithCamera",
),
},
)
.refine(
(value: string) => {
const otherPolygonNames =
polygons
?.filter((_, index) => index !== activePolygonIndex)
.map((polygon) => polygon.name) || [];
return !otherPolygonNames.includes(value);
},
{
message: t("masksAndZones.form.zoneName.error.alreadyExists"),
},
),
inertia: z.coerce
.number()
.min(1, {
@@ -247,6 +270,7 @@ export default function ZoneEditPane({
mode: "onBlur",
defaultValues: {
name: polygon?.name ?? "",
friendly_name: polygon?.friendly_name ?? polygon?.name ?? "",
inertia:
polygon?.camera &&
polygon?.name &&
@@ -286,6 +310,7 @@ export default function ZoneEditPane({
async (
{
name: zoneName,
friendly_name,
inertia,
loitering_time,
objects: form_objects,
@@ -415,9 +440,14 @@ export default function ZoneEditPane({
}
}
let friendlyNameQuery = "";
if (friendly_name) {
friendlyNameQuery = `&cameras.${polygon?.camera}.zones.${zoneName}.friendly_name=${encodeURIComponent(friendly_name)}`;
}
axios
.put(
`config/set?cameras.${polygon?.camera}.zones.${zoneName}.coordinates=${coordinates}${inertiaQuery}${loiteringTimeQuery}${speedThresholdQuery}${distancesQuery}${objectQueries}${alertQueries}${detectionQueries}`,
`config/set?cameras.${polygon?.camera}.zones.${zoneName}.coordinates=${coordinates}${inertiaQuery}${loiteringTimeQuery}${speedThresholdQuery}${distancesQuery}${objectQueries}${friendlyNameQuery}${alertQueries}${detectionQueries}`,
{
requires_restart: 0,
update_topic: `config/cameras/${polygon.camera}/zones`,
@@ -427,7 +457,7 @@ export default function ZoneEditPane({
if (res.status === 200) {
toast.success(
t("masksAndZones.zones.toast.success", {
zoneName,
zoneName: friendly_name || zoneName,
}),
{
position: "top-center",
@@ -541,26 +571,16 @@ export default function ZoneEditPane({
<Form {...form}>
<form onSubmit={form.handleSubmit(onSubmit)} className="mt-2 space-y-6">
<FormField
<NameAndIdFields
type="zone"
control={form.control}
name="name"
render={({ field }) => (
<FormItem>
<FormLabel>{t("masksAndZones.zones.name.title")}</FormLabel>
<FormControl>
<Input
className="text-md w-full border border-input bg-background p-2 hover:bg-accent hover:text-accent-foreground dark:[color-scheme:dark]"
placeholder={t("masksAndZones.zones.name.inputPlaceHolder")}
{...field}
/>
</FormControl>
<FormDescription>
{t("masksAndZones.zones.name.tips")}
</FormDescription>
<FormMessage />
</FormItem>
)}
nameField="friendly_name"
idField="name"
nameLabel={t("masksAndZones.zones.name.title")}
nameDescription={t("masksAndZones.zones.name.tips")}
placeholderName={t("masksAndZones.zones.name.inputPlaceHolder")}
/>
<Separator className="my-2 flex bg-secondary" />
<FormField
control={form.control}