From 25b5d5d0a783311ae5fe584d47d76692154a22ec Mon Sep 17 00:00:00 2001 From: "anthropic-code-agent[bot]" <242468646+Claude@users.noreply.github.com> Date: Tue, 21 Apr 2026 14:34:57 +0000 Subject: [PATCH] feat: remove GlobalSidebar and implement auto-enter environment flow Agent-Logs-Url: https://github.com/objectstack-ai/framework/sessions/88dcfe26-ab72-4c22-86fb-3bd482a172e9 Co-authored-by: os-zhuang <277994282+os-zhuang@users.noreply.github.com> --- apps/studio/src/routes/__root.tsx | 31 +---------- .../routes/environments.$environmentId.tsx | 21 +++++--- apps/studio/src/routes/index.tsx | 51 ++++++++++++++----- apps/studio/src/routes/login.tsx | 29 +++++++++-- 4 files changed, 75 insertions(+), 57 deletions(-) diff --git a/apps/studio/src/routes/__root.tsx b/apps/studio/src/routes/__root.tsx index 5bd9b94d2..15d8367f7 100644 --- a/apps/studio/src/routes/__root.tsx +++ b/apps/studio/src/routes/__root.tsx @@ -8,7 +8,6 @@ import { SidebarProvider } from '@/components/ui/sidebar'; import { Toaster } from '@/components/ui/toaster'; import { AiChatPanel } from '@/components/AiChatPanel'; import { ProductionGuardProvider } from '@/components/production-guard'; -import { GlobalSidebar } from '@/components/global-sidebar'; import { TopBar } from '@/components/top-bar'; import { PluginRegistryProvider } from '../plugins'; import { builtInPlugins } from '../plugins/built-in'; @@ -20,31 +19,6 @@ import type { InstalledPackage } from '@objectstack/spec/kernel'; /** Routes that don't require authentication. */ const PUBLIC_ROUTES = new Set(['/login', '/register']); -/** - * Returns true when the current route should render the GlobalSidebar - * (the top-level nav shell) rather than the package-scoped AppSidebar. - * - * Package-scoped routes (`/$package/*` and `/environments/:id/:package/*`) - * have their own AppSidebar injected by their layout component, so the - * GlobalSidebar must be suppressed there to avoid rendering two sidebars. - */ -function isGlobalShellPath(pathname: string): boolean { - if (PUBLIC_ROUTES.has(pathname)) return false; - // Once the user drills into a package under an environment - // (`/environments/:envId/:package/*` where :package is NOT the reserved - // `packages` segment), the package-scoped AppSidebar takes over. - if (/^\/environments\/[^/]+\/(?!packages(?:\/|$))[^/]+/.test(pathname)) { - return false; - } - // /environments (list), /environments/:envId (overview), and - // /environments/:envId/packages (package management) all keep GlobalSidebar. - const globalPrefixes = ['/orgs', '/environments', '/packages', '/api-console']; - if (pathname === '/') return true; - return globalPrefixes.some( - (p) => pathname === p || pathname.startsWith(p + '/'), - ); -} - /** * Routes where an environment selection is NOT required. * Everything under /environments (list + detail), org mgmt, auth pages. @@ -134,10 +108,8 @@ function RequireAuth({ children }: { children: React.ReactNode }) { return null; } - // Authenticated layout with TopBar + Sidebar + Content + // Authenticated layout with TopBar + Content if (user) { - const showGlobalSidebar = isGlobalShellPath(location.pathname); - return (
@@ -147,7 +119,6 @@ function RequireAuth({ children }: { children: React.ReactNode }) { onSelectPackage={handleSelectPackage} />
- {showGlobalSidebar && }
{children}
diff --git a/apps/studio/src/routes/environments.$environmentId.tsx b/apps/studio/src/routes/environments.$environmentId.tsx index 6180a4951..9d378f542 100644 --- a/apps/studio/src/routes/environments.$environmentId.tsx +++ b/apps/studio/src/routes/environments.$environmentId.tsx @@ -11,9 +11,11 @@ * {@link useEnvironmentDetail}, causing every downstream API request to * carry the `X-Environment-Id` header. * - Render the package-scoped {@link AppSidebar} (metadata tree) for ALL - * routes under `/environments/:envId/*` — overview, package management, - * and the package workspace — so the user sees the metadata list as soon - * as they select an environment. + * routes under `/environments/:envId/*` EXCEPT `/packages` (the package + * management page), where the sidebar would compete with the page content. + * - When no package is selected (URL has no `:package` segment), AppSidebar + * renders environment-wide metadata by passing `packageId: undefined` to + * `client.meta.getItems`. * - Redirect back to `/environments` when the environment cannot be loaded. */ @@ -112,15 +114,18 @@ function EnvironmentLayoutComponent() { } }, [error, navigate]); - // Only render the package-scoped AppSidebar once the user has drilled into - // a specific package. Environment overview and the packages management page - // continue to render the GlobalSidebar from `routes/__root.tsx`. + // Reserved child segments that do NOT get the metadata AppSidebar. + // /packages is the package-management surface; rendering the tree there + // would compete with the page's own content. Everything else — overview + // and the package workspace — shows the sidebar. + const hideSidebar = location.pathname.endsWith(`/environments/${environmentId}/packages`); + return ( <> - {activePackageId && ( + {!hideSidebar && ( diff --git a/apps/studio/src/routes/index.tsx b/apps/studio/src/routes/index.tsx index 543608652..24b2b7a3b 100644 --- a/apps/studio/src/routes/index.tsx +++ b/apps/studio/src/routes/index.tsx @@ -1,24 +1,47 @@ // Copyright (c) 2025 ObjectStack. Licensed under the Apache-2.0 license. -import { createFileRoute } from '@tanstack/react-router'; -import { DeveloperOverview } from '../components/DeveloperOverview'; -import { usePackages } from '../hooks/usePackages'; +import { createFileRoute, useNavigate } from '@tanstack/react-router'; +import { useEffect } from 'react'; +import { useSession } from '@/hooks/useSession'; +import { useEnvironments } from '@/hooks/useEnvironments'; -function IndexComponent() { - const { packages, selectedPackage } = usePackages(); +function IndexRedirect() { + const navigate = useNavigate(); + const { user, session, loading: sessionLoading } = useSession(); + const { environments, loading: envsLoading } = useEnvironments(); + + useEffect(() => { + if (sessionLoading || !user) return; // RequireAuth sends to /login + + if (!session?.activeOrganizationId) { + navigate({ to: '/orgs' }); + return; + } + if (envsLoading) return; + + const lastEnvId = localStorage.getItem('objectstack.lastEnvId'); + const targetEnv = + (lastEnvId && environments.find((e) => e.id === lastEnvId)) || + environments.find((e) => e.isDefault) || + environments[0]; + + if (targetEnv) { + navigate({ + to: '/environments/$environmentId', + params: { environmentId: targetEnv.id }, + }); + } else { + navigate({ to: '/environments' }); + } + }, [user, session, sessionLoading, environments, envsLoading, navigate]); return ( -
-
- -
-
+
+
+
); } export const Route = createFileRoute('/')({ - component: IndexComponent, + component: IndexRedirect, }); diff --git a/apps/studio/src/routes/login.tsx b/apps/studio/src/routes/login.tsx index 31aafbbef..fe1be8eab 100644 --- a/apps/studio/src/routes/login.tsx +++ b/apps/studio/src/routes/login.tsx @@ -9,6 +9,7 @@ import { Input } from '@/components/ui/input'; import { Label } from '@/components/ui/label'; import { toast } from '@/hooks/use-toast'; import { useSession } from '@/hooks/useSession'; +import { useEnvironments } from '@/hooks/useEnvironments'; export const Route = createFileRoute('/login')({ component: LoginPage, @@ -17,16 +18,35 @@ export const Route = createFileRoute('/login')({ function LoginPage() { const navigate = useNavigate(); const client = useClient() as any; - const { user, refresh } = useSession(); + const { session, user, refresh } = useSession(); + const { environments, loading: envsLoading } = useEnvironments(); const [email, setEmail] = useState(''); const [password, setPassword] = useState(''); const [submitting, setSubmitting] = useState(false); useEffect(() => { - if (user) { - navigate({ to: '/' }); + if (!user) return; + if (!session?.activeOrganizationId) { + navigate({ to: '/orgs' }); + return; } - }, [user, navigate]); + if (envsLoading) return; + + const lastEnvId = localStorage.getItem('objectstack.lastEnvId'); + const targetEnv = + (lastEnvId && environments.find((e) => e.id === lastEnvId)) || + environments.find((e) => e.isDefault) || + environments[0]; + + if (targetEnv) { + navigate({ + to: '/environments/$environmentId', + params: { environmentId: targetEnv.id }, + }); + } else { + navigate({ to: '/environments' }); + } + }, [user, session, environments, envsLoading, navigate]); const handleSubmit = async (e: React.FormEvent) => { e.preventDefault(); @@ -36,7 +56,6 @@ function LoginPage() { await client.auth.login({ type: 'email', email, password }); await refresh(); toast({ title: 'Welcome back' }); - navigate({ to: '/' }); } catch (err) { toast({ title: 'Sign in failed',