feat(panel): wire role-switcher into the app shell
Build the React consumer over the fail-closed view-mode logic so an admin can view the app as each of the three homes (User/Admin/SysAdmin) and step down to a plain User-Side home. - ViewModeProvider holds the raw requested home (seeded from localStorage, shape-checked only) and resolves it live against is_admin on every render, so a demotion or transient /me failure collapses to the User home with no flash, while an unentitled value is never stored or applied. - RoleSwitcher renders only for admins (availableViewModes > 1); switching re-gates the choice and navigates to the chosen home's root. - AppShell drives its sidebar from sectionsForView(view, isAdmin), which only ever narrows visibleSections — an admin viewing as a user sees a plain user's sidebar and lands on the Dashboard at /. - landingPathForView / viewModeLabelKey added to the logic layer (tested); view_* and view_switch_label i18n keys added for en-US and zh-CN. Placement note: the spec calls for a top-right avatar control, but the panel has no desktop top bar, so the switcher lives in the sidebar foot beside the user strip. Functionally complete; placement is not yet spec-parity.
This commit is contained in:
8 files changed
+277
-9
No files matched your search
@@ -5,8 +5,11 @@ import { useTranslation } from "react-i18next";
|
||||
import * as SelectPrimitive from "@radix-ui/react-select";
|
||||
import { cn } from "@/lib/utils";
|
||||
import { useTier } from "@/lib/tier";
|
||||
import { visibleSections, type NavSection } from "@/lib/nav";
|
||||
import { type NavSection } from "@/lib/nav";
|
||||
import { sectionsForView } from "@/lib/viewmode";
|
||||
import { useViewMode } from "@/lib/viewmode-store";
|
||||
import { Select, SelectContent, SelectItem } from "@/components/ui/select";
|
||||
import { RoleSwitcher } from "@/components/RoleSwitcher";
|
||||
import { useTheme } from "@/lib/theme";
|
||||
import { api } from "@/lib/api";
|
||||
|
||||
@@ -138,11 +141,14 @@ function ThemeToggle() {
|
||||
|
||||
export function AppShell() {
|
||||
const { isAdmin } = useTier();
|
||||
const { view } = useViewMode();
|
||||
const { t, i18n } = useTranslation("navigation");
|
||||
// Sections are derived purely from is_admin: User-Side always, Admin/SysAdmin
|
||||
// only for admins. isAdmin is fail-closed (false while /me loads or on failure),
|
||||
// so admin sections appear only once identity is confirmed.
|
||||
const sections = visibleSections(isAdmin);
|
||||
// Sections are derived from is_admin AND the chosen home: visibleSections drops
|
||||
// every admin-gated section for a non-admin (fail-closed — false while /me loads or
|
||||
// on failure), then the role-switcher's view narrows further to the home the admin
|
||||
// is currently viewing. sectionsForView can only ever narrow, never widen, so an
|
||||
// admin who steps down to the User-home sees a plain user's sidebar.
|
||||
const sections = sectionsForView(view, isAdmin);
|
||||
|
||||
// Sync document metadata with the active language.
|
||||
useEffect(() => {
|
||||
@@ -176,10 +182,13 @@ export function AppShell() {
|
||||
</span>
|
||||
</div>
|
||||
|
||||
{/* 2. Profile Card */}
|
||||
{/* 2. Role switcher (admin-only; renders nothing for a plain user) */}
|
||||
<RoleSwitcher />
|
||||
|
||||
{/* 3. Profile Card */}
|
||||
<UserStrip />
|
||||
|
||||
{/* 3. Branding Sign-off (sits tight at the absolute bottom with leading-tight and centered) */}
|
||||
{/* 4. Branding Sign-off (sits tight at the absolute bottom with leading-tight and centered) */}
|
||||
<div className="px-1 text-[10px] text-muted-foreground/50 leading-tight text-center">
|
||||
{t("common:brand_tagline")}
|
||||
</div>
|
||||
|
||||
Reference in new issue
Block a user