fix(admin): hide collapse button when page has only one section

`/admin/api-tokens` carried a Collapse / Expand toggle inherited
from the v1.4 shared admin page where 13 sections lived together.
On the v1.5 dedicated route the toggle just hides the only card on
the page — Marc reported it as "sinnlos" (pointless): the page
title already says "API Tokens", the only thing below it is the
tokens card, and collapsing it leaves an empty surface with the
section heading staring back at you.

Drop the local `expanded` state and the toggle button from the
section. The card always renders, the query always fetches, and the
header is now a plain icon + title row. The shared
`settings.collapse` / `settings.expand` i18n keys stay in messages
because `<LoginOverviewSection>` still uses them.

The matching test asserts no Collapse / Expand / Einklappen /
Ausklappen string appears in either locale, and that no
`aria-expanded` button shows up on the section header.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
Marc-André BombeckandClaude Opus 4.7 committed 2026-05-10 13:21:36 +02:00
1 parent dd8212e202
commit 7a70db6c86
2 files changed
+95 -30

No files matched your search

@@ -0,0 +1,82 @@
import { describe, expect, it, vi } from "vitest";
import { renderToStaticMarkup } from "react-dom/server";
/**
* v1.4.19 phase A7 — `/admin/api-tokens` no longer renders a Collapse
* / Expand toggle. The toggle existed as an escape hatch from the
* v1.4 shared-admin page where 13 sections lived on one route; in
* v1.5 every section has its own dedicated route, so collapsing the
* only card on `/admin/api-tokens` hides the entire surface and
* confused Marc more than it helped.
*/
vi.mock("next/navigation", () => ({
useRouter: () => ({ push: vi.fn(), replace: vi.fn() }),
usePathname: () => "/admin/api-tokens",
}));
vi.mock("@tanstack/react-query", () => ({
useQuery: () => ({
data: [],
isLoading: false,
isError: false,
refetch: vi.fn(),
}),
useQueryClient: () => ({ invalidateQueries: vi.fn() }),
useMutation: () => ({
mutate: vi.fn(),
isPending: false,
isError: false,
error: null,
}),
}));
vi.mock("@/hooks/use-auth", () => ({
useAuth: () => ({
user: {
id: "u1",
username: "marc",
email: "marc@example.com",
role: "ADMIN",
},
isAuthenticated: true,
isLoading: false,
refetch: vi.fn(),
}),
}));
import { I18nProvider } from "@/lib/i18n/context";
import { ApiTokenOverviewSection } from "../api-token-overview-section";
function render(locale: "en" | "de" = "en") {
return renderToStaticMarkup(
<I18nProvider initialLocale={locale}>
<ApiTokenOverviewSection />
</I18nProvider>,
);
}
describe("ApiTokenOverviewSection — no collapse button", () => {
it("does not render a Collapse / Expand button (English)", () => {
const html = render("en");
expect(html).not.toMatch(/\bCollapse\b/);
expect(html).not.toMatch(/\bExpand\b/);
});
it("does not render an Einklappen / Ausklappen button (German)", () => {
const html = render("de");
expect(html).not.toMatch(/\bEinklappen\b/);
expect(html).not.toMatch(/\bAusklappen\b/);
});
it("never renders an `aria-expanded` button on the section header", () => {
const html = render();
// The toggle previously surfaced as <button aria-expanded={…}>.
// No button on the section header should carry that attribute now.
const sectionHeader = html.match(
/<div\b[^>]*flex items-center[^>]*>[\s\S]*?<\/div>/,
);
expect(sectionHeader).not.toBeNull();
expect(sectionHeader![0]).not.toMatch(/aria-expanded=/);
});
});
@@ -1,10 +1,8 @@
"use client";
import { useState } from "react";
import { useQuery } from "@tanstack/react-query";
import { ChevronDown, Key, Loader2 } from "lucide-react";
import { Key, Loader2 } from "lucide-react";
import { Badge } from "@/components/ui/badge";
import { Button } from "@/components/ui/button";
import { EmptyState } from "@/components/ui/empty-state";
import {
Tooltip,
@@ -62,11 +60,12 @@ function TruncatedCell({
export function ApiTokenOverviewSection() {
const { t } = useTranslations();
// v1.5 phase-4b moved this to a dedicated route (`/admin/api-tokens`),
// so the user has already opted into seeing tokens by visiting the
// page. Default to expanded; the toggle stays as an escape hatch.
const [expanded, setExpanded] = useState(true);
// v1.4.19 phase A7 — `/admin/api-tokens` is a dedicated single-
// section route. The previous pattern carried a "Collapse / Expand"
// toggle as an escape hatch from the v1.4 shared admin page where
// 13 sections lived together, but on a route that only renders this
// one card the toggle hides the entire surface. Marc reported it
// as "sinnlos" — gone.
const { data: tokens, isLoading } = useQuery({
queryKey: ["admin", "tokens"],
queryFn: async () => {
@@ -74,32 +73,17 @@ export function ApiTokenOverviewSection() {
if (!res.ok) throw new Error("Failed");
return (await res.json()).data as ApiTokenInfo[];
},
enabled: expanded,
});
return (
<div className="bg-card border-border overflow-hidden rounded-xl border p-4 sm:p-6">
<div className="flex items-center justify-between gap-2">
<div className="flex items-center gap-2">
<Key className="text-primary h-5 w-5" />
<div className="text-lg font-semibold">{t("admin.apiTokens")}</div>
</div>
<Button
variant="ghost"
size="sm"
onClick={() => setExpanded((prev) => !prev)}
aria-expanded={expanded}
>
{expanded ? t("settings.collapse") : t("settings.expand")}
<ChevronDown
className={`ml-1 h-4 w-4 transition-transform ${expanded ? "rotate-180" : ""}`}
/>
</Button>
<div className="flex items-center gap-2">
<Key className="text-primary h-5 w-5" />
<div className="text-lg font-semibold">{t("admin.apiTokens")}</div>
</div>
{expanded && (
<div className="mt-4">
{isLoading ? (
<div className="mt-4">
{isLoading ? (
<div className="flex justify-center py-4">
<Loader2 className="text-muted-foreground h-5 w-5 animate-spin" />
</div>
@@ -291,8 +275,7 @@ export function ApiTokenOverviewSection() {
</ul>
</>
)}
</div>
)}
</div>
</div>
);
}