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 { ConfigBanner, VersionBadge } from "@/components/RuntimeStatus";
import { ErrorBoundary } from "@/components/ErrorBoundary"; import { ErrorBoundary } from "@/components/ErrorBoundary";
import { Loading } from "@/components/States"; import { Loading } from "@/components/States";
import { ROLE_LABEL_KEY } from "@/components/RoleBadge";
function SectionGroup({ function SectionGroup({
section, section,
@@ -71,9 +72,9 @@ const FOOT_ICON_BTN =
function UserStrip() { function UserStrip() {
// The sidebar foot identifies the principal and exposes one action — sign out. // 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 // It shows the email and the role by its name in the UI language (user_id is a
// server-truth not display). While /me is loading or has failed we render a // UUID). While /me is loading or has failed both lines hold a muted dash, so
// muted placeholder rather than a broken row, so the strip never flashes empty. // the strip never flashes empty and never names a role it has not read.
const { identity, refresh } = useTier(); const { identity, refresh } = useTier();
const { t } = useTranslation("account"); const { t } = useTranslation("account");
const [signingOut, setSigningOut] = useState(false); const [signingOut, setSigningOut] = useState(false);
@@ -99,8 +100,8 @@ function UserStrip() {
> >
{identity?.email ?? <span className="text-muted-foreground/40">—</span>} {identity?.email ?? <span className="text-muted-foreground/40">—</span>}
</div> </div>
<div className="text-[10px] text-muted-foreground/70 capitalize font-medium"> <div className="text-[10px] text-muted-foreground/70 font-medium">
{identity?.role ?? "user"} {identity ? t(`admin:${ROLE_LABEL_KEY[identity.role]}`) : <span className="text-muted-foreground/40">—</span>}
</div> </div>
</div> </div>
<button <button
+3 -2
View File
@@ -10,7 +10,8 @@ const STYLE_MAP = {
user: "bg-muted text-muted-foreground", 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", owner: "users_role_owner",
admin: "users_role_admin", admin: "users_role_admin",
user: "users_role_user", user: "users_role_user",
@@ -27,7 +28,7 @@ export function RoleBadge({ role }: { role: "owner" | "admin" | "user" }) {
)} )}
> >
<Icon className="h-3 w-3" /> <Icon className="h-3 w-3" />
{t(I18N_KEY[role])} {t(ROLE_LABEL_KEY[role])}
</span> </span>
); );
} }