Mask/zone editor fixes (#22732)

* add guards to reject missing sub commands

* mask/zone bugfixes

- fix websocket crash when creating a new mask or zone before a name is assigned
- fix deleted masks and zones not disappearing from the list until navigating away
- fix deleting profile override not reverting to the base mask in the list
- fix inertia defaulting to nan

* disable save button on invalid form state

* fix validation for speed estimation

* ensure polygon is closed before allowing save

* require all masks and zones to be on the base config

* clarify dialog message and tooltip when removing an override

* clarify docs
This commit is contained in:
Josh Hawkins
2026-04-02 08:15:51 -06:00
committed by GitHub
parent adc8c2a6e8
commit 5059311c9d
9 changed files with 235 additions and 86 deletions
+89 -21
View File
@@ -30,7 +30,6 @@ import useSWR from "swr";
import { FrigateConfig } from "@/types/frigateConfig";
import { reviewQueries } from "@/utils/zoneEdutUtil";
import IconWrapper from "../ui/icon-wrapper";
import { buttonVariants } from "../ui/button";
import { Trans, useTranslation } from "react-i18next";
import ActivityIndicator from "../indicators/activity-indicator";
import { cn } from "@/lib/utils";
@@ -51,6 +50,7 @@ type PolygonItemProps = {
setLoadingPolygonIndex: (index: number | undefined) => void;
editingProfile?: string | null;
allProfileNames?: string[];
onDeleted?: () => void;
};
export default function PolygonItem({
@@ -67,6 +67,7 @@ export default function PolygonItem({
setLoadingPolygonIndex,
editingProfile,
allProfileNames,
onDeleted,
}: PolygonItemProps) {
const { t } = useTranslation("views/settings");
const { data: config, mutate: updateConfig } =
@@ -152,7 +153,19 @@ export default function PolygonItem({
cameraConfig?.review.alerts.required_zones || [],
cameraConfig?.review.detections.required_zones || [],
);
url = `cameras.${polygon.camera}.zones.${polygon.name}${alertQueries}${detectionQueries}`;
// Also delete from profiles that have overrides for this zone
let profileQueries = "";
if (allProfileNames && cameraConfig) {
for (const profileName of allProfileNames) {
if (
cameraConfig.profiles?.[profileName]?.zones?.[polygon.name] !==
undefined
) {
profileQueries += `&cameras.${polygon.camera}.profiles.${profileName}.zones.${polygon.name}`;
}
}
}
url = `cameras.${polygon.camera}.zones.${polygon.name}${alertQueries}${detectionQueries}${profileQueries}`;
}
await axios
@@ -169,6 +182,7 @@ export default function PolygonItem({
{ position: "top-center" },
);
updateConfig();
onDeleted?.();
} else {
toast.error(
t("toast.save.error.title", {
@@ -211,11 +225,41 @@ export default function PolygonItem({
},
};
let cameraUpdate: Record<string, unknown>;
if (editingProfile) {
cameraUpdate = { profiles: { [editingProfile]: deleteSection } };
} else {
// Base mode: also delete from profiles that have overrides for this mask
const profileDeletes: Record<string, unknown> = {};
if (allProfileNames && cameraConfig) {
for (const profileName of allProfileNames) {
const profileData = cameraConfig.profiles?.[profileName];
if (!profileData) continue;
const hasMask =
polygon.type === "motion_mask"
? profileData.motion?.mask?.[polygon.name] !== undefined
: polygon.type === "object_mask"
? profileData.objects?.mask?.[polygon.name] !== undefined ||
Object.values(profileData.objects?.filters || {}).some(
(f) => f?.mask?.[polygon.name] !== undefined,
)
: false;
if (hasMask) {
profileDeletes[profileName] = deleteSection;
}
}
}
cameraUpdate =
Object.keys(profileDeletes).length > 0
? { ...deleteSection, profiles: profileDeletes }
: deleteSection;
}
const configUpdate = {
cameras: {
[polygon.camera]: editingProfile
? { profiles: { [editingProfile]: deleteSection } }
: deleteSection,
[polygon.camera]: cameraUpdate,
},
};
@@ -234,6 +278,7 @@ export default function PolygonItem({
{ position: "top-center" },
);
updateConfig();
onDeleted?.();
} else {
toast.error(
t("toast.save.error.title", {
@@ -267,6 +312,8 @@ export default function PolygonItem({
index,
setLoadingPolygonIndex,
editingProfile,
allProfileNames,
onDeleted,
],
);
@@ -425,32 +472,51 @@ export default function PolygonItem({
<AlertDialogContent>
<AlertDialogHeader>
<AlertDialogTitle>
{t("masksAndZones.form.polygonDrawing.delete.title")}
{polygon.polygonSource === "override"
? t("masksAndZones.form.polygonDrawing.revertOverride.title")
: t("masksAndZones.form.polygonDrawing.delete.title")}
</AlertDialogTitle>
</AlertDialogHeader>
<AlertDialogDescription>
<Trans
ns="views/settings"
values={{
type: t(
`masksAndZones.form.polygonDrawing.type.${polygon.type}`,
{ ns: "views/settings" },
),
name: polygon.friendly_name ?? polygon.name,
}}
>
masksAndZones.form.polygonDrawing.delete.desc
</Trans>
{polygon.polygonSource === "override" ? (
<Trans
ns="views/settings"
values={{
type: t(
`masksAndZones.form.polygonDrawing.type.${polygon.type}`,
{ ns: "views/settings" },
),
name: polygon.friendly_name ?? polygon.name,
}}
>
masksAndZones.form.polygonDrawing.revertOverride.desc
</Trans>
) : (
<Trans
ns="views/settings"
values={{
type: t(
`masksAndZones.form.polygonDrawing.type.${polygon.type}`,
{ ns: "views/settings" },
),
name: polygon.friendly_name ?? polygon.name,
}}
>
masksAndZones.form.polygonDrawing.delete.desc
</Trans>
)}
</AlertDialogDescription>
<AlertDialogFooter>
<AlertDialogCancel>
{t("button.cancel", { ns: "common" })}
</AlertDialogCancel>
<AlertDialogAction
className={buttonVariants({ variant: "destructive" })}
className="bg-destructive text-white hover:bg-destructive/90"
onClick={handleDelete}
>
{t("button.delete", { ns: "common" })}
{polygon.polygonSource === "override"
? t("masksAndZones.form.polygonDrawing.revertOverride.title")
: t("button.delete", { ns: "common" })}
</AlertDialogAction>
</AlertDialogFooter>
</AlertDialogContent>
@@ -563,7 +629,9 @@ export default function PolygonItem({
/>
</TooltipTrigger>
<TooltipContent>
{t("button.delete", { ns: "common" })}
{polygon.polygonSource === "override"
? t("masksAndZones.form.polygonDrawing.revertOverride.title")
: t("button.delete", { ns: "common" })}
</TooltipContent>
</Tooltip>
</div>