Frontend fixes (#22309)

* prevent unnecessary reloads in useUserPersistence hook

* always render ProtectedRoute (handling undefined roles internally) and add Suspense fallback

* add missing i18n namespaces

react 19 enforces Suspense more strictly, so components using useTranslation() with unloaded namespaces would suspend, blanking the content behind the empty Suspense fallback

* add missing namespace

* remove unneeded

* remove modal from actions dropdown
This commit is contained in:
Josh Hawkins 2026-03-07 07:43:00 -06:00 committed by GitHub
parent dda9f7bfed
commit 889dfca36c
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
5 changed files with 38 additions and 25 deletions

View File

@ -11,7 +11,6 @@ import { Redirect } from "./components/navigation/Redirect";
import { cn } from "./lib/utils"; import { cn } from "./lib/utils";
import { isPWA } from "./utils/isPWA"; import { isPWA } from "./utils/isPWA";
import ProtectedRoute from "@/components/auth/ProtectedRoute"; import ProtectedRoute from "@/components/auth/ProtectedRoute";
import { AuthProvider } from "@/context/auth-context";
import useSWR from "swr"; import useSWR from "swr";
import { FrigateConfig } from "./types/frigateConfig"; import { FrigateConfig } from "./types/frigateConfig";
import ActivityIndicator from "@/components/indicators/activity-indicator"; import ActivityIndicator from "@/components/indicators/activity-indicator";
@ -39,13 +38,11 @@ function App() {
return ( return (
<Providers> <Providers>
<AuthProvider> <BrowserRouter basename={window.baseUrl}>
<BrowserRouter basename={window.baseUrl}> <Wrapper>
<Wrapper> {config?.safe_mode ? <SafeAppView /> : <DefaultAppView />}
{config?.safe_mode ? <SafeAppView /> : <DefaultAppView />} </Wrapper>
</Wrapper> </BrowserRouter>
</BrowserRouter>
</AuthProvider>
</Providers> </Providers>
); );
} }
@ -85,17 +82,13 @@ function DefaultAppView() {
: "bottom-8 left-[52px]", : "bottom-8 left-[52px]",
)} )}
> >
<Suspense> <Suspense
fallback={
<ActivityIndicator className="absolute left-1/2 top-1/2 -translate-x-1/2 -translate-y-1/2" />
}
>
<Routes> <Routes>
<Route <Route element={<ProtectedRoute requiredRoles={mainRouteRoles} />}>
element={
mainRouteRoles ? (
<ProtectedRoute requiredRoles={mainRouteRoles} />
) : (
<ActivityIndicator className="absolute left-1/2 top-1/2 -translate-x-1/2 -translate-y-1/2" />
)
}
>
<Route index element={<Live />} /> <Route index element={<Live />} />
<Route path="/review" element={<Events />} /> <Route path="/review" element={<Events />} />
<Route path="/explore" element={<Explore />} /> <Route path="/explore" element={<Explore />} />

View File

@ -10,7 +10,7 @@ import {
export default function ProtectedRoute({ export default function ProtectedRoute({
requiredRoles, requiredRoles,
}: { }: {
requiredRoles: string[]; requiredRoles?: string[];
}) { }) {
const { auth } = useContext(AuthContext); const { auth } = useContext(AuthContext);
@ -36,6 +36,13 @@ export default function ProtectedRoute({
); );
} }
// Wait for config to provide required roles
if (!requiredRoles) {
return (
<ActivityIndicator className="absolute left-1/2 top-1/2 -translate-x-1/2 -translate-y-1/2" />
);
}
if (auth.isLoading) { if (auth.isLoading) {
return ( return (
<ActivityIndicator className="absolute left-1/2 top-1/2 -translate-x-1/2 -translate-y-1/2" /> <ActivityIndicator className="absolute left-1/2 top-1/2 -translate-x-1/2 -translate-y-1/2" />

View File

@ -20,7 +20,7 @@ export default function ActionsDropdown({
const { t } = useTranslation(["components/dialog", "views/replay", "common"]); const { t } = useTranslation(["components/dialog", "views/replay", "common"]);
return ( return (
<DropdownMenu> <DropdownMenu modal={false}>
<DropdownMenuTrigger asChild> <DropdownMenuTrigger asChild>
<Button <Button
className="flex items-center gap-2" className="flex items-center gap-2"

View File

@ -116,6 +116,11 @@ export function useUserPersistence<S>(
return; return;
} }
// Skip reload if we're already loaded for this key
if (loadedKeyRef.current === namespacedKey) {
return;
}
// Reset state when key changes - this prevents stale writes // Reset state when key changes - this prevents stale writes
loadedKeyRef.current = null; loadedKeyRef.current = null;
migrationAttemptedRef.current = false; migrationAttemptedRef.current = false;

View File

@ -40,23 +40,31 @@ i18n
"common", "common",
"objects", "objects",
"audio", "audio",
"components/auth",
"components/camera", "components/camera",
"components/dialog", "components/dialog",
"components/filter", "components/filter",
"components/icons", "components/icons",
"components/input",
"components/player", "components/player",
"views/events",
"views/chat", "views/chat",
"views/classificationModel",
"views/configEditor",
"views/events",
"views/explore", "views/explore",
"views/exports",
"views/faceLibrary",
"views/live", "views/live",
"views/motionSearch",
"views/recording",
"views/replay",
"views/search",
"views/settings", "views/settings",
"views/system", "views/system",
"views/exports",
"views/explore",
"config/global",
"config/cameras", "config/cameras",
"config/validation", "config/global",
"config/groups", "config/groups",
"config/validation",
], ],
defaultNS: "common", defaultNS: "common",