feat(web): assemble header — breadcrumb, search, user menu; remove standalone sign out (#54)
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -1,8 +1,10 @@
|
|||||||
import { expect, test, beforeEach, afterEach } from "vitest";
|
import { expect, test, beforeEach, afterEach } from "vitest";
|
||||||
import { screen, waitFor } from "@testing-library/react";
|
import { screen, waitFor, within } from "@testing-library/react";
|
||||||
import userEvent from "@testing-library/user-event";
|
import userEvent from "@testing-library/user-event";
|
||||||
import { Routes, Route } from "react-router-dom";
|
import { Routes, Route } from "react-router-dom";
|
||||||
|
import { http, HttpResponse } from "msw";
|
||||||
import i18n from "../i18n";
|
import i18n from "../i18n";
|
||||||
|
import { server } from "../test/server";
|
||||||
import { renderApp } from "../test/render";
|
import { renderApp } from "../test/render";
|
||||||
import { AppShell } from "./app-shell";
|
import { AppShell } from "./app-shell";
|
||||||
|
|
||||||
@@ -39,3 +41,28 @@ test("language switch toggles to Swedish", async () => {
|
|||||||
await userEvent.click(await screen.findByRole("button", { name: "SV" }));
|
await userEvent.click(await screen.findByRole("button", { name: "SV" }));
|
||||||
await waitFor(() => expect(screen.getByText("Föremål")).toBeInTheDocument());
|
await waitFor(() => expect(screen.getByText("Föremål")).toBeInTheDocument());
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test("signs out via the user menu and navigates to /login", async () => {
|
||||||
|
let loggedOut = false;
|
||||||
|
server.use(
|
||||||
|
http.post("/api/admin/logout", () => {
|
||||||
|
loggedOut = true;
|
||||||
|
return new HttpResponse(null, { status: 204 });
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
|
||||||
|
renderApp(tree(), { route: "/objects" });
|
||||||
|
|
||||||
|
// The user menu trigger shows the signed-in email (from /api/admin/me).
|
||||||
|
const trigger = await screen.findByRole("button", { name: /editor@example.com/ });
|
||||||
|
await userEvent.click(trigger);
|
||||||
|
|
||||||
|
// Menu content renders in a portal on document.body.
|
||||||
|
const menu = within(document.body);
|
||||||
|
const signOut = await menu.findByText("Sign out");
|
||||||
|
await userEvent.click(signOut);
|
||||||
|
|
||||||
|
await waitFor(() => expect(loggedOut).toBe(true));
|
||||||
|
// Sign-out replaces the route with /login.
|
||||||
|
expect(await screen.findByText("login page")).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
|||||||
@@ -1,24 +1,14 @@
|
|||||||
import { Outlet, useNavigate } from "react-router-dom";
|
import { Outlet } from "react-router-dom";
|
||||||
import { useTranslation } from "react-i18next";
|
|
||||||
|
|
||||||
import { useLogout } from "../api/queries";
|
|
||||||
import { Button } from "@/components/ui/button";
|
|
||||||
import { LangSwitch } from "./lang-switch";
|
import { LangSwitch } from "./lang-switch";
|
||||||
import { ThemeSwitch } from "./theme-switch";
|
import { ThemeSwitch } from "./theme-switch";
|
||||||
import { Sidebar } from "./sidebar";
|
import { Sidebar } from "./sidebar";
|
||||||
import { BreadcrumbProvider } from "./breadcrumb-provider";
|
import { BreadcrumbProvider } from "./breadcrumb-provider";
|
||||||
import { Breadcrumb } from "./breadcrumb";
|
import { Breadcrumb } from "./breadcrumb";
|
||||||
|
import { HeaderSearch } from "./header-search";
|
||||||
|
import { UserMenu } from "./user-menu";
|
||||||
|
|
||||||
export function AppShell() {
|
export function AppShell() {
|
||||||
const { t } = useTranslation();
|
|
||||||
const navigate = useNavigate();
|
|
||||||
const logout = useLogout();
|
|
||||||
|
|
||||||
const onSignOut = () =>
|
|
||||||
logout.mutate(undefined, {
|
|
||||||
onSuccess: () => navigate("/login", { replace: true }),
|
|
||||||
});
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex min-h-screen">
|
<div className="flex min-h-screen">
|
||||||
<Sidebar />
|
<Sidebar />
|
||||||
@@ -26,11 +16,10 @@ export function AppShell() {
|
|||||||
<div className="flex flex-1 flex-col">
|
<div className="flex flex-1 flex-col">
|
||||||
<header className="flex items-center gap-4 border-b px-4 py-2">
|
<header className="flex items-center gap-4 border-b px-4 py-2">
|
||||||
<Breadcrumb />
|
<Breadcrumb />
|
||||||
|
<HeaderSearch />
|
||||||
<ThemeSwitch />
|
<ThemeSwitch />
|
||||||
<LangSwitch />
|
<LangSwitch />
|
||||||
<Button variant="ghost" size="sm" onClick={onSignOut}>
|
<UserMenu />
|
||||||
{t("auth.signOut")}
|
|
||||||
</Button>
|
|
||||||
</header>
|
</header>
|
||||||
<main className="flex-1 overflow-hidden">
|
<main className="flex-1 overflow-hidden">
|
||||||
<Outlet />
|
<Outlet />
|
||||||
|
|||||||
Reference in New Issue
Block a user