115 lines
3.9 KiB
TypeScript
115 lines
3.9 KiB
TypeScript
import {
|
|
useEffect,
|
|
useMemo,
|
|
useRef,
|
|
useState,
|
|
type MouseEvent
|
|
} from 'react';
|
|
import { useDashboardHealthQuery } from './api';
|
|
import { AppShell } from './app/AppShell';
|
|
import { ScreenContent } from './app/ScreenContent';
|
|
import {
|
|
routeFromLocation,
|
|
routeHref,
|
|
type ScreenRoute
|
|
} from './app/routes';
|
|
import {
|
|
applyDesignSystemTheme,
|
|
getStoredDesignSystemTheme,
|
|
type DesignSystemThemeId
|
|
} from './designSystem';
|
|
import { canLeaveCurrentScreen } from './navigationGuard';
|
|
import { PrimaryActionProvider } from './primaryAction';
|
|
|
|
export function App() {
|
|
const [theme, setTheme] = useState<DesignSystemThemeId>(() => getStoredDesignSystemTheme());
|
|
const [activeRoute, setActiveRoute] = useState<ScreenRoute | null>(() => routeFromLocation());
|
|
const healthState = useDashboardHealthQuery();
|
|
|
|
// The guarded `libraries` route's approved sub-path. App owns this (rather than the wrapper
|
|
// self-listening for popstate) so the dirty guard is consulted BEFORE the wrapper switches
|
|
// sub-screen (design §D.2, finding 4). Scoped to the libraries route only: the write is gated on
|
|
// the arrived route being 'libraries' so Playouts/Media sub-path pops stay byte-identical — they
|
|
// still bail App's setActiveRoute via Object.is and own their own pathname.
|
|
const [librariesSubPath, setLibrariesSubPath] = useState<string>(() => window.location.pathname);
|
|
|
|
// The last path the guard has approved us being on. Browser Back/Forward (popstate) cannot be
|
|
// cancelled, so when the dirty guard vetoes a pop we re-push THIS path to undo the browser's URL
|
|
// change. Kept in a ref (not state) so the popstate handler reads it synchronously at event time.
|
|
const currentPathRef = useRef<string>(window.location.pathname);
|
|
|
|
useEffect(() => {
|
|
applyDesignSystemTheme(theme);
|
|
}, [theme]);
|
|
|
|
useEffect(() => {
|
|
// popstate covers both real Back/Forward and the synthetic pop navigateToPath() dispatches. If a
|
|
// screen has registered a dirty guard and it vetoes, the URL has already moved, so restore the
|
|
// last approved path and leave activeRoute untouched.
|
|
const onPopState = () => {
|
|
if (!canLeaveCurrentScreen()) {
|
|
window.history.pushState(null, '', currentPathRef.current);
|
|
return;
|
|
}
|
|
|
|
currentPathRef.current = window.location.pathname;
|
|
const nextRoute = routeFromLocation();
|
|
setActiveRoute(nextRoute);
|
|
|
|
// Guarded sub-path route: update the wrapper only after navigation is approved. Unguarded
|
|
// routes (Playouts/Media) skip this write and keep their screen-owned pathname listeners.
|
|
if (nextRoute?.id === 'libraries') {
|
|
setLibrariesSubPath(window.location.pathname);
|
|
}
|
|
};
|
|
|
|
window.addEventListener('popstate', onPopState);
|
|
return () => window.removeEventListener('popstate', onPopState);
|
|
}, []);
|
|
|
|
const route = useMemo(() => activeRoute, [activeRoute]);
|
|
|
|
const navigate = (nextRoute: ScreenRoute, event: MouseEvent) => {
|
|
if (event.metaKey || event.ctrlKey || event.shiftKey || event.button !== 0) {
|
|
return;
|
|
}
|
|
|
|
event.preventDefault();
|
|
|
|
if (nextRoute.id === route?.id) {
|
|
return;
|
|
}
|
|
|
|
if (!canLeaveCurrentScreen()) {
|
|
return;
|
|
}
|
|
|
|
window.history.pushState(null, '', routeHref(nextRoute));
|
|
currentPathRef.current = window.location.pathname;
|
|
setActiveRoute(nextRoute);
|
|
|
|
// A sidebar click into Libraries always targets the hub, so clear any previously-owned editor path.
|
|
if (nextRoute.id === 'libraries') {
|
|
setLibrariesSubPath(window.location.pathname);
|
|
}
|
|
};
|
|
|
|
return (
|
|
<PrimaryActionProvider>
|
|
<AppShell
|
|
healthState={healthState}
|
|
onNavigate={navigate}
|
|
onThemeChange={setTheme}
|
|
route={route}
|
|
theme={theme}
|
|
>
|
|
<ScreenContent
|
|
healthState={healthState}
|
|
librariesSubPath={librariesSubPath}
|
|
route={route}
|
|
/>
|
|
</AppShell>
|
|
</PrimaryActionProvider>
|
|
);
|
|
}
|