diff --git a/apps/web/src/App.tsx b/apps/web/src/App.tsx index 80151a3a..ab125a85 100644 --- a/apps/web/src/App.tsx +++ b/apps/web/src/App.tsx @@ -13,8 +13,9 @@ import { import { FeedbackWidget } from "./components/feedback"; import { ProtectedAppLayout } from "./components/layout/ProtectedAppLayout"; import { KeyboardShortcutsModal } from "./components/settings"; -import { PageSkeleton } from "./components/ui"; +import { PageSkeleton, type PageSkeletonVariant } from "./components/ui"; import { OnboardingLoadingScreen } from "./components/ui/onboarding-state"; +import { WorkspaceChooserPage } from "./pages/WorkspaceChooserPage"; const LoginPage = lazy(() => import("./pages/LoginPage").then((m) => ({ default: m.LoginPage })), @@ -42,11 +43,6 @@ const CompanySetupPage = lazy(() => default: m.CompanySetupPage, })), ); -const WorkspaceChooserPage = lazy(() => - import("./pages/WorkspaceChooserPage").then((m) => ({ - default: m.WorkspaceChooserPage, - })), -); const OAuthConsentPage = lazy(() => import("./pages/OAuthConsentPage").then((m) => ({ default: m.OAuthConsentPage, @@ -86,7 +82,7 @@ function LazyPage({ variant = "default", }: { children: ReactNode; - variant?: "auth" | "chat" | "team" | "settings" | "dashboard" | "default"; + variant?: PageSkeletonVariant; }) { return ( - - - + } /> diff --git a/apps/web/src/components/auth/ProtectedRoute.test.tsx b/apps/web/src/components/auth/ProtectedRoute.test.tsx index d01ec965..b743c6f7 100644 --- a/apps/web/src/components/auth/ProtectedRoute.test.tsx +++ b/apps/web/src/components/auth/ProtectedRoute.test.tsx @@ -26,6 +26,7 @@ let auth: AuthStub = { }; const navigateTargets: string[] = []; +let pathname = "/inbox"; // `mock.module` replaces a module for every test file that runs after this one // in the same process, so each stub keeps the real exports and overrides only @@ -67,15 +68,17 @@ mock.module("react-router", () => ({ navigateTargets.push(to); return null; }, - useLocation: () => ({ pathname: "/inbox" }), + useLocation: () => ({ pathname }), })); const { ProtectedRoute } = await import("./ProtectedRoute"); -function render(): string { +function render( + workspaceMode: "required" | "chooser" | "setup" = "required", +): string { navigateTargets.length = 0; return renderToStaticMarkup( - +

protected content

, ); @@ -106,4 +109,15 @@ describe("ProtectedRoute session handling", () => { expect(navigateTargets).toEqual([]); expect(markup).toContain("protected content"); }); + + test("keeps the workspace chooser visible while the session restores", () => { + pathname = "/workspaces"; + auth = { ...auth, isLoading: true }; + const markup = render("chooser"); + + expect(navigateTargets).toEqual([]); + expect(markup).toContain('aria-label="Loading your workspaces…"'); + expect(markup).toContain("WATeamInbox"); + expect(markup).not.toContain("We’re restoring your session"); + }); }); diff --git a/apps/web/src/components/auth/ProtectedRoute.tsx b/apps/web/src/components/auth/ProtectedRoute.tsx index bca0181f..9b33f5d5 100644 --- a/apps/web/src/components/auth/ProtectedRoute.tsx +++ b/apps/web/src/components/auth/ProtectedRoute.tsx @@ -56,6 +56,9 @@ export function ProtectedRoute({ /> ); } + if (location.pathname === "/workspaces") { + return ; + } return ( ; case "team": return ; + case "workspace-chooser": + return ; default: return ; } @@ -384,6 +388,65 @@ function DashboardPageSkeleton({ className }: { className?: string }) { ); } +/** + * Workspace chooser skeleton - preserves the chooser's frame while auth and + * memberships restore. This route used to borrow the onboarding/auth loading + * screen, which looked like a brief jump back to login before the chooser + * appeared. + */ +function WorkspaceChooserSkeleton({ className }: { className?: string }) { + const { t } = useTranslation(); + + return ( +
+
+ ); +} + /** * Auth page skeleton - centered form card */