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 09:15:51 -05:00 committed by GitHub
parent adc8c2a6e8
commit 5059311c9d
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
9 changed files with 235 additions and 86 deletions

View File

@ -120,7 +120,7 @@ The following camera configuration sections can be overridden in a profile:
:::note :::note
Only the fields you explicitly set in a profile override are applied. All other fields retain their base configuration values. For zones, profile zones are merged with the camera's base zones — any zone defined in the profile will override or add to the base zones. Only the fields you explicitly set in a profile override are applied. All other fields retain their base configuration values. For masks and zones, profile zones **override** the camera's base masks and zones. If configuring profiles via YAML, you should not define masks or zones in profiles that are not defined in the base config.
::: :::

View File

@ -1224,6 +1224,15 @@ def camera_set(
status_code=400, status_code=400,
) )
if not sub_command and feature in _SUB_COMMAND_FEATURES:
return JSONResponse(
content={
"success": False,
"message": f"Feature '{feature}' requires a sub-command (e.g. mask or zone name)",
},
status_code=400,
)
if camera_name == "*": if camera_name == "*":
cameras = list(frigate_config.cameras.keys()) cameras = list(frigate_config.cameras.keys())
elif camera_name not in frigate_config.cameras: elif camera_name not in frigate_config.cameras:

View File

@ -118,10 +118,21 @@ class Dispatcher:
try: try:
if command_type == "set": if command_type == "set":
# Commands that require a sub-command (mask/zone name)
sub_command_required = {
"motion_mask",
"object_mask",
"zone",
}
if sub_command: if sub_command:
self._camera_settings_handlers[command]( self._camera_settings_handlers[command](
camera_name, sub_command, payload camera_name, sub_command, payload
) )
elif command in sub_command_required:
logger.error(
"Command %s requires a sub-command (mask/zone name)",
command,
)
else: else:
self._camera_settings_handlers[command](camera_name, payload) self._camera_settings_handlers[command](camera_name, payload)
elif command_type == "ptz": elif command_type == "ptz":

View File

@ -539,6 +539,7 @@
}, },
"restart_required": "Restart required (masks/zones changed)", "restart_required": "Restart required (masks/zones changed)",
"disabledInConfig": "Item is disabled in the config file", "disabledInConfig": "Item is disabled in the config file",
"addDisabledProfile": "Add to the base config first, then override in the profile",
"profileBase": "(base)", "profileBase": "(base)",
"profileOverride": "(override)", "profileOverride": "(override)",
"toast": { "toast": {
@ -613,6 +614,10 @@
"desc": "Are you sure you want to delete the {{type}} <em>{{name}}</em>?", "desc": "Are you sure you want to delete the {{type}} <em>{{name}}</em>?",
"success": "{{name}} has been deleted." "success": "{{name}} has been deleted."
}, },
"revertOverride": {
"title": "Revert to Base Config",
"desc": "This will remove the profile override for the {{type}} <em>{{name}}</em> and revert to the base configuration."
},
"error": { "error": {
"mustBeFinished": "Polygon drawing must be finished before saving." "mustBeFinished": "Polygon drawing must be finished before saving."
} }

View File

@ -74,9 +74,11 @@ export default function MotionMaskEditPane({
} }
}, [polygons, activePolygonIndex]); }, [polygons, activePolygonIndex]);
const maskCamera = polygon?.camera || "";
const maskName = polygon?.name || "";
const { send: sendMotionMaskState } = useMotionMaskState( const { send: sendMotionMaskState } = useMotionMaskState(
polygon?.camera || "", maskCamera,
polygon?.name || "", maskName,
); );
const cameraConfig = useMemo(() => { const cameraConfig = useMemo(() => {
@ -154,7 +156,7 @@ export default function MotionMaskEditPane({
message: t("masksAndZones.form.name.error.mustNotBeEmpty"), message: t("masksAndZones.form.name.error.mustNotBeEmpty"),
}), }),
enabled: z.boolean(), enabled: z.boolean(),
isFinished: z.boolean().refine(() => polygon?.isFinished === true, { isFinished: z.boolean().refine((val) => val === true, {
message: t("masksAndZones.form.polygonDrawing.error.mustBeFinished"), message: t("masksAndZones.form.polygonDrawing.error.mustBeFinished"),
}), }),
}); });
@ -170,6 +172,12 @@ export default function MotionMaskEditPane({
}, },
}); });
useEffect(() => {
if (polygon?.isFinished !== undefined) {
form.setValue("isFinished", polygon.isFinished, { shouldValidate: true });
}
}, [polygon?.isFinished, form]);
const saveToConfig = useCallback( const saveToConfig = useCallback(
async ({ async ({
name: maskId, name: maskId,
@ -250,8 +258,8 @@ export default function MotionMaskEditPane({
}, },
); );
updateConfig(); updateConfig();
// Only publish WS state for base config // Only publish WS state for base config when mask has a name
if (!editingProfile) { if (!editingProfile && maskName) {
sendMotionMaskState(enabled ? "ON" : "OFF"); sendMotionMaskState(enabled ? "ON" : "OFF");
} }
} else { } else {
@ -291,6 +299,7 @@ export default function MotionMaskEditPane({
cameraConfig, cameraConfig,
t, t,
sendMotionMaskState, sendMotionMaskState,
maskName,
editingProfile, editingProfile,
], ],
); );
@ -449,7 +458,7 @@ export default function MotionMaskEditPane({
<Button <Button
variant="select" variant="select"
aria-label={t("button.save", { ns: "common" })} aria-label={t("button.save", { ns: "common" })}
disabled={isLoading} disabled={isLoading || !form.formState.isValid}
className="flex flex-1" className="flex flex-1"
type="submit" type="submit"
> >

View File

@ -80,9 +80,10 @@ export default function ObjectMaskEditPane({
} }
}, [polygons, activePolygonIndex]); }, [polygons, activePolygonIndex]);
const maskName = polygon?.name || "";
const { send: sendObjectMaskState } = useObjectMaskState( const { send: sendObjectMaskState } = useObjectMaskState(
polygon?.camera || "", polygon?.camera || "",
polygon?.name || "", maskName,
); );
const cameraConfig = useMemo(() => { const cameraConfig = useMemo(() => {
@ -143,7 +144,7 @@ export default function ObjectMaskEditPane({
}), }),
enabled: z.boolean(), enabled: z.boolean(),
objects: z.string(), objects: z.string(),
isFinished: z.boolean().refine(() => polygon?.isFinished === true, { isFinished: z.boolean().refine((val) => val === true, {
message: t("masksAndZones.form.polygonDrawing.error.mustBeFinished"), message: t("masksAndZones.form.polygonDrawing.error.mustBeFinished"),
}), }),
}); });
@ -160,6 +161,12 @@ export default function ObjectMaskEditPane({
}, },
}); });
useEffect(() => {
if (polygon?.isFinished !== undefined) {
form.setValue("isFinished", polygon.isFinished, { shouldValidate: true });
}
}, [polygon?.isFinished, form]);
const saveToConfig = useCallback( const saveToConfig = useCallback(
async ({ async ({
name: maskId, name: maskId,
@ -256,8 +263,8 @@ export default function ObjectMaskEditPane({
}, },
); );
updateConfig(); updateConfig();
// Only publish WS state for base config // Only publish WS state for base config when mask has a name
if (!editingProfile) { if (!editingProfile && maskName) {
sendObjectMaskState(enabled ? "ON" : "OFF"); sendObjectMaskState(enabled ? "ON" : "OFF");
} }
} else { } else {
@ -300,6 +307,7 @@ export default function ObjectMaskEditPane({
cameraConfig, cameraConfig,
t, t,
sendObjectMaskState, sendObjectMaskState,
maskName,
editingProfile, editingProfile,
], ],
); );
@ -454,7 +462,7 @@ export default function ObjectMaskEditPane({
</Button> </Button>
<Button <Button
variant="select" variant="select"
disabled={isLoading} disabled={isLoading || !form.formState.isValid}
className="flex flex-1" className="flex flex-1"
aria-label={t("button.save", { ns: "common" })} aria-label={t("button.save", { ns: "common" })}
type="submit" type="submit"

View File

@ -30,7 +30,6 @@ import useSWR from "swr";
import { FrigateConfig } from "@/types/frigateConfig"; import { FrigateConfig } from "@/types/frigateConfig";
import { reviewQueries } from "@/utils/zoneEdutUtil"; import { reviewQueries } from "@/utils/zoneEdutUtil";
import IconWrapper from "../ui/icon-wrapper"; import IconWrapper from "../ui/icon-wrapper";
import { buttonVariants } from "../ui/button";
import { Trans, useTranslation } from "react-i18next"; import { Trans, useTranslation } from "react-i18next";
import ActivityIndicator from "../indicators/activity-indicator"; import ActivityIndicator from "../indicators/activity-indicator";
import { cn } from "@/lib/utils"; import { cn } from "@/lib/utils";
@ -51,6 +50,7 @@ type PolygonItemProps = {
setLoadingPolygonIndex: (index: number | undefined) => void; setLoadingPolygonIndex: (index: number | undefined) => void;
editingProfile?: string | null; editingProfile?: string | null;
allProfileNames?: string[]; allProfileNames?: string[];
onDeleted?: () => void;
}; };
export default function PolygonItem({ export default function PolygonItem({
@ -67,6 +67,7 @@ export default function PolygonItem({
setLoadingPolygonIndex, setLoadingPolygonIndex,
editingProfile, editingProfile,
allProfileNames, allProfileNames,
onDeleted,
}: PolygonItemProps) { }: PolygonItemProps) {
const { t } = useTranslation("views/settings"); const { t } = useTranslation("views/settings");
const { data: config, mutate: updateConfig } = const { data: config, mutate: updateConfig } =
@ -152,7 +153,19 @@ export default function PolygonItem({
cameraConfig?.review.alerts.required_zones || [], cameraConfig?.review.alerts.required_zones || [],
cameraConfig?.review.detections.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 await axios
@ -169,6 +182,7 @@ export default function PolygonItem({
{ position: "top-center" }, { position: "top-center" },
); );
updateConfig(); updateConfig();
onDeleted?.();
} else { } else {
toast.error( toast.error(
t("toast.save.error.title", { 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 = { const configUpdate = {
cameras: { cameras: {
[polygon.camera]: editingProfile [polygon.camera]: cameraUpdate,
? { profiles: { [editingProfile]: deleteSection } }
: deleteSection,
}, },
}; };
@ -234,6 +278,7 @@ export default function PolygonItem({
{ position: "top-center" }, { position: "top-center" },
); );
updateConfig(); updateConfig();
onDeleted?.();
} else { } else {
toast.error( toast.error(
t("toast.save.error.title", { t("toast.save.error.title", {
@ -267,6 +312,8 @@ export default function PolygonItem({
index, index,
setLoadingPolygonIndex, setLoadingPolygonIndex,
editingProfile, editingProfile,
allProfileNames,
onDeleted,
], ],
); );
@ -425,10 +472,26 @@ export default function PolygonItem({
<AlertDialogContent> <AlertDialogContent>
<AlertDialogHeader> <AlertDialogHeader>
<AlertDialogTitle> <AlertDialogTitle>
{t("masksAndZones.form.polygonDrawing.delete.title")} {polygon.polygonSource === "override"
? t("masksAndZones.form.polygonDrawing.revertOverride.title")
: t("masksAndZones.form.polygonDrawing.delete.title")}
</AlertDialogTitle> </AlertDialogTitle>
</AlertDialogHeader> </AlertDialogHeader>
<AlertDialogDescription> <AlertDialogDescription>
{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 <Trans
ns="views/settings" ns="views/settings"
values={{ values={{
@ -441,16 +504,19 @@ export default function PolygonItem({
> >
masksAndZones.form.polygonDrawing.delete.desc masksAndZones.form.polygonDrawing.delete.desc
</Trans> </Trans>
)}
</AlertDialogDescription> </AlertDialogDescription>
<AlertDialogFooter> <AlertDialogFooter>
<AlertDialogCancel> <AlertDialogCancel>
{t("button.cancel", { ns: "common" })} {t("button.cancel", { ns: "common" })}
</AlertDialogCancel> </AlertDialogCancel>
<AlertDialogAction <AlertDialogAction
className={buttonVariants({ variant: "destructive" })} className="bg-destructive text-white hover:bg-destructive/90"
onClick={handleDelete} onClick={handleDelete}
> >
{t("button.delete", { ns: "common" })} {polygon.polygonSource === "override"
? t("masksAndZones.form.polygonDrawing.revertOverride.title")
: t("button.delete", { ns: "common" })}
</AlertDialogAction> </AlertDialogAction>
</AlertDialogFooter> </AlertDialogFooter>
</AlertDialogContent> </AlertDialogContent>
@ -563,7 +629,9 @@ export default function PolygonItem({
/> />
</TooltipTrigger> </TooltipTrigger>
<TooltipContent> <TooltipContent>
{t("button.delete", { ns: "common" })} {polygon.polygonSource === "override"
? t("masksAndZones.form.polygonDrawing.revertOverride.title")
: t("button.delete", { ns: "common" })}
</TooltipContent> </TooltipContent>
</Tooltip> </Tooltip>
</div> </div>

View File

@ -91,10 +91,8 @@ export default function ZoneEditPane({
} }
}, [polygons, activePolygonIndex]); }, [polygons, activePolygonIndex]);
const { send: sendZoneState } = useZoneState( const zoneName = polygon?.name || "";
polygon?.camera || "", const { send: sendZoneState } = useZoneState(polygon?.camera || "", zoneName);
polygon?.name || "",
);
const cameraConfig = useMemo(() => { const cameraConfig = useMemo(() => {
if (polygon?.camera && config) { if (polygon?.camera && config) {
@ -210,7 +208,7 @@ export default function ZoneEditPane({
}) })
.optional() .optional()
.or(z.literal("")), .or(z.literal("")),
isFinished: z.boolean().refine(() => polygon?.isFinished === true, { isFinished: z.boolean().refine((val) => val === true, {
message: t("masksAndZones.form.polygonDrawing.error.mustBeFinished"), message: t("masksAndZones.form.polygonDrawing.error.mustBeFinished"),
}), }),
objects: z.array(z.string()).optional(), objects: z.array(z.string()).optional(),
@ -295,7 +293,7 @@ export default function ZoneEditPane({
const form = useForm<z.infer<typeof formSchema>>({ const form = useForm<z.infer<typeof formSchema>>({
resolver: zodResolver(formSchema), resolver: zodResolver(formSchema),
mode: "onBlur", mode: "onChange",
defaultValues: { defaultValues: {
name: polygon?.name ?? "", name: polygon?.name ?? "",
friendly_name: polygon?.friendly_name ?? polygon?.name ?? "", friendly_name: polygon?.friendly_name ?? polygon?.name ?? "",
@ -303,8 +301,8 @@ export default function ZoneEditPane({
resolvedZoneData?.enabled !== undefined resolvedZoneData?.enabled !== undefined
? resolvedZoneData.enabled ? resolvedZoneData.enabled
: (polygon?.enabled ?? true), : (polygon?.enabled ?? true),
inertia: resolvedZoneData?.inertia, inertia: resolvedZoneData?.inertia ?? 3,
loitering_time: resolvedZoneData?.loitering_time, loitering_time: resolvedZoneData?.loitering_time ?? 0,
isFinished: polygon?.isFinished ?? false, isFinished: polygon?.isFinished ?? false,
objects: polygon?.objects ?? [], objects: polygon?.objects ?? [],
speedEstimation: !!(lineA || lineB || lineC || lineD), speedEstimation: !!(lineA || lineB || lineC || lineD),
@ -316,18 +314,31 @@ export default function ZoneEditPane({
}, },
}); });
const watchSpeedEstimation = form.watch("speedEstimation");
const watchLineA = form.watch("lineA");
const watchLineB = form.watch("lineB");
const watchLineC = form.watch("lineC");
const watchLineD = form.watch("lineD");
const canSave =
form.formState.isValid &&
(!watchSpeedEstimation ||
(!!watchLineA && !!watchLineB && !!watchLineC && !!watchLineD));
useEffect(() => { useEffect(() => {
if ( if (watchSpeedEstimation && polygon && polygon.points.length !== 4) {
form.watch("speedEstimation") &&
polygon &&
polygon.points.length !== 4
) {
toast.error( toast.error(
t("masksAndZones.zones.speedThreshold.toast.error.pointLengthError"), t("masksAndZones.zones.speedThreshold.toast.error.pointLengthError"),
); );
form.setValue("speedEstimation", false); form.setValue("speedEstimation", false);
} }
}, [polygon, form, t]); }, [polygon, form, t, watchSpeedEstimation]);
useEffect(() => {
if (polygon?.isFinished !== undefined) {
form.setValue("isFinished", polygon.isFinished, { shouldValidate: true });
}
}, [polygon?.isFinished, form]);
const saveToConfig = useCallback( const saveToConfig = useCallback(
async ( async (
@ -516,8 +527,8 @@ export default function ZoneEditPane({
}, },
); );
updateConfig(); updateConfig();
// Only publish WS state for base config (not profiles) // Only publish WS state for base config when zone has a name
if (!editingProfile) { if (!editingProfile && zoneName) {
sendZoneState(enabled ? "ON" : "OFF"); sendZoneState(enabled ? "ON" : "OFF");
} }
} else { } else {
@ -968,7 +979,7 @@ export default function ZoneEditPane({
</Button> </Button>
<Button <Button
variant="select" variant="select"
disabled={isLoading} disabled={isLoading || !canSave}
className="flex flex-1" className="flex flex-1"
aria-label={t("button.save", { ns: "common" })} aria-label={t("button.save", { ns: "common" })}
type="submit" type="submit"

View File

@ -201,6 +201,16 @@ export default function MasksAndZonesView({
setUnsavedChanges(false); setUnsavedChanges(false);
}, [editingPolygons, setUnsavedChanges]); }, [editingPolygons, setUnsavedChanges]);
const handlePolygonDeleted = useCallback(() => {
// Temporarily clear the edit pane guard so the useEffect that
// rebuilds editingPolygons from config will run when the fresh
// config arrives via updateConfig(). This handles all cases:
// base deletes, profile override deletes (which revert to base),
// and profile-only deletes.
setEditPane(undefined);
setActivePolygonIndex(undefined);
}, [setEditPane, setActivePolygonIndex]);
useEffect(() => { useEffect(() => {
if (isLoading) { if (isLoading) {
return; return;
@ -806,10 +816,12 @@ export default function MasksAndZonesView({
</HoverCard> </HoverCard>
<Tooltip> <Tooltip>
<TooltipTrigger asChild> <TooltipTrigger asChild>
<span className="inline-flex">
<Button <Button
variant="secondary" variant="secondary"
className="size-6 rounded-md bg-secondary-foreground p-1 text-background" className="size-6 rounded-md bg-secondary-foreground p-1 text-background"
aria-label={t("masksAndZones.zones.add")} aria-label={t("masksAndZones.zones.add")}
disabled={!!currentEditingProfile}
onClick={() => { onClick={() => {
setEditPane("zone"); setEditPane("zone");
handleNewPolygon("zone"); handleNewPolygon("zone");
@ -817,9 +829,12 @@ export default function MasksAndZonesView({
> >
<LuPlus /> <LuPlus />
</Button> </Button>
</span>
</TooltipTrigger> </TooltipTrigger>
<TooltipContent> <TooltipContent>
{t("masksAndZones.zones.add")} {currentEditingProfile
? t("masksAndZones.addDisabledProfile")
: t("masksAndZones.zones.add")}
</TooltipContent> </TooltipContent>
</Tooltip> </Tooltip>
</div> </div>
@ -843,6 +858,7 @@ export default function MasksAndZonesView({
setLoadingPolygonIndex={setLoadingPolygonIndex} setLoadingPolygonIndex={setLoadingPolygonIndex}
editingProfile={currentEditingProfile} editingProfile={currentEditingProfile}
allProfileNames={profileState?.allProfileNames} allProfileNames={profileState?.allProfileNames}
onDeleted={handlePolygonDeleted}
/> />
))} ))}
</div> </div>
@ -880,10 +896,12 @@ export default function MasksAndZonesView({
</HoverCard> </HoverCard>
<Tooltip> <Tooltip>
<TooltipTrigger asChild> <TooltipTrigger asChild>
<span className="inline-flex">
<Button <Button
variant="secondary" variant="secondary"
className="size-6 rounded-md bg-secondary-foreground p-1 text-background" className="size-6 rounded-md bg-secondary-foreground p-1 text-background"
aria-label={t("masksAndZones.motionMasks.add")} aria-label={t("masksAndZones.motionMasks.add")}
disabled={!!currentEditingProfile}
onClick={() => { onClick={() => {
setEditPane("motion_mask"); setEditPane("motion_mask");
handleNewPolygon("motion_mask"); handleNewPolygon("motion_mask");
@ -891,9 +909,12 @@ export default function MasksAndZonesView({
> >
<LuPlus /> <LuPlus />
</Button> </Button>
</span>
</TooltipTrigger> </TooltipTrigger>
<TooltipContent> <TooltipContent>
{t("masksAndZones.motionMasks.add")} {currentEditingProfile
? t("masksAndZones.addDisabledProfile")
: t("masksAndZones.motionMasks.add")}
</TooltipContent> </TooltipContent>
</Tooltip> </Tooltip>
</div> </div>
@ -919,6 +940,7 @@ export default function MasksAndZonesView({
setLoadingPolygonIndex={setLoadingPolygonIndex} setLoadingPolygonIndex={setLoadingPolygonIndex}
editingProfile={currentEditingProfile} editingProfile={currentEditingProfile}
allProfileNames={profileState?.allProfileNames} allProfileNames={profileState?.allProfileNames}
onDeleted={handlePolygonDeleted}
/> />
))} ))}
</div> </div>
@ -956,10 +978,12 @@ export default function MasksAndZonesView({
</HoverCard> </HoverCard>
<Tooltip> <Tooltip>
<TooltipTrigger asChild> <TooltipTrigger asChild>
<span className="inline-flex">
<Button <Button
variant="secondary" variant="secondary"
className="size-6 rounded-md bg-secondary-foreground p-1 text-background" className="size-6 rounded-md bg-secondary-foreground p-1 text-background"
aria-label={t("masksAndZones.objectMasks.add")} aria-label={t("masksAndZones.objectMasks.add")}
disabled={!!currentEditingProfile}
onClick={() => { onClick={() => {
setEditPane("object_mask"); setEditPane("object_mask");
handleNewPolygon("object_mask"); handleNewPolygon("object_mask");
@ -967,9 +991,12 @@ export default function MasksAndZonesView({
> >
<LuPlus /> <LuPlus />
</Button> </Button>
</span>
</TooltipTrigger> </TooltipTrigger>
<TooltipContent> <TooltipContent>
{t("masksAndZones.objectMasks.add")} {currentEditingProfile
? t("masksAndZones.addDisabledProfile")
: t("masksAndZones.objectMasks.add")}
</TooltipContent> </TooltipContent>
</Tooltip> </Tooltip>
</div> </div>
@ -995,6 +1022,7 @@ export default function MasksAndZonesView({
setLoadingPolygonIndex={setLoadingPolygonIndex} setLoadingPolygonIndex={setLoadingPolygonIndex}
editingProfile={currentEditingProfile} editingProfile={currentEditingProfile}
allProfileNames={profileState?.allProfileNames} allProfileNames={profileState?.allProfileNames}
onDeleted={handlePolygonDeleted}
/> />
))} ))}
</div> </div>