Compare commits

..

No commits in common. "520d9eeb7f0fe46021f29fb8169741dd1d429271" and "adc8c2a6e848bbb6a443d4a17bafd745a1cdb638" have entirely different histories.

10 changed files with 96 additions and 235 deletions

View File

@ -227,6 +227,16 @@ http {
include proxy.conf; include proxy.conf;
} }
# frontend uses this to fetch the version
location /api/go2rtc/api {
include auth_request.conf;
limit_except GET {
deny all;
}
proxy_pass http://go2rtc/api;
include proxy.conf;
}
# integration uses this to add webrtc candidate # integration uses this to add webrtc candidate
location /api/go2rtc/webrtc { location /api/go2rtc/webrtc {
include auth_request.conf; include auth_request.conf;

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 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. 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.
::: :::

View File

@ -1224,15 +1224,6 @@ 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,21 +118,10 @@ 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,7 +539,6 @@
}, },
"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": {
@ -614,10 +613,6 @@
"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,11 +74,9 @@ export default function MotionMaskEditPane({
} }
}, [polygons, activePolygonIndex]); }, [polygons, activePolygonIndex]);
const maskCamera = polygon?.camera || "";
const maskName = polygon?.name || "";
const { send: sendMotionMaskState } = useMotionMaskState( const { send: sendMotionMaskState } = useMotionMaskState(
maskCamera, polygon?.camera || "",
maskName, polygon?.name || "",
); );
const cameraConfig = useMemo(() => { const cameraConfig = useMemo(() => {
@ -156,7 +154,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((val) => val === true, { isFinished: z.boolean().refine(() => polygon?.isFinished === true, {
message: t("masksAndZones.form.polygonDrawing.error.mustBeFinished"), message: t("masksAndZones.form.polygonDrawing.error.mustBeFinished"),
}), }),
}); });
@ -172,12 +170,6 @@ 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,
@ -258,8 +250,8 @@ export default function MotionMaskEditPane({
}, },
); );
updateConfig(); updateConfig();
// Only publish WS state for base config when mask has a name // Only publish WS state for base config
if (!editingProfile && maskName) { if (!editingProfile) {
sendMotionMaskState(enabled ? "ON" : "OFF"); sendMotionMaskState(enabled ? "ON" : "OFF");
} }
} else { } else {
@ -299,7 +291,6 @@ export default function MotionMaskEditPane({
cameraConfig, cameraConfig,
t, t,
sendMotionMaskState, sendMotionMaskState,
maskName,
editingProfile, editingProfile,
], ],
); );
@ -458,7 +449,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 || !form.formState.isValid} disabled={isLoading}
className="flex flex-1" className="flex flex-1"
type="submit" type="submit"
> >

View File

@ -80,10 +80,9 @@ export default function ObjectMaskEditPane({
} }
}, [polygons, activePolygonIndex]); }, [polygons, activePolygonIndex]);
const maskName = polygon?.name || "";
const { send: sendObjectMaskState } = useObjectMaskState( const { send: sendObjectMaskState } = useObjectMaskState(
polygon?.camera || "", polygon?.camera || "",
maskName, polygon?.name || "",
); );
const cameraConfig = useMemo(() => { const cameraConfig = useMemo(() => {
@ -144,7 +143,7 @@ export default function ObjectMaskEditPane({
}), }),
enabled: z.boolean(), enabled: z.boolean(),
objects: z.string(), objects: z.string(),
isFinished: z.boolean().refine((val) => val === true, { isFinished: z.boolean().refine(() => polygon?.isFinished === true, {
message: t("masksAndZones.form.polygonDrawing.error.mustBeFinished"), message: t("masksAndZones.form.polygonDrawing.error.mustBeFinished"),
}), }),
}); });
@ -161,12 +160,6 @@ 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,
@ -263,8 +256,8 @@ export default function ObjectMaskEditPane({
}, },
); );
updateConfig(); updateConfig();
// Only publish WS state for base config when mask has a name // Only publish WS state for base config
if (!editingProfile && maskName) { if (!editingProfile) {
sendObjectMaskState(enabled ? "ON" : "OFF"); sendObjectMaskState(enabled ? "ON" : "OFF");
} }
} else { } else {
@ -307,7 +300,6 @@ export default function ObjectMaskEditPane({
cameraConfig, cameraConfig,
t, t,
sendObjectMaskState, sendObjectMaskState,
maskName,
editingProfile, editingProfile,
], ],
); );
@ -462,7 +454,7 @@ export default function ObjectMaskEditPane({
</Button> </Button>
<Button <Button
variant="select" variant="select"
disabled={isLoading || !form.formState.isValid} disabled={isLoading}
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,6 +30,7 @@ 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";
@ -50,7 +51,6 @@ 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,7 +67,6 @@ 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 } =
@ -153,19 +152,7 @@ export default function PolygonItem({
cameraConfig?.review.alerts.required_zones || [], cameraConfig?.review.alerts.required_zones || [],
cameraConfig?.review.detections.required_zones || [], cameraConfig?.review.detections.required_zones || [],
); );
// Also delete from profiles that have overrides for this zone url = `cameras.${polygon.camera}.zones.${polygon.name}${alertQueries}${detectionQueries}`;
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
@ -182,7 +169,6 @@ 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", {
@ -225,41 +211,11 @@ 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]: cameraUpdate, [polygon.camera]: editingProfile
? { profiles: { [editingProfile]: deleteSection } }
: deleteSection,
}, },
}; };
@ -278,7 +234,6 @@ 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", {
@ -312,8 +267,6 @@ export default function PolygonItem({
index, index,
setLoadingPolygonIndex, setLoadingPolygonIndex,
editingProfile, editingProfile,
allProfileNames,
onDeleted,
], ],
); );
@ -472,26 +425,10 @@ export default function PolygonItem({
<AlertDialogContent> <AlertDialogContent>
<AlertDialogHeader> <AlertDialogHeader>
<AlertDialogTitle> <AlertDialogTitle>
{polygon.polygonSource === "override" {t("masksAndZones.form.polygonDrawing.delete.title")}
? 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={{
@ -504,19 +441,16 @@ 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="bg-destructive text-white hover:bg-destructive/90" className={buttonVariants({ variant: "destructive" })}
onClick={handleDelete} onClick={handleDelete}
> >
{polygon.polygonSource === "override" {t("button.delete", { ns: "common" })}
? t("masksAndZones.form.polygonDrawing.revertOverride.title")
: t("button.delete", { ns: "common" })}
</AlertDialogAction> </AlertDialogAction>
</AlertDialogFooter> </AlertDialogFooter>
</AlertDialogContent> </AlertDialogContent>
@ -629,9 +563,7 @@ export default function PolygonItem({
/> />
</TooltipTrigger> </TooltipTrigger>
<TooltipContent> <TooltipContent>
{polygon.polygonSource === "override" {t("button.delete", { ns: "common" })}
? t("masksAndZones.form.polygonDrawing.revertOverride.title")
: t("button.delete", { ns: "common" })}
</TooltipContent> </TooltipContent>
</Tooltip> </Tooltip>
</div> </div>

View File

@ -91,8 +91,10 @@ export default function ZoneEditPane({
} }
}, [polygons, activePolygonIndex]); }, [polygons, activePolygonIndex]);
const zoneName = polygon?.name || ""; const { send: sendZoneState } = useZoneState(
const { send: sendZoneState } = useZoneState(polygon?.camera || "", zoneName); polygon?.camera || "",
polygon?.name || "",
);
const cameraConfig = useMemo(() => { const cameraConfig = useMemo(() => {
if (polygon?.camera && config) { if (polygon?.camera && config) {
@ -208,7 +210,7 @@ export default function ZoneEditPane({
}) })
.optional() .optional()
.or(z.literal("")), .or(z.literal("")),
isFinished: z.boolean().refine((val) => val === true, { isFinished: z.boolean().refine(() => polygon?.isFinished === 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(),
@ -293,7 +295,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: "onChange", mode: "onBlur",
defaultValues: { defaultValues: {
name: polygon?.name ?? "", name: polygon?.name ?? "",
friendly_name: polygon?.friendly_name ?? polygon?.name ?? "", friendly_name: polygon?.friendly_name ?? polygon?.name ?? "",
@ -301,8 +303,8 @@ export default function ZoneEditPane({
resolvedZoneData?.enabled !== undefined resolvedZoneData?.enabled !== undefined
? resolvedZoneData.enabled ? resolvedZoneData.enabled
: (polygon?.enabled ?? true), : (polygon?.enabled ?? true),
inertia: resolvedZoneData?.inertia ?? 3, inertia: resolvedZoneData?.inertia,
loitering_time: resolvedZoneData?.loitering_time ?? 0, loitering_time: resolvedZoneData?.loitering_time,
isFinished: polygon?.isFinished ?? false, isFinished: polygon?.isFinished ?? false,
objects: polygon?.objects ?? [], objects: polygon?.objects ?? [],
speedEstimation: !!(lineA || lineB || lineC || lineD), speedEstimation: !!(lineA || lineB || lineC || lineD),
@ -314,31 +316,18 @@ 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 (watchSpeedEstimation && polygon && polygon.points.length !== 4) { if (
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, watchSpeedEstimation]); }, [polygon, form, t]);
useEffect(() => {
if (polygon?.isFinished !== undefined) {
form.setValue("isFinished", polygon.isFinished, { shouldValidate: true });
}
}, [polygon?.isFinished, form]);
const saveToConfig = useCallback( const saveToConfig = useCallback(
async ( async (
@ -527,8 +516,8 @@ export default function ZoneEditPane({
}, },
); );
updateConfig(); updateConfig();
// Only publish WS state for base config when zone has a name // Only publish WS state for base config (not profiles)
if (!editingProfile && zoneName) { if (!editingProfile) {
sendZoneState(enabled ? "ON" : "OFF"); sendZoneState(enabled ? "ON" : "OFF");
} }
} else { } else {
@ -979,7 +968,7 @@ export default function ZoneEditPane({
</Button> </Button>
<Button <Button
variant="select" variant="select"
disabled={isLoading || !canSave} disabled={isLoading}
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,16 +201,6 @@ 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;
@ -816,12 +806,10 @@ 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");
@ -829,12 +817,9 @@ export default function MasksAndZonesView({
> >
<LuPlus /> <LuPlus />
</Button> </Button>
</span>
</TooltipTrigger> </TooltipTrigger>
<TooltipContent> <TooltipContent>
{currentEditingProfile {t("masksAndZones.zones.add")}
? t("masksAndZones.addDisabledProfile")
: t("masksAndZones.zones.add")}
</TooltipContent> </TooltipContent>
</Tooltip> </Tooltip>
</div> </div>
@ -858,7 +843,6 @@ export default function MasksAndZonesView({
setLoadingPolygonIndex={setLoadingPolygonIndex} setLoadingPolygonIndex={setLoadingPolygonIndex}
editingProfile={currentEditingProfile} editingProfile={currentEditingProfile}
allProfileNames={profileState?.allProfileNames} allProfileNames={profileState?.allProfileNames}
onDeleted={handlePolygonDeleted}
/> />
))} ))}
</div> </div>
@ -896,12 +880,10 @@ 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");
@ -909,12 +891,9 @@ export default function MasksAndZonesView({
> >
<LuPlus /> <LuPlus />
</Button> </Button>
</span>
</TooltipTrigger> </TooltipTrigger>
<TooltipContent> <TooltipContent>
{currentEditingProfile {t("masksAndZones.motionMasks.add")}
? t("masksAndZones.addDisabledProfile")
: t("masksAndZones.motionMasks.add")}
</TooltipContent> </TooltipContent>
</Tooltip> </Tooltip>
</div> </div>
@ -940,7 +919,6 @@ export default function MasksAndZonesView({
setLoadingPolygonIndex={setLoadingPolygonIndex} setLoadingPolygonIndex={setLoadingPolygonIndex}
editingProfile={currentEditingProfile} editingProfile={currentEditingProfile}
allProfileNames={profileState?.allProfileNames} allProfileNames={profileState?.allProfileNames}
onDeleted={handlePolygonDeleted}
/> />
))} ))}
</div> </div>
@ -978,12 +956,10 @@ 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");
@ -991,12 +967,9 @@ export default function MasksAndZonesView({
> >
<LuPlus /> <LuPlus />
</Button> </Button>
</span>
</TooltipTrigger> </TooltipTrigger>
<TooltipContent> <TooltipContent>
{currentEditingProfile {t("masksAndZones.objectMasks.add")}
? t("masksAndZones.addDisabledProfile")
: t("masksAndZones.objectMasks.add")}
</TooltipContent> </TooltipContent>
</Tooltip> </Tooltip>
</div> </div>
@ -1022,7 +995,6 @@ export default function MasksAndZonesView({
setLoadingPolygonIndex={setLoadingPolygonIndex} setLoadingPolygonIndex={setLoadingPolygonIndex}
editingProfile={currentEditingProfile} editingProfile={currentEditingProfile}
allProfileNames={profileState?.allProfileNames} allProfileNames={profileState?.allProfileNames}
onDeleted={handlePolygonDeleted}
/> />
))} ))}
</div> </div>