Unverified Commit 5282d552 authored by Lemon-miaow's avatar Lemon-miaow
Browse files

fix(panel): 侧栏用户卡按界面语言显示角色名,身份未读到时显示横线

parent 68387a3b
Loading
Loading
Loading
Loading
+63 −0
Changes for panel/src/components/AppShell.test.tsx: 63 added lines, 0 removed lines.
Original line number Diff line number Diff line
// @vitest-environment jsdom
import { describe, it, expect, vi, afterEach } from "vitest";
import { render, screen } from "@testing-library/react";
import { MemoryRouter, Route, Routes } from "react-router-dom";
import i18next from "i18next";
import { AppShell } from "./AppShell";
import type { Identity } from "@/lib/types";

const tier = vi.hoisted(() => ({
  identity: null as Identity | null,
  isAdmin: false,
  isOwner: false,
  loading: false,
  refresh: () => Promise.resolve(),
}));
vi.mock("@/lib/tier", () => ({ useTier: () => tier }));
vi.mock("@/lib/config", async (importOriginal) => {
  const actual = await importOriginal<typeof import("@/lib/config")>();
  return { ...actual, loadConfig: () => Promise.resolve({}) };
});

afterEach(() => {
  tier.identity = null;
  return i18next.changeLanguage("en-US");
});

function renderShell() {
  render(
    <MemoryRouter initialEntries={["/"]}>
      <Routes>
        <Route path="/" element={<AppShell />}>
          <Route index element={<p>page</p>} />
        </Route>
      </Routes>
    </MemoryRouter>,
  );
}

// The line under the email in the sidebar's user card.
function roleLine(email: string) {
  return screen.getByText(email).nextElementSibling?.textContent;
}

describe("AppShell user card", () => {
  it.each([
    ["en-US", "owner", "Owner"],
    ["en-US", "user", "User"],
    ["zh-CN", "admin", "管理员"],
    ["zh-CN", "user", "普通用户"],
  ] as const)("names the role in %s: %s is %s", async (lang, role, shown) => {
    await i18next.changeLanguage(lang);
    tier.identity = { user_id: "u-1", email: "[email protected]", role } as Identity;
    renderShell();
    expect(roleLine("[email protected]")).toBe(shown);
  });

  it("names no role before it has read one", () => {
    renderShell();
    const dashes = screen.getAllByText("—");
    expect(dashes).toHaveLength(2);
    expect(screen.queryByText(/^user$/i)).toBeNull();
  });
});
+6 −5
Changes for panel/src/components/AppShell.tsx: 6 added lines, 5 removed lines.
Original line number Diff line number Diff line
@@ -14,6 +14,7 @@ import { Button } from "@/components/ui/button";
import { ConfigBanner, VersionBadge } from "@/components/RuntimeStatus";
import { ErrorBoundary } from "@/components/ErrorBoundary";
import { Loading } from "@/components/States";
import { ROLE_LABEL_KEY } from "@/components/RoleBadge";

function SectionGroup({
  section,
@@ -71,9 +72,9 @@ const FOOT_ICON_BTN =

function UserStrip() {
  // The sidebar foot identifies the principal and exposes one action — sign out.
  // identity?.email is the only display-safe field (user_id is a UUID, role is
  // server-truth not display). While /me is loading or has failed we render a
  // muted placeholder rather than a broken row, so the strip never flashes empty.
  // It shows the email and the role by its name in the UI language (user_id is a
  // UUID). While /me is loading or has failed both lines hold a muted dash, so
  // the strip never flashes empty and never names a role it has not read.
  const { identity, refresh } = useTier();
  const { t } = useTranslation("account");
  const [signingOut, setSigningOut] = useState(false);
@@ -99,8 +100,8 @@ function UserStrip() {
        >
          {identity?.email ?? <span className="text-muted-foreground/40">—</span>}
        </div>
        <div className="text-[10px] text-muted-foreground/70 capitalize font-medium">
          {identity?.role ?? "user"}
        <div className="text-[10px] text-muted-foreground/70 font-medium">
          {identity ? t(`admin:${ROLE_LABEL_KEY[identity.role]}`) : <span className="text-muted-foreground/40">—</span>}
        </div>
      </div>
      <button
+3 −2
Changes for panel/src/components/RoleBadge.tsx: 3 added lines, 2 removed lines.
Original line number Diff line number Diff line
@@ -10,7 +10,8 @@ const STYLE_MAP = {
  user: "bg-muted text-muted-foreground",
};

const I18N_KEY = {
// The role's name in the UI language, under the admin namespace.
export const ROLE_LABEL_KEY = {
  owner: "users_role_owner",
  admin: "users_role_admin",
  user: "users_role_user",
@@ -27,7 +28,7 @@ export function RoleBadge({ role }: { role: "owner" | "admin" | "user" }) {
      )}
    >
      <Icon className="h-3 w-3" />
      {t(I18N_KEY[role])}
      {t(ROLE_LABEL_KEY[role])}
    </span>
  );
}