mirror of
https://github.com/jellyfin/jellyfin-web
synced 2025-03-30 19:56:21 +00:00
feat: migrate experimental app to use react data router
This commit is contained in:
parent
06b991ddcf
commit
675f9625f2
8 changed files with 162 additions and 62 deletions
|
@ -4,42 +4,43 @@ import { History } from '@remix-run/router';
|
|||
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
|
||||
import { ReactQueryDevtools } from '@tanstack/react-query-devtools';
|
||||
import React from 'react';
|
||||
import { useLocation } from 'react-router-dom';
|
||||
|
||||
import { DASHBOARD_APP_PATHS } from 'apps/dashboard/App';
|
||||
import AppHeader from 'components/AppHeader';
|
||||
import Backdrop from 'components/Backdrop';
|
||||
import { HistoryRouter } from 'components/router/HistoryRouter';
|
||||
import { ApiProvider } from 'hooks/useApi';
|
||||
import { WebConfigProvider } from 'hooks/useWebConfig';
|
||||
import theme from 'themes/theme';
|
||||
import { HistoryRouter } from 'components/router/HistoryRouter';
|
||||
|
||||
const DashboardApp = loadable(() => import('./apps/dashboard/App'));
|
||||
const ExperimentalApp = loadable(() => import('./apps/experimental/App'));
|
||||
const StableApp = loadable(() => import('./apps/stable/App'));
|
||||
const RootAppRouter = loadable(() => import('./RootAppRouter'));
|
||||
|
||||
const queryClient = new QueryClient();
|
||||
|
||||
const RootAppLayout = () => {
|
||||
const RootAppLayout = ({ history }: { history: History }) => {
|
||||
const layoutMode = localStorage.getItem('layout');
|
||||
const isExperimentalLayout = layoutMode === 'experimental';
|
||||
|
||||
const location = useLocation();
|
||||
const isNewLayoutPath = Object.values(DASHBOARD_APP_PATHS)
|
||||
.some(path => location.pathname.startsWith(`/${path}`));
|
||||
.some(path => window.location.pathname.startsWith(`/${path}`));
|
||||
|
||||
return (
|
||||
<>
|
||||
<Backdrop />
|
||||
<AppHeader isHidden={isExperimentalLayout || isNewLayoutPath} />
|
||||
|
||||
{
|
||||
isExperimentalLayout ?
|
||||
<ExperimentalApp /> :
|
||||
{isExperimentalLayout ?
|
||||
<RootAppRouter history={history} /> :
|
||||
<HistoryRouter history={history}>
|
||||
<StableApp />
|
||||
</HistoryRouter>
|
||||
}
|
||||
|
||||
<DashboardApp />
|
||||
<HistoryRouter history={history}>
|
||||
<DashboardApp />
|
||||
</HistoryRouter>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
@ -49,9 +50,7 @@ const RootApp = ({ history }: { history: History }) => (
|
|||
<ApiProvider>
|
||||
<WebConfigProvider>
|
||||
<ThemeProvider theme={theme}>
|
||||
<HistoryRouter history={history}>
|
||||
<RootAppLayout />
|
||||
</HistoryRouter>
|
||||
<RootAppLayout history={history} />
|
||||
</ThemeProvider>
|
||||
</WebConfigProvider>
|
||||
</ApiProvider>
|
||||
|
|
17
src/RootAppRouter.tsx
Normal file
17
src/RootAppRouter.tsx
Normal file
|
@ -0,0 +1,17 @@
|
|||
|
||||
import { History } from '@remix-run/router';
|
||||
import React from 'react';
|
||||
import { RouterProvider, createHashRouter } from 'react-router-dom';
|
||||
|
||||
import { EXPERIMENTAL_APP_ROUTES } from 'apps/experimental/routes/routes';
|
||||
import { useLegacyRouterSync } from 'hooks/useLegacyRouterSync';
|
||||
|
||||
const router = createHashRouter([
|
||||
...EXPERIMENTAL_APP_ROUTES
|
||||
]);
|
||||
|
||||
export default function RootAppRouter({ history }: { history: History}) {
|
||||
useLegacyRouterSync({ router, history });
|
||||
|
||||
return <RouterProvider router={router} />;
|
||||
}
|
|
@ -1,48 +0,0 @@
|
|||
import React from 'react';
|
||||
import { Navigate, Route, Routes } from 'react-router-dom';
|
||||
|
||||
import { DASHBOARD_APP_PATHS } from 'apps/dashboard/App';
|
||||
import { REDIRECTS } from 'apps/stable/routes/_redirects';
|
||||
import ConnectionRequired from 'components/ConnectionRequired';
|
||||
import { toAsyncPageRoute } from 'components/router/AsyncRoute';
|
||||
import { toViewManagerPageRoute } from 'components/router/LegacyRoute';
|
||||
import { toRedirectRoute } from 'components/router/Redirect';
|
||||
|
||||
import AppLayout from './AppLayout';
|
||||
import { ASYNC_USER_ROUTES } from './routes/asyncRoutes';
|
||||
import { LEGACY_PUBLIC_ROUTES, LEGACY_USER_ROUTES } from './routes/legacyRoutes';
|
||||
|
||||
const ExperimentalApp = () => {
|
||||
return (
|
||||
<Routes>
|
||||
<Route path='/*' element={<AppLayout />}>
|
||||
{/* User routes */}
|
||||
<Route element={<ConnectionRequired />}>
|
||||
{ASYNC_USER_ROUTES.map(toAsyncPageRoute)}
|
||||
{LEGACY_USER_ROUTES.map(toViewManagerPageRoute)}
|
||||
</Route>
|
||||
|
||||
{/* Public routes */}
|
||||
<Route element={<ConnectionRequired isUserRequired={false} />}>
|
||||
<Route index element={<Navigate replace to='/home.html' />} />
|
||||
|
||||
{LEGACY_PUBLIC_ROUTES.map(toViewManagerPageRoute)}
|
||||
</Route>
|
||||
</Route>
|
||||
|
||||
{/* Redirects for old paths */}
|
||||
{REDIRECTS.map(toRedirectRoute)}
|
||||
|
||||
{/* Ignore dashboard routes */}
|
||||
{Object.entries(DASHBOARD_APP_PATHS).map(([ key, path ]) => (
|
||||
<Route
|
||||
key={key}
|
||||
path={`/${path}/*`}
|
||||
element={null}
|
||||
/>
|
||||
))}
|
||||
</Routes>
|
||||
);
|
||||
};
|
||||
|
||||
export default ExperimentalApp;
|
42
src/apps/experimental/routes/routes.tsx
Normal file
42
src/apps/experimental/routes/routes.tsx
Normal file
|
@ -0,0 +1,42 @@
|
|||
import React from 'react';
|
||||
import { RouteObject, redirect } from 'react-router-dom';
|
||||
|
||||
import { REDIRECTS } from 'apps/dashboard/routes/_redirects';
|
||||
import ConnectionRequired from 'components/ConnectionRequired';
|
||||
import { toAsyncPageRouteConfig } from 'components/router/AsyncRoute';
|
||||
import { toViewManagerPageRouteConfig } from 'components/router/LegacyRoute';
|
||||
import { toRedirectRouteConfig } from 'components/router/Redirect';
|
||||
import AppLayout from '../AppLayout';
|
||||
import { ASYNC_USER_ROUTES } from './asyncRoutes';
|
||||
import { LEGACY_PUBLIC_ROUTES, LEGACY_USER_ROUTES } from './legacyRoutes';
|
||||
import { DASHBOARD_APP_PATHS } from 'apps/dashboard/App';
|
||||
|
||||
export const EXPERIMENTAL_APP_ROUTES: RouteObject[] = [
|
||||
{
|
||||
path: '/*',
|
||||
element: <AppLayout />,
|
||||
children: [
|
||||
{
|
||||
/* User routes: Any child route of this layout is authenticated */
|
||||
element: <ConnectionRequired isUserRequired />,
|
||||
children: [
|
||||
...ASYNC_USER_ROUTES.map(toAsyncPageRouteConfig),
|
||||
...LEGACY_USER_ROUTES.map(toViewManagerPageRouteConfig)
|
||||
]
|
||||
},
|
||||
|
||||
/* Public routes */
|
||||
{ index: true, loader: () => redirect('/home.html') },
|
||||
...LEGACY_PUBLIC_ROUTES.map(toViewManagerPageRouteConfig)
|
||||
]
|
||||
},
|
||||
|
||||
/* Redirects for old paths */
|
||||
...REDIRECTS.map(toRedirectRouteConfig),
|
||||
|
||||
/* Ignore dashboard routes */
|
||||
...Object.entries(DASHBOARD_APP_PATHS).map(([, path]) => ({
|
||||
path: `/${path}/*`,
|
||||
element: null
|
||||
}))
|
||||
];
|
|
@ -52,3 +52,16 @@ export const toAsyncPageRoute = ({ path, page, element, type = AsyncRouteType.St
|
|||
/>
|
||||
);
|
||||
};
|
||||
|
||||
export function toAsyncPageRouteConfig({ path, page, element, type = AsyncRouteType.Stable }: AsyncRoute) {
|
||||
const Element = element || (
|
||||
type === AsyncRouteType.Experimental ?
|
||||
ExperimentalAsyncPage :
|
||||
StableAsyncPage
|
||||
);
|
||||
|
||||
return {
|
||||
path,
|
||||
element: <Element page={page ?? path} />
|
||||
};
|
||||
}
|
||||
|
|
|
@ -19,3 +19,10 @@ export function toViewManagerPageRoute(route: LegacyRoute) {
|
|||
/>
|
||||
);
|
||||
}
|
||||
|
||||
export function toViewManagerPageRouteConfig(route: LegacyRoute) {
|
||||
return {
|
||||
path: route.path,
|
||||
element: <ViewManagerPage {...route.pageProps} />
|
||||
};
|
||||
}
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
import React from 'react';
|
||||
import { Navigate, Route, useLocation } from 'react-router-dom';
|
||||
import { Navigate, Route, RouteObject, useLocation } from 'react-router-dom';
|
||||
|
||||
export interface Redirect {
|
||||
from: string
|
||||
|
@ -26,3 +26,10 @@ export function toRedirectRoute({ from, to }: Redirect) {
|
|||
/>
|
||||
);
|
||||
}
|
||||
|
||||
export function toRedirectRouteConfig({ from, to }: Redirect): RouteObject {
|
||||
return {
|
||||
path: from,
|
||||
element: <RedirectWithSearch to={to} />
|
||||
};
|
||||
}
|
||||
|
|
63
src/hooks/useLegacyRouterSync.ts
Normal file
63
src/hooks/useLegacyRouterSync.ts
Normal file
|
@ -0,0 +1,63 @@
|
|||
import { Update } from 'history';
|
||||
import { useLayoutEffect, useState } from 'react';
|
||||
import type { History, Router } from '@remix-run/router';
|
||||
|
||||
const normalizePath = (pathname: string) => pathname.replace(/^!/, '');
|
||||
|
||||
interface UseLegacyRouterSyncProps {
|
||||
router: Router;
|
||||
history: History;
|
||||
}
|
||||
export function useLegacyRouterSync({ router, history }: UseLegacyRouterSyncProps) {
|
||||
const [routerLocation, setRouterLocation] = useState(router.state.location);
|
||||
|
||||
useLayoutEffect(() => {
|
||||
const onHistoryChange = async (update: Update) => {
|
||||
const isSynced = router.createHref(router.state.location) === router.createHref(update.location);
|
||||
|
||||
/**
|
||||
* Some legacy codepaths may still use the `#!` routing scheme which is unsupported with the React routing
|
||||
* implementation, so we need to remove the leading `!` from the pathname. React Router already removes the
|
||||
* hash for us.
|
||||
*/
|
||||
if (update.location.pathname.startsWith('!')) {
|
||||
history.replace(normalizePath(update.location.pathname), update.location.state);
|
||||
} else if (!isSynced) {
|
||||
await router.navigate(update.location, { replace: true });
|
||||
}
|
||||
};
|
||||
|
||||
const unlisten = history.listen(onHistoryChange);
|
||||
|
||||
return () => {
|
||||
unlisten();
|
||||
};
|
||||
}, [history, router]);
|
||||
|
||||
/**
|
||||
* Because the router subscription needs to be in a zero-dependencies effect, syncing changes to the router back to
|
||||
* the legacy history API needs to be in a separate effect. This should run any time the router location changes.
|
||||
*/
|
||||
useLayoutEffect(() => {
|
||||
const isSynced = router.createHref(routerLocation) === router.createHref(history.location);
|
||||
if (!isSynced) {
|
||||
history.replace(routerLocation);
|
||||
}
|
||||
}, [history, router, routerLocation]);
|
||||
|
||||
/**
|
||||
* We want to use an effect with no dependencies here when we set up the router subscription to ensure that we only
|
||||
* subscribe to the router state once. The router doesn't provide a way to remove subscribers, so we need to be
|
||||
* careful to not create multiple subscribers.
|
||||
*/
|
||||
useLayoutEffect(() => {
|
||||
router.subscribe((newState) => {
|
||||
setRouterLocation((prevLocation) => {
|
||||
if (newState.location !== prevLocation) {
|
||||
return newState.location;
|
||||
}
|
||||
return prevLocation;
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
Loading…
Add table
Add a link
Reference in a new issue