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

This commit is contained in:
Lemon-miaow committed 2026-09-27 15:53:51 +08:00
1 parent 68387a3b12
commit 5282d55214
3 files changed
+72 -7

No files matched your search

+63
View File
@@ -0,0 +1,63 @@
// @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
View File
@@ -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
View File
@@ -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>
);
}