Miscellaneous Fixes (#21050)

* Don't add to history when opening search dialog

* Update caniuse

* Revamp the history handling for dialog components

* clarify audio transcription docs

* Use titlecase helper

* Allow running object clasasification on stationary objects

* small spacing tweaks for tablets

* require admin role to delete users

* explicitly prevent deletion of admin user

---------

Co-authored-by: Josh Hawkins <32435876+hawkeye217@users.noreply.github.com>
This commit is contained in:
Nicolas Mowen
2025-11-26 06:23:51 -07:00
committed by GitHub
parent e79ff9a079
commit de2144f158
13 changed files with 144 additions and 119 deletions

View File

@@ -13,7 +13,7 @@ import { cn } from "@/lib/utils";
import { isPWA } from "@/utils/isPWA";
import { Button } from "@/components/ui/button";
import { useTranslation } from "react-i18next";
import { useLocation } from "react-router-dom";
import { useHistoryBack } from "@/hooks/use-history-back";
const MobilePageContext = createContext<{
open: boolean;
@@ -24,15 +24,16 @@ type MobilePageProps = {
children: React.ReactNode;
open?: boolean;
onOpenChange?: (open: boolean) => void;
enableHistoryBack?: boolean;
};
export function MobilePage({
children,
open: controlledOpen,
onOpenChange,
enableHistoryBack = true,
}: MobilePageProps) {
const [uncontrolledOpen, setUncontrolledOpen] = useState(false);
const location = useLocation();
const open = controlledOpen ?? uncontrolledOpen;
const setOpen = useCallback(
@@ -46,33 +47,12 @@ export function MobilePage({
[onOpenChange, setUncontrolledOpen],
);
useEffect(() => {
let isActive = true;
if (open && isActive) {
window.history.pushState({ isMobilePage: true }, "", location.pathname);
}
const handlePopState = (event: PopStateEvent) => {
if (open && isActive) {
event.preventDefault();
setOpen(false);
// Delay replaceState to ensure state updates are processed
setTimeout(() => {
if (isActive) {
window.history.replaceState(null, "", location.pathname);
}
}, 0);
}
};
window.addEventListener("popstate", handlePopState);
return () => {
isActive = false;
window.removeEventListener("popstate", handlePopState);
};
}, [open, setOpen, location.pathname]);
// Handle browser back button to close mobile page
useHistoryBack({
enabled: enableHistoryBack,
open,
onClose: () => setOpen(false),
});
return (
<MobilePageContext.Provider value={{ open, onOpenChange: setOpen }}>

View File

@@ -113,7 +113,12 @@ export function PlatformAwareSheet({
}
return (
<Sheet open={open} onOpenChange={onOpenChange} modal={false}>
<Sheet
open={open}
onOpenChange={onOpenChange}
modal={false}
enableHistoryBack
>
<SheetTrigger asChild className={triggerClassName}>
{trigger}
</SheetTrigger>

View File

@@ -2,6 +2,7 @@ import * as React from "react";
import * as DialogPrimitive from "@radix-ui/react-dialog";
import { X } from "lucide-react";
import { cn } from "@/lib/utils";
import { useHistoryBack } from "@/hooks/use-history-back";
// Enhanced Dialog with History Support
interface HistoryDialogProps extends DialogPrimitive.DialogProps {
@@ -15,51 +16,28 @@ const Dialog = ({
...props
}: HistoryDialogProps) => {
const [internalOpen, setInternalOpen] = React.useState(open || false);
const historyStateRef = React.useRef<null | {
listener: (e: PopStateEvent) => void;
}>(null);
// Sync internal state with controlled open prop
React.useEffect(() => {
if (open !== undefined) {
setInternalOpen(open);
}
}, [open]);
React.useEffect(() => {
if (enableHistoryBack) {
if (internalOpen) {
window.history.pushState({ dialogOpen: true }, "");
const handleOpenChange = React.useCallback(
(newOpen: boolean) => {
setInternalOpen(newOpen);
onOpenChange?.(newOpen);
},
[onOpenChange],
);
const listener = () => {
setInternalOpen(false);
if (onOpenChange) onOpenChange(false);
};
historyStateRef.current = { listener };
window.addEventListener("popstate", listener);
return () => {
if (internalOpen) {
window.removeEventListener("popstate", listener);
historyStateRef.current = null;
}
};
} else if (historyStateRef.current) {
window.removeEventListener(
"popstate",
historyStateRef.current.listener,
);
historyStateRef.current = null;
}
}
}, [enableHistoryBack, internalOpen, onOpenChange]);
const handleOpenChange = (open: boolean) => {
setInternalOpen(open);
if (onOpenChange) {
onOpenChange(open);
}
};
// Handle browser back button to close dialog
useHistoryBack({
enabled: enableHistoryBack,
open: internalOpen,
onClose: () => handleOpenChange(false),
});
return (
<DialogPrimitive.Root

View File

@@ -4,6 +4,7 @@ import { cva, type VariantProps } from "class-variance-authority";
import { X } from "lucide-react";
import { cn } from "@/lib/utils";
import { useHistoryBack } from "@/hooks/use-history-back";
// Enhanced Sheet with History Support
interface HistorySheetProps extends SheetPrimitive.DialogProps {
@@ -17,51 +18,28 @@ const Sheet = ({
...props
}: HistorySheetProps) => {
const [internalOpen, setInternalOpen] = React.useState(open || false);
const historyStateRef = React.useRef<null | {
listener: (e: PopStateEvent) => void;
}>(null);
// Sync internal state with controlled open prop
React.useEffect(() => {
if (open !== undefined) {
setInternalOpen(open);
}
}, [open]);
React.useEffect(() => {
if (enableHistoryBack) {
if (internalOpen) {
window.history.pushState({ sheetOpen: true }, "");
const handleOpenChange = React.useCallback(
(newOpen: boolean) => {
setInternalOpen(newOpen);
onOpenChange?.(newOpen);
},
[onOpenChange],
);
const listener = () => {
setInternalOpen(false);
if (onOpenChange) onOpenChange(false);
};
historyStateRef.current = { listener };
window.addEventListener("popstate", listener);
return () => {
if (internalOpen) {
window.removeEventListener("popstate", listener);
historyStateRef.current = null;
}
};
} else if (historyStateRef.current) {
window.removeEventListener(
"popstate",
historyStateRef.current.listener,
);
historyStateRef.current = null;
}
}
}, [enableHistoryBack, internalOpen, onOpenChange]);
const handleOpenChange = (open: boolean) => {
setInternalOpen(open);
if (onOpenChange) {
onOpenChange(open);
}
};
// Handle browser back button to close sheet
useHistoryBack({
enabled: enableHistoryBack,
open: internalOpen,
onClose: () => handleOpenChange(false),
});
return (
<SheetPrimitive.Root

View File

@@ -0,0 +1,57 @@
import * as React from "react";
interface UseHistoryBackOptions {
enabled: boolean;
open: boolean;
onClose: () => void;
}
/**
* Hook that manages browser history for overlay components (dialogs, sheets, etc.)
* When enabled, pressing the browser back button will close the overlay instead of navigating away.
*/
export function useHistoryBack({
enabled,
open,
onClose,
}: UseHistoryBackOptions): void {
const historyPushedRef = React.useRef(false);
const closedByBackRef = React.useRef(false);
// Keep onClose in a ref to avoid effect re-runs that cause multiple history pushes
const onCloseRef = React.useRef(onClose);
React.useLayoutEffect(() => {
onCloseRef.current = onClose;
});
React.useEffect(() => {
if (!enabled) return;
if (open) {
// Only push history state if we haven't already (prevents duplicates in strict mode)
if (!historyPushedRef.current) {
window.history.pushState({ overlayOpen: true }, "");
historyPushedRef.current = true;
}
const handlePopState = () => {
closedByBackRef.current = true;
historyPushedRef.current = false;
onCloseRef.current();
};
window.addEventListener("popstate", handlePopState);
return () => {
window.removeEventListener("popstate", handlePopState);
};
} else {
// Overlay is closing - clean up history if we pushed and it wasn't via back button
if (historyPushedRef.current && !closedByBackRef.current) {
window.history.back();
}
historyPushedRef.current = false;
closedByBackRef.current = false;
}
}, [enabled, open]);
}

View File

@@ -35,6 +35,7 @@ import { useTranslation } from "react-i18next";
import { useDocDomain } from "@/hooks/use-doc-domain";
import { getTranslatedLabel } from "@/utils/i18n";
import { cn } from "@/lib/utils";
type MasksAndZoneViewProps = {
selectedCamera: string;
@@ -697,7 +698,10 @@ export default function MasksAndZonesView({
</div>
<div
ref={containerRef}
className="flex max-h-[50%] md:mr-3 md:h-dvh md:max-h-full md:w-7/12 md:grow"
className={cn(
"flex max-h-[50%] md:h-dvh md:max-h-full md:w-7/12 md:grow",
isDesktop && "md:mr-3",
)}
>
<div className="mx-auto flex size-full flex-row justify-center">
{cameraConfig &&

View File

@@ -23,6 +23,8 @@ import { LuExternalLink } from "react-icons/lu";
import { StatusBarMessagesContext } from "@/context/statusbar-provider";
import { Trans, useTranslation } from "react-i18next";
import { useDocDomain } from "@/hooks/use-doc-domain";
import { cn } from "@/lib/utils";
import { isDesktop } from "react-device-detect";
type MotionTunerViewProps = {
selectedCamera: string;
@@ -325,7 +327,12 @@ export default function MotionTunerView({
</div>
{cameraConfig ? (
<div className="flex max-h-[70%] md:mr-3 md:h-dvh md:max-h-full md:w-7/12 md:grow">
<div
className={cn(
"flex max-h-[70%] md:h-dvh md:max-h-full md:w-7/12 md:grow",
isDesktop && "md:mr-3",
)}
>
<div className="size-full min-h-10">
<AutoUpdatingCameraImage
camera={cameraConfig.name}

View File

@@ -43,6 +43,7 @@ import { useTriggers } from "@/api/ws";
import { useCameraFriendlyName } from "@/hooks/use-camera-friendly-name";
import { CiCircleAlert } from "react-icons/ci";
import { useDocDomain } from "@/hooks/use-doc-domain";
import { isDesktop } from "react-device-detect";
type ConfigSetBody = {
requires_restart: number;
@@ -440,7 +441,12 @@ export default function TriggerView({
return (
<div className="flex size-full flex-col md:flex-row">
<Toaster position="top-center" closeButton={true} />
<div className="scrollbar-container order-last mb-2 mt-2 flex h-full w-full flex-col overflow-y-auto pb-2 md:order-none md:mr-3 md:mt-0">
<div
className={cn(
"scrollbar-container order-last mb-2 mt-2 flex h-full w-full flex-col overflow-y-auto pb-2",
isDesktop && "order-none mr-3 mt-0",
)}
>
{!isSemanticSearchEnabled ? (
<div className="mb-5 flex flex-row items-center justify-between gap-2">
<div className="flex flex-col items-start">
@@ -651,7 +657,7 @@ export default function TriggerView({
</div>
{/* Desktop Table View */}
<div className="scrollbar-container hidden flex-1 overflow-hidden rounded-lg border border-border bg-background_alt md:mr-3 md:block">
<div className="scrollbar-container hidden flex-1 overflow-hidden rounded-lg border border-border bg-background_alt md:block">
<div className="h-full overflow-auto">
<Table>
<TableHeader className="sticky top-0 bg-muted/50">