import { useCallback, useMemo, useState } from "react"; import { useTranslation } from "react-i18next"; import type { SectionConfig } from "@/components/config-form/sections"; import { ConfigSectionTemplate } from "@/components/config-form/sections"; import type { PolygonType } from "@/types/canvas"; import { Badge } from "@/components/ui/badge"; import { Tooltip, TooltipContent, TooltipTrigger, } from "@/components/ui/tooltip"; import type { ConfigSectionData } from "@/types/configForm"; import type { ProfileState } from "@/types/profile"; import { getSectionConfig } from "@/utils/configUtil"; import { getProfileColor } from "@/utils/profileColors"; import { cn } from "@/lib/utils"; import { useDocDomain } from "@/hooks/use-doc-domain"; import { Link } from "react-router-dom"; import { LuExternalLink } from "react-icons/lu"; import Heading from "@/components/ui/heading"; export type SettingsPageProps = { selectedCamera?: string; setUnsavedChanges?: React.Dispatch>; selectedZoneMask?: PolygonType[]; onSectionStatusChange?: ( sectionKey: string, level: "global" | "camera", status: SectionStatus, ) => void; pendingDataBySection?: Record; onPendingDataChange?: ( sectionKey: string, cameraName: string | undefined, data: ConfigSectionData | null, ) => void; profileState?: ProfileState; /** Callback to delete the current profile's overrides for the current section */ onDeleteProfileSection?: (profileName: string) => void; profilesUIEnabled?: boolean; setProfilesUIEnabled?: React.Dispatch>; }; export type SectionStatus = { hasChanges: boolean; isOverridden: boolean; /** Where the override comes from: "global" = camera overrides global, "profile" = profile overrides base */ overrideSource?: "global" | "profile"; hasValidationErrors: boolean; }; export type SingleSectionPageOptions = { sectionKey: string; level: "global" | "camera"; sectionConfig?: SectionConfig; requiresRestart?: boolean; showOverrideIndicator?: boolean; }; export type SingleSectionPageProps = SettingsPageProps & SingleSectionPageOptions; export function SingleSectionPage({ sectionKey, level, sectionConfig, requiresRestart, showOverrideIndicator = true, selectedCamera, setUnsavedChanges, onSectionStatusChange, pendingDataBySection, onPendingDataChange, profileState, onDeleteProfileSection, }: SingleSectionPageProps) { const sectionNamespace = level === "camera" ? "config/cameras" : "config/global"; const { t, i18n } = useTranslation([ sectionNamespace, "views/settings", "common", ]); const { getLocaleDocUrl } = useDocDomain(); const [sectionStatus, setSectionStatus] = useState({ hasChanges: false, isOverridden: false, hasValidationErrors: false, }); const resolvedSectionConfig = useMemo( () => sectionConfig ?? getSectionConfig(sectionKey, level), [level, sectionConfig, sectionKey], ); const sectionDocsUrl = resolvedSectionConfig.sectionDocs ? getLocaleDocUrl(resolvedSectionConfig.sectionDocs) : undefined; const currentEditingProfile = selectedCamera ? (profileState?.editingProfile[selectedCamera] ?? null) : null; const profileColor = useMemo( () => currentEditingProfile && profileState?.allProfileNames ? getProfileColor(currentEditingProfile, profileState.allProfileNames) : undefined, [currentEditingProfile, profileState?.allProfileNames], ); const handleDeleteProfileSection = useCallback(() => { if (currentEditingProfile && onDeleteProfileSection) { onDeleteProfileSection(currentEditingProfile); } }, [currentEditingProfile, onDeleteProfileSection]); const handleSectionStatusChange = useCallback( (status: SectionStatus) => { setSectionStatus(status); onSectionStatusChange?.(sectionKey, level, status); }, [level, onSectionStatusChange, sectionKey], ); if (level === "camera" && !selectedCamera) { return (
{t("configForm.camera.noCameras", { ns: "views/settings" })}
); } return (
{t(`${sectionKey}.label`, { ns: sectionNamespace })} {i18n.exists(`${sectionKey}.description`, { ns: sectionNamespace, }) && (
{t(`${sectionKey}.description`, { ns: sectionNamespace })}
)} {sectionDocsUrl && (
{t("readTheDocumentation", { ns: "common" })}
)}
{/* Desktop: badge inline next to title */}
{level === "camera" && showOverrideIndicator && sectionStatus.isOverridden && ( {sectionStatus.overrideSource === "profile" ? t("button.overriddenBaseConfig", { ns: "views/settings", defaultValue: "Overridden (Base Config)", }) : t("button.overriddenGlobal", { ns: "views/settings", defaultValue: "Overridden (Global)", })} {sectionStatus.overrideSource === "profile" ? t("button.overriddenBaseConfigTooltip", { ns: "views/settings", profile: currentEditingProfile ? (profileState?.profileFriendlyNames.get( currentEditingProfile, ) ?? currentEditingProfile) : "", }) : t("button.overriddenGlobalTooltip", { ns: "views/settings", })} )} {sectionStatus.hasChanges && ( {t("modified", { ns: "common", defaultValue: "Modified" })} )}
{/* Mobile: badge below title/description */}
{level === "camera" && showOverrideIndicator && sectionStatus.isOverridden && ( {sectionStatus.overrideSource === "profile" ? t("button.overriddenBaseConfig", { ns: "views/settings", defaultValue: "Overridden (Base Config)", }) : t("button.overriddenGlobal", { ns: "views/settings", defaultValue: "Overridden (Global)", })} )} {sectionStatus.hasChanges && ( {t("modified", { ns: "common", defaultValue: "Modified" })} )}
setUnsavedChanges?.(false)} showTitle={false} sectionConfig={resolvedSectionConfig} pendingDataBySection={pendingDataBySection} onPendingDataChange={onPendingDataChange} requiresRestart={requiresRestart} onStatusChange={handleSectionStatusChange} profileName={currentEditingProfile ?? undefined} profileFriendlyName={ currentEditingProfile ? (profileState?.profileFriendlyNames.get(currentEditingProfile) ?? currentEditingProfile) : undefined } profileBorderColor={profileColor?.border} onDeleteProfileSection={ currentEditingProfile ? handleDeleteProfileSection : undefined } />
); }