fix(panel): 侧栏用户卡按界面语言显示角色名,身份未读到时显示横线
This commit is contained in:
3 files changed
+72
-7
No files matched your search
@@ -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();
|
||||
});
|
||||
});
|
||||
@@ -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
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
Reference in new issue
Block a user