diff --git a/src/RootApp.tsx b/src/RootApp.tsx
index cc10ca7baa..609cfc6fdd 100644
--- a/src/RootApp.tsx
+++ b/src/RootApp.tsx
@@ -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 (
<>
- {
- isExperimentalLayout ?
- :
+ {isExperimentalLayout ?
+ :
+
+
}
-
+
+
+
>
);
};
@@ -49,9 +50,7 @@ const RootApp = ({ history }: { history: History }) => (
-
-
-
+
diff --git a/src/RootAppRouter.tsx b/src/RootAppRouter.tsx
new file mode 100644
index 0000000000..ff04d82545
--- /dev/null
+++ b/src/RootAppRouter.tsx
@@ -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 ;
+}
diff --git a/src/apps/experimental/App.tsx b/src/apps/experimental/App.tsx
deleted file mode 100644
index b17e9054ee..0000000000
--- a/src/apps/experimental/App.tsx
+++ /dev/null
@@ -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 (
-
- }>
- {/* User routes */}
- }>
- {ASYNC_USER_ROUTES.map(toAsyncPageRoute)}
- {LEGACY_USER_ROUTES.map(toViewManagerPageRoute)}
-
-
- {/* Public routes */}
- }>
- } />
-
- {LEGACY_PUBLIC_ROUTES.map(toViewManagerPageRoute)}
-
-
-
- {/* Redirects for old paths */}
- {REDIRECTS.map(toRedirectRoute)}
-
- {/* Ignore dashboard routes */}
- {Object.entries(DASHBOARD_APP_PATHS).map(([ key, path ]) => (
-
- ))}
-
- );
-};
-
-export default ExperimentalApp;
diff --git a/src/apps/experimental/routes/routes.tsx b/src/apps/experimental/routes/routes.tsx
new file mode 100644
index 0000000000..09ef6d396f
--- /dev/null
+++ b/src/apps/experimental/routes/routes.tsx
@@ -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: ,
+ children: [
+ {
+ /* User routes: Any child route of this layout is authenticated */
+ element: ,
+ 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
+ }))
+];
diff --git a/src/components/router/AsyncRoute.tsx b/src/components/router/AsyncRoute.tsx
index 031e5700ea..aedd000867 100644
--- a/src/components/router/AsyncRoute.tsx
+++ b/src/components/router/AsyncRoute.tsx
@@ -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:
+ };
+}
diff --git a/src/components/router/LegacyRoute.tsx b/src/components/router/LegacyRoute.tsx
index bba780a513..e0a9df7417 100644
--- a/src/components/router/LegacyRoute.tsx
+++ b/src/components/router/LegacyRoute.tsx
@@ -19,3 +19,10 @@ export function toViewManagerPageRoute(route: LegacyRoute) {
/>
);
}
+
+export function toViewManagerPageRouteConfig(route: LegacyRoute) {
+ return {
+ path: route.path,
+ element:
+ };
+}
diff --git a/src/components/router/Redirect.tsx b/src/components/router/Redirect.tsx
index 7354f16c55..188493242a 100644
--- a/src/components/router/Redirect.tsx
+++ b/src/components/router/Redirect.tsx
@@ -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:
+ };
+}
diff --git a/src/hooks/useLegacyRouterSync.ts b/src/hooks/useLegacyRouterSync.ts
new file mode 100644
index 0000000000..fe28df0905
--- /dev/null
+++ b/src/hooks/useLegacyRouterSync.ts
@@ -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;
+ });
+ });
+ });
+}