mirror of
https://github.com/jellyfin/jellyfin-web
synced 2025-03-30 19:56:21 +00:00
Merge pull request #4834 from grhallenbeck/dev-react-router
Migrate React Router to React Data Router pattern
This commit is contained in:
commit
35429ca61f
17 changed files with 346 additions and 302 deletions
|
@ -71,6 +71,7 @@
|
||||||
- [scampower3](https://github.com/scampower3)
|
- [scampower3](https://github.com/scampower3)
|
||||||
- [LittleBigOwI] (https://github.com/LittleBigOwI/)
|
- [LittleBigOwI] (https://github.com/LittleBigOwI/)
|
||||||
- [Nate G](https://github.com/GGProGaming)
|
- [Nate G](https://github.com/GGProGaming)
|
||||||
|
- [Grady Hallenbeck](https://github.com/grhallenbeck)
|
||||||
|
|
||||||
# Emby Contributors
|
# Emby Contributors
|
||||||
|
|
||||||
|
|
|
@ -4,59 +4,35 @@ import { History } from '@remix-run/router';
|
||||||
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
|
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
|
||||||
import { ReactQueryDevtools } from '@tanstack/react-query-devtools';
|
import { ReactQueryDevtools } from '@tanstack/react-query-devtools';
|
||||||
import React from 'react';
|
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 { ApiProvider } from 'hooks/useApi';
|
||||||
import { WebConfigProvider } from 'hooks/useWebConfig';
|
import { WebConfigProvider } from 'hooks/useWebConfig';
|
||||||
import theme from 'themes/theme';
|
import theme from 'themes/theme';
|
||||||
|
|
||||||
const DashboardApp = loadable(() => import('./apps/dashboard/App'));
|
const StableAppRouter = loadable(() => import('./apps/stable/AppRouter'));
|
||||||
const ExperimentalApp = loadable(() => import('./apps/experimental/App'));
|
const RootAppRouter = loadable(() => import('./RootAppRouter'));
|
||||||
const StableApp = loadable(() => import('./apps/stable/App'));
|
|
||||||
|
|
||||||
const queryClient = new QueryClient();
|
const queryClient = new QueryClient();
|
||||||
|
|
||||||
const RootAppLayout = () => {
|
const RootApp = ({ history }: Readonly<{ history: History }>) => {
|
||||||
const layoutMode = localStorage.getItem('layout');
|
const layoutMode = localStorage.getItem('layout');
|
||||||
const isExperimentalLayout = layoutMode === 'experimental';
|
const isExperimentalLayout = layoutMode === 'experimental';
|
||||||
|
|
||||||
const location = useLocation();
|
|
||||||
const isNewLayoutPath = Object.values(DASHBOARD_APP_PATHS)
|
|
||||||
.some(path => location.pathname.startsWith(`/${path}`));
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
|
||||||
<Backdrop />
|
|
||||||
<AppHeader isHidden={isExperimentalLayout || isNewLayoutPath} />
|
|
||||||
|
|
||||||
{
|
|
||||||
isExperimentalLayout ?
|
|
||||||
<ExperimentalApp /> :
|
|
||||||
<StableApp />
|
|
||||||
}
|
|
||||||
|
|
||||||
<DashboardApp />
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
const RootApp = ({ history }: { history: History }) => (
|
|
||||||
<QueryClientProvider client={queryClient}>
|
<QueryClientProvider client={queryClient}>
|
||||||
<ApiProvider>
|
<ApiProvider>
|
||||||
<WebConfigProvider>
|
<WebConfigProvider>
|
||||||
<ThemeProvider theme={theme}>
|
<ThemeProvider theme={theme}>
|
||||||
<HistoryRouter history={history}>
|
{isExperimentalLayout ?
|
||||||
<RootAppLayout />
|
<RootAppRouter history={history} /> :
|
||||||
</HistoryRouter>
|
<StableAppRouter history={history} />
|
||||||
|
}
|
||||||
</ThemeProvider>
|
</ThemeProvider>
|
||||||
</WebConfigProvider>
|
</WebConfigProvider>
|
||||||
</ApiProvider>
|
</ApiProvider>
|
||||||
<ReactQueryDevtools initialIsOpen={false} />
|
<ReactQueryDevtools initialIsOpen={false} />
|
||||||
</QueryClientProvider>
|
</QueryClientProvider>
|
||||||
);
|
);
|
||||||
|
};
|
||||||
|
|
||||||
export default RootApp;
|
export default RootApp;
|
||||||
|
|
41
src/RootAppRouter.tsx
Normal file
41
src/RootAppRouter.tsx
Normal file
|
@ -0,0 +1,41 @@
|
||||||
|
|
||||||
|
import { History } from '@remix-run/router';
|
||||||
|
import React from 'react';
|
||||||
|
import {
|
||||||
|
RouterProvider,
|
||||||
|
createHashRouter,
|
||||||
|
Outlet
|
||||||
|
} from 'react-router-dom';
|
||||||
|
|
||||||
|
import { EXPERIMENTAL_APP_ROUTES } from 'apps/experimental/routes/routes';
|
||||||
|
import AppHeader from 'components/AppHeader';
|
||||||
|
import Backdrop from 'components/Backdrop';
|
||||||
|
import { useLegacyRouterSync } from 'hooks/useLegacyRouterSync';
|
||||||
|
import { DASHBOARD_APP_ROUTES } from 'apps/dashboard/routes/routes';
|
||||||
|
|
||||||
|
const router = createHashRouter([
|
||||||
|
{
|
||||||
|
element: <RootAppLayout />,
|
||||||
|
children: [
|
||||||
|
...EXPERIMENTAL_APP_ROUTES,
|
||||||
|
...DASHBOARD_APP_ROUTES
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]);
|
||||||
|
|
||||||
|
export default function RootAppRouter({ history }: Readonly<{ history: History}>) {
|
||||||
|
useLegacyRouterSync({ router, history });
|
||||||
|
|
||||||
|
return <RouterProvider router={router} />;
|
||||||
|
}
|
||||||
|
|
||||||
|
function RootAppLayout() {
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<Backdrop />
|
||||||
|
<AppHeader isHidden />
|
||||||
|
|
||||||
|
<Outlet />
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
|
@ -1,66 +0,0 @@
|
||||||
import loadable from '@loadable/component';
|
|
||||||
import React from 'react';
|
|
||||||
import { Route, Routes } from 'react-router-dom';
|
|
||||||
|
|
||||||
import ConnectionRequired from 'components/ConnectionRequired';
|
|
||||||
import { toViewManagerPageRoute } from 'components/router/LegacyRoute';
|
|
||||||
import { AsyncPageProps, AsyncRoute, toAsyncPageRoute } from 'components/router/AsyncRoute';
|
|
||||||
import { toRedirectRoute } from 'components/router/Redirect';
|
|
||||||
import ServerContentPage from 'components/ServerContentPage';
|
|
||||||
|
|
||||||
import AppLayout from './AppLayout';
|
|
||||||
import { REDIRECTS } from './routes/_redirects';
|
|
||||||
import { ASYNC_ADMIN_ROUTES } from './routes/_asyncRoutes';
|
|
||||||
import { LEGACY_ADMIN_ROUTES } from './routes/_legacyRoutes';
|
|
||||||
|
|
||||||
const DashboardAsyncPage = loadable(
|
|
||||||
(props: { page: string }) => import(/* webpackChunkName: "[request]" */ `./routes/${props.page}`),
|
|
||||||
{ cacheKey: (props: AsyncPageProps) => props.page }
|
|
||||||
);
|
|
||||||
|
|
||||||
const toDashboardAsyncPageRoute = (route: AsyncRoute) => (
|
|
||||||
toAsyncPageRoute({
|
|
||||||
...route,
|
|
||||||
element: DashboardAsyncPage
|
|
||||||
})
|
|
||||||
);
|
|
||||||
|
|
||||||
export const DASHBOARD_APP_PATHS = {
|
|
||||||
Dashboard: 'dashboard',
|
|
||||||
MetadataManager: 'metadata',
|
|
||||||
PluginConfig: 'configurationpage'
|
|
||||||
};
|
|
||||||
|
|
||||||
const DashboardApp = () => (
|
|
||||||
<Routes>
|
|
||||||
<Route element={<ConnectionRequired isAdminRequired />}>
|
|
||||||
<Route element={<AppLayout drawerlessPaths={[ DASHBOARD_APP_PATHS.MetadataManager ]} />}>
|
|
||||||
<Route path={DASHBOARD_APP_PATHS.Dashboard}>
|
|
||||||
{ASYNC_ADMIN_ROUTES.map(toDashboardAsyncPageRoute)}
|
|
||||||
{LEGACY_ADMIN_ROUTES.map(toViewManagerPageRoute)}
|
|
||||||
</Route>
|
|
||||||
|
|
||||||
{/* NOTE: The metadata editor might deserve a dedicated app in the future */}
|
|
||||||
{toViewManagerPageRoute({
|
|
||||||
path: DASHBOARD_APP_PATHS.MetadataManager,
|
|
||||||
pageProps: {
|
|
||||||
controller: 'edititemmetadata',
|
|
||||||
view: 'edititemmetadata.html'
|
|
||||||
}
|
|
||||||
})}
|
|
||||||
|
|
||||||
<Route path={DASHBOARD_APP_PATHS.PluginConfig} element={
|
|
||||||
<ServerContentPage view='/web/configurationpage' />
|
|
||||||
} />
|
|
||||||
</Route>
|
|
||||||
</Route>
|
|
||||||
|
|
||||||
{/* Suppress warnings for unhandled routes */}
|
|
||||||
<Route path='*' element={null} />
|
|
||||||
|
|
||||||
{/* Redirects for old paths */}
|
|
||||||
{REDIRECTS.map(toRedirectRoute)}
|
|
||||||
</Routes>
|
|
||||||
);
|
|
||||||
|
|
||||||
export default DashboardApp;
|
|
|
@ -1,12 +1,12 @@
|
||||||
import type { AsyncRoute } from 'components/router/AsyncRoute';
|
import { AsyncRouteType, type AsyncRoute } from 'components/router/AsyncRoute';
|
||||||
|
|
||||||
export const ASYNC_ADMIN_ROUTES: AsyncRoute[] = [
|
export const ASYNC_ADMIN_ROUTES: AsyncRoute[] = [
|
||||||
{ path: 'activity' },
|
{ path: 'activity', type: AsyncRouteType.Dashboard },
|
||||||
{ path: 'notifications' },
|
{ path: 'notifications', type: AsyncRouteType.Dashboard },
|
||||||
{ path: 'users' },
|
{ path: 'users', type: AsyncRouteType.Dashboard },
|
||||||
{ path: 'users/access' },
|
{ path: 'users/access', type: AsyncRouteType.Dashboard },
|
||||||
{ path: 'users/add' },
|
{ path: 'users/add', type: AsyncRouteType.Dashboard },
|
||||||
{ path: 'users/parentalcontrol' },
|
{ path: 'users/parentalcontrol', type: AsyncRouteType.Dashboard },
|
||||||
{ path: 'users/password' },
|
{ path: 'users/password', type: AsyncRouteType.Dashboard },
|
||||||
{ path: 'users/profile' }
|
{ path: 'users/profile', type: AsyncRouteType.Dashboard }
|
||||||
];
|
];
|
||||||
|
|
49
src/apps/dashboard/routes/routes.tsx
Normal file
49
src/apps/dashboard/routes/routes.tsx
Normal file
|
@ -0,0 +1,49 @@
|
||||||
|
import React from 'react';
|
||||||
|
import { RouteObject } from 'react-router-dom';
|
||||||
|
import AppLayout from '../AppLayout';
|
||||||
|
import ConnectionRequired from 'components/ConnectionRequired';
|
||||||
|
import { ASYNC_ADMIN_ROUTES } from './_asyncRoutes';
|
||||||
|
import { toAsyncPageRoute } from 'components/router/AsyncRoute';
|
||||||
|
import { toViewManagerPageRoute } from 'components/router/LegacyRoute';
|
||||||
|
import { LEGACY_ADMIN_ROUTES } from './_legacyRoutes';
|
||||||
|
import ServerContentPage from 'components/ServerContentPage';
|
||||||
|
|
||||||
|
export const DASHBOARD_APP_PATHS = {
|
||||||
|
Dashboard: 'dashboard',
|
||||||
|
MetadataManager: 'metadata',
|
||||||
|
PluginConfig: 'configurationpage'
|
||||||
|
};
|
||||||
|
|
||||||
|
export const DASHBOARD_APP_ROUTES: RouteObject[] = [
|
||||||
|
{
|
||||||
|
element: <ConnectionRequired isAdminRequired />,
|
||||||
|
children: [
|
||||||
|
{
|
||||||
|
element: <AppLayout drawerlessPaths={[ DASHBOARD_APP_PATHS.MetadataManager ]} />,
|
||||||
|
children: [
|
||||||
|
{
|
||||||
|
path: DASHBOARD_APP_PATHS.Dashboard,
|
||||||
|
children: [
|
||||||
|
...ASYNC_ADMIN_ROUTES.map(toAsyncPageRoute),
|
||||||
|
...LEGACY_ADMIN_ROUTES.map(toViewManagerPageRoute)
|
||||||
|
]
|
||||||
|
},
|
||||||
|
|
||||||
|
/* NOTE: The metadata editor might deserve a dedicated app in the future */
|
||||||
|
toViewManagerPageRoute({
|
||||||
|
path: DASHBOARD_APP_PATHS.MetadataManager,
|
||||||
|
pageProps: {
|
||||||
|
controller: 'edititemmetadata',
|
||||||
|
view: 'edititemmetadata.html'
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
|
||||||
|
{
|
||||||
|
path: DASHBOARD_APP_PATHS.PluginConfig,
|
||||||
|
element: <ServerContentPage view='/web/configurationpage' />
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
];
|
|
@ -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 { DASHBOARD_APP_PATHS } from 'apps/dashboard/routes/routes';
|
||||||
|
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 './asyncRoutes';
|
||||||
|
import { LEGACY_PUBLIC_ROUTES, LEGACY_USER_ROUTES } from './legacyRoutes';
|
||||||
|
|
||||||
|
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(toAsyncPageRoute),
|
||||||
|
...LEGACY_USER_ROUTES.map(toViewManagerPageRoute)
|
||||||
|
]
|
||||||
|
},
|
||||||
|
|
||||||
|
/* Public routes */
|
||||||
|
{ index: true, loader: () => redirect('/home.html') },
|
||||||
|
...LEGACY_PUBLIC_ROUTES.map(toViewManagerPageRoute)
|
||||||
|
]
|
||||||
|
},
|
||||||
|
|
||||||
|
/* Redirects for old paths */
|
||||||
|
...REDIRECTS.map(toRedirectRoute),
|
||||||
|
|
||||||
|
/* Ignore dashboard routes */
|
||||||
|
...Object.entries(DASHBOARD_APP_PATHS).map(([, path]) => ({
|
||||||
|
path: `/${path}/*`,
|
||||||
|
element: null
|
||||||
|
}))
|
||||||
|
];
|
|
@ -1,55 +0,0 @@
|
||||||
import React from 'react';
|
|
||||||
import { Navigate, Outlet, Route, Routes } from 'react-router-dom';
|
|
||||||
|
|
||||||
import { DASHBOARD_APP_PATHS } from 'apps/dashboard/App';
|
|
||||||
import AppBody from 'components/AppBody';
|
|
||||||
import ConnectionRequired from 'components/ConnectionRequired';
|
|
||||||
import { toAsyncPageRoute } from 'components/router/AsyncRoute';
|
|
||||||
import { toViewManagerPageRoute } from 'components/router/LegacyRoute';
|
|
||||||
import { toRedirectRoute } from 'components/router/Redirect';
|
|
||||||
|
|
||||||
import { ASYNC_USER_ROUTES } from './routes/asyncRoutes';
|
|
||||||
import { LEGACY_PUBLIC_ROUTES, LEGACY_USER_ROUTES } from './routes/legacyRoutes';
|
|
||||||
import { REDIRECTS } from './routes/_redirects';
|
|
||||||
|
|
||||||
const Layout = () => (
|
|
||||||
<AppBody>
|
|
||||||
<Outlet />
|
|
||||||
</AppBody>
|
|
||||||
);
|
|
||||||
|
|
||||||
const StableApp = () => (
|
|
||||||
<Routes>
|
|
||||||
<Route element={<Layout />}>
|
|
||||||
{/* User routes */}
|
|
||||||
<Route path='/' element={<ConnectionRequired />}>
|
|
||||||
{ASYNC_USER_ROUTES.map(toAsyncPageRoute)}
|
|
||||||
{LEGACY_USER_ROUTES.map(toViewManagerPageRoute)}
|
|
||||||
</Route>
|
|
||||||
|
|
||||||
{/* Public routes */}
|
|
||||||
<Route path='/' element={<ConnectionRequired isUserRequired={false} />}>
|
|
||||||
<Route index element={<Navigate replace to='/home.html' />} />
|
|
||||||
|
|
||||||
{LEGACY_PUBLIC_ROUTES.map(toViewManagerPageRoute)}
|
|
||||||
</Route>
|
|
||||||
|
|
||||||
{/* Suppress warnings for unhandled routes */}
|
|
||||||
<Route path='*' element={null} />
|
|
||||||
</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 StableApp;
|
|
24
src/apps/stable/AppLayout.tsx
Normal file
24
src/apps/stable/AppLayout.tsx
Normal file
|
@ -0,0 +1,24 @@
|
||||||
|
import React from 'react';
|
||||||
|
import { Outlet, useLocation } from 'react-router-dom';
|
||||||
|
|
||||||
|
import AppBody from 'components/AppBody';
|
||||||
|
import { DASHBOARD_APP_PATHS } from 'apps/dashboard/routes/routes';
|
||||||
|
import Backdrop from 'components/Backdrop';
|
||||||
|
import AppHeader from 'components/AppHeader';
|
||||||
|
|
||||||
|
export default function AppLayout() {
|
||||||
|
const location = useLocation();
|
||||||
|
const isNewLayoutPath = Object.values(DASHBOARD_APP_PATHS)
|
||||||
|
.some(path => location.pathname.startsWith(`/${path}`));
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<Backdrop />
|
||||||
|
<AppHeader isHidden={isNewLayoutPath} />
|
||||||
|
|
||||||
|
<AppBody>
|
||||||
|
<Outlet />
|
||||||
|
</AppBody>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
18
src/apps/stable/AppRouter.tsx
Normal file
18
src/apps/stable/AppRouter.tsx
Normal file
|
@ -0,0 +1,18 @@
|
||||||
|
import { History } from '@remix-run/router';
|
||||||
|
import React from 'react';
|
||||||
|
import { RouterProvider, createHashRouter } from 'react-router-dom';
|
||||||
|
|
||||||
|
import { DASHBOARD_APP_ROUTES } from 'apps/dashboard/routes/routes';
|
||||||
|
import { useLegacyRouterSync } from 'hooks/useLegacyRouterSync';
|
||||||
|
import { STABLE_APP_ROUTES } from './routes/routes';
|
||||||
|
|
||||||
|
const router = createHashRouter([
|
||||||
|
...STABLE_APP_ROUTES,
|
||||||
|
...DASHBOARD_APP_ROUTES
|
||||||
|
]);
|
||||||
|
|
||||||
|
export default function StableAppRouter({ history }: Readonly<{ history: History }>) {
|
||||||
|
useLegacyRouterSync({ router, history });
|
||||||
|
|
||||||
|
return <RouterProvider router={router} />;
|
||||||
|
}
|
46
src/apps/stable/routes/routes.tsx
Normal file
46
src/apps/stable/routes/routes.tsx
Normal file
|
@ -0,0 +1,46 @@
|
||||||
|
import { RouteObject, redirect } from 'react-router-dom';
|
||||||
|
import React from 'react';
|
||||||
|
|
||||||
|
import { DASHBOARD_APP_PATHS } from 'apps/dashboard/routes/routes';
|
||||||
|
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 { REDIRECTS } from './_redirects';
|
||||||
|
import { ASYNC_USER_ROUTES } from './asyncRoutes';
|
||||||
|
import { LEGACY_PUBLIC_ROUTES, LEGACY_USER_ROUTES } from './legacyRoutes';
|
||||||
|
|
||||||
|
export const STABLE_APP_ROUTES: RouteObject[] = [
|
||||||
|
{
|
||||||
|
path: '/*',
|
||||||
|
element: <AppLayout />,
|
||||||
|
children: [
|
||||||
|
{
|
||||||
|
/* User routes */
|
||||||
|
element: <ConnectionRequired isUserRequired />,
|
||||||
|
children: [
|
||||||
|
...ASYNC_USER_ROUTES.map(toAsyncPageRoute),
|
||||||
|
...LEGACY_USER_ROUTES.map(toViewManagerPageRoute)
|
||||||
|
]
|
||||||
|
},
|
||||||
|
|
||||||
|
/* Public routes */
|
||||||
|
{ index: true, loader: () => redirect('/home.html') },
|
||||||
|
...LEGACY_PUBLIC_ROUTES.map(toViewManagerPageRoute),
|
||||||
|
|
||||||
|
/* Suppress warnings for unhandled routes */
|
||||||
|
{ path: '*', element: null }
|
||||||
|
]
|
||||||
|
},
|
||||||
|
|
||||||
|
/* Redirects for old paths */
|
||||||
|
...REDIRECTS.map(toRedirectRoute),
|
||||||
|
|
||||||
|
/* Ignore dashboard routes */
|
||||||
|
...Object.entries(DASHBOARD_APP_PATHS).map(([, path]) => ({
|
||||||
|
path: `/${path}/*`,
|
||||||
|
element: null
|
||||||
|
}))
|
||||||
|
|
||||||
|
];
|
|
@ -1,10 +1,10 @@
|
||||||
import loadable, { LoadableComponent } from '@loadable/component';
|
import loadable, { LoadableComponent } from '@loadable/component';
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { Route } from 'react-router-dom';
|
|
||||||
|
|
||||||
export enum AsyncRouteType {
|
export enum AsyncRouteType {
|
||||||
Stable,
|
Stable,
|
||||||
Experimental
|
Experimental,
|
||||||
|
Dashboard,
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface AsyncRoute {
|
export interface AsyncRoute {
|
||||||
|
@ -26,6 +26,11 @@ export interface AsyncPageProps {
|
||||||
page: string
|
page: string
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const DashboardAsyncPage = loadable(
|
||||||
|
(props: { page: string }) => import(/* webpackChunkName: "[request]" */ `../../apps/dashboard/routes/${props.page}`),
|
||||||
|
{ cacheKey: (props: AsyncPageProps) => props.page }
|
||||||
|
);
|
||||||
|
|
||||||
const ExperimentalAsyncPage = loadable(
|
const ExperimentalAsyncPage = loadable(
|
||||||
(props: { page: string }) => import(/* webpackChunkName: "[request]" */ `../../apps/experimental/routes/${props.page}`),
|
(props: { page: string }) => import(/* webpackChunkName: "[request]" */ `../../apps/experimental/routes/${props.page}`),
|
||||||
{ cacheKey: (props: AsyncPageProps) => props.page }
|
{ cacheKey: (props: AsyncPageProps) => props.page }
|
||||||
|
@ -36,19 +41,24 @@ const StableAsyncPage = loadable(
|
||||||
{ cacheKey: (props: AsyncPageProps) => props.page }
|
{ cacheKey: (props: AsyncPageProps) => props.page }
|
||||||
);
|
);
|
||||||
|
|
||||||
export const toAsyncPageRoute = ({ path, page, element, type = AsyncRouteType.Stable }: AsyncRoute) => {
|
export function toAsyncPageRoute({ path, page, element, type = AsyncRouteType.Stable }: AsyncRoute) {
|
||||||
const Element = element
|
let Element = element;
|
||||||
|| (
|
if (!Element) {
|
||||||
type === AsyncRouteType.Experimental ?
|
switch (type) {
|
||||||
ExperimentalAsyncPage :
|
case AsyncRouteType.Dashboard:
|
||||||
StableAsyncPage
|
Element = DashboardAsyncPage;
|
||||||
);
|
break;
|
||||||
|
case AsyncRouteType.Experimental:
|
||||||
|
Element = ExperimentalAsyncPage;
|
||||||
|
break;
|
||||||
|
case AsyncRouteType.Stable:
|
||||||
|
default:
|
||||||
|
Element = StableAsyncPage;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return {
|
||||||
<Route
|
path,
|
||||||
key={path}
|
element: <Element page={page ?? path} />
|
||||||
path={path}
|
};
|
||||||
element={<Element page={page ?? path} />}
|
}
|
||||||
/>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
|
@ -1,48 +0,0 @@
|
||||||
import React, { useLayoutEffect } from 'react';
|
|
||||||
import { HistoryRouterProps, Router } from 'react-router-dom';
|
|
||||||
import { Update } from 'history';
|
|
||||||
|
|
||||||
/** Strips leading "!" from paths */
|
|
||||||
const normalizePath = (pathname: string) => pathname.replace(/^!/, '');
|
|
||||||
|
|
||||||
/**
|
|
||||||
* A slightly customized version of the HistoryRouter from react-router-dom.
|
|
||||||
* We need to use HistoryRouter to have a shared history state between react-router and appRouter, but it does not seem
|
|
||||||
* to be properly exported in the upstream package.
|
|
||||||
* We also needed some customizations to handle #! routes.
|
|
||||||
* Refs: https://github.com/remix-run/react-router/blob/v6.3.0/packages/react-router-dom/index.tsx#L222
|
|
||||||
*/
|
|
||||||
export function HistoryRouter({ basename, children, history }: HistoryRouterProps) {
|
|
||||||
const [state, setState] = React.useState<Update>({
|
|
||||||
action: history.action,
|
|
||||||
location: history.location
|
|
||||||
});
|
|
||||||
|
|
||||||
useLayoutEffect(() => {
|
|
||||||
const onHistoryChange = (update: Update) => {
|
|
||||||
if (update.location.pathname.startsWith('!')) {
|
|
||||||
// When the location changes, we need to check for #! paths and replace the location with the "!" stripped
|
|
||||||
history.replace(normalizePath(update.location.pathname), update.location.state);
|
|
||||||
} else {
|
|
||||||
setState(update);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
history.listen(onHistoryChange);
|
|
||||||
}, [ history ]);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Router
|
|
||||||
basename={basename}
|
|
||||||
// eslint-disable-next-line react/no-children-prop
|
|
||||||
children={children}
|
|
||||||
location={{
|
|
||||||
...state.location,
|
|
||||||
// The original location does not get replaced with the normalized version, so we need to strip it here
|
|
||||||
pathname: normalizePath(state.location.pathname)
|
|
||||||
}}
|
|
||||||
navigationType={state.action}
|
|
||||||
navigator={history}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
}
|
|
|
@ -1,5 +1,4 @@
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { Route } from 'react-router-dom';
|
|
||||||
|
|
||||||
import ViewManagerPage, { ViewManagerPageProps } from '../viewManager/ViewManagerPage';
|
import ViewManagerPage, { ViewManagerPageProps } from '../viewManager/ViewManagerPage';
|
||||||
|
|
||||||
|
@ -9,13 +8,8 @@ export interface LegacyRoute {
|
||||||
}
|
}
|
||||||
|
|
||||||
export function toViewManagerPageRoute(route: LegacyRoute) {
|
export function toViewManagerPageRoute(route: LegacyRoute) {
|
||||||
return (
|
return {
|
||||||
<Route
|
path: route.path,
|
||||||
key={route.path}
|
element: <ViewManagerPage {...route.pageProps} />
|
||||||
path={route.path}
|
};
|
||||||
element={
|
|
||||||
<ViewManagerPage {...route.pageProps} />
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { Navigate, Route, useLocation } from 'react-router-dom';
|
import { Navigate, RouteObject, useLocation } from 'react-router-dom';
|
||||||
|
|
||||||
export interface Redirect {
|
export interface Redirect {
|
||||||
from: string
|
from: string
|
||||||
|
@ -17,12 +17,9 @@ const RedirectWithSearch = ({ to }: { to: string }) => {
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
export function toRedirectRoute({ from, to }: Redirect) {
|
export function toRedirectRoute({ from, to }: Redirect): RouteObject {
|
||||||
return (
|
return {
|
||||||
<Route
|
path: from,
|
||||||
key={from}
|
element: <RedirectWithSearch to={to} />
|
||||||
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