feat(web): page <h1> + document.title on list/form routes (#57)
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -6,7 +6,9 @@ import type { components } from "../api/schema";
|
||||
import { useAuthorities, useCreateAuthority } from "../api/queries";
|
||||
import { LabelEditor } from "../components/label-editor";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { PageTitle } from "@/components/ui/page-title";
|
||||
import { AuthorityRow } from "./authority-row";
|
||||
import { useDocumentTitle } from "../lib/use-document-title";
|
||||
|
||||
type LabelInput = components["schemas"]["LabelInput"];
|
||||
|
||||
@@ -26,6 +28,8 @@ export function AuthoritiesPage() {
|
||||
const [labels, setLabels] = useState<LabelInput[]>([]);
|
||||
const [error, setError] = useState(false);
|
||||
|
||||
useDocumentTitle(t("nav.authorities"));
|
||||
|
||||
if (!isValidKind) return <Navigate to="/authorities/person" replace />;
|
||||
|
||||
const onCreate = (event: FormEvent) => {
|
||||
@@ -45,6 +49,7 @@ export function AuthoritiesPage() {
|
||||
|
||||
return (
|
||||
<div className="overflow-auto p-4">
|
||||
<PageTitle className="mb-3">{t("nav.authorities")}</PageTitle>
|
||||
<div role="tablist" className="mb-3 flex gap-2">
|
||||
{KINDS.map((k) => (
|
||||
<NavLink
|
||||
|
||||
@@ -1,25 +1,34 @@
|
||||
import { useState } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
|
||||
import type { components } from "../api/schema";
|
||||
import { FieldList } from "./field-list";
|
||||
import { FieldForm } from "./field-form";
|
||||
import { useDocumentTitle } from "../lib/use-document-title";
|
||||
import { PageTitle } from "@/components/ui/page-title";
|
||||
|
||||
type FieldDefinitionView = components["schemas"]["FieldDefinitionView"];
|
||||
|
||||
export function FieldsPage() {
|
||||
const { t } = useTranslation();
|
||||
const [selected, setSelected] = useState<FieldDefinitionView | null>(null);
|
||||
|
||||
useDocumentTitle(t("fields.title"));
|
||||
|
||||
return (
|
||||
<div className="grid h-full grid-cols-[20rem_1fr]">
|
||||
<div className="overflow-hidden border-r">
|
||||
<FieldList selectedKey={selected?.key ?? null} onSelect={setSelected} />
|
||||
</div>
|
||||
<div className="overflow-hidden">
|
||||
<FieldForm
|
||||
key={selected?.key ?? "create"}
|
||||
editing={selected}
|
||||
onDone={() => setSelected(null)}
|
||||
/>
|
||||
<div className="flex h-full flex-col">
|
||||
<PageTitle className="px-4 pt-4 pb-2">{t("fields.title")}</PageTitle>
|
||||
<div className="grid flex-1 grid-cols-[20rem_1fr] overflow-hidden">
|
||||
<div className="overflow-hidden border-r">
|
||||
<FieldList selectedKey={selected?.key ?? null} onSelect={setSelected} />
|
||||
</div>
|
||||
<div className="overflow-hidden">
|
||||
<FieldForm
|
||||
key={selected?.key ?? "create"}
|
||||
editing={selected}
|
||||
onDone={() => setSelected(null)}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -4,6 +4,8 @@ import { useTranslation } from "react-i18next";
|
||||
|
||||
import { ObjectForm, type ObjectFormValues } from "./object-form";
|
||||
import { useCreateObject, useSetFields, FieldRejection } from "../api/queries";
|
||||
import { useDocumentTitle } from "../lib/use-document-title";
|
||||
import { PageTitle } from "@/components/ui/page-title";
|
||||
|
||||
export function ObjectNewPage() {
|
||||
const { t } = useTranslation();
|
||||
@@ -12,6 +14,8 @@ export function ObjectNewPage() {
|
||||
const setFields = useSetFields();
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
|
||||
useDocumentTitle(t("objects.new"));
|
||||
|
||||
const onSubmit = async (values: ObjectFormValues) => {
|
||||
setError(null);
|
||||
|
||||
@@ -44,6 +48,7 @@ export function ObjectNewPage() {
|
||||
|
||||
return (
|
||||
<div className="mx-auto max-w-2xl">
|
||||
<PageTitle className="mb-4">{t("objects.new")}</PageTitle>
|
||||
<ObjectForm
|
||||
mode="create"
|
||||
formError={error}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { afterEach, expect, test, vi } from "vitest";
|
||||
import { screen, within } from "@testing-library/react";
|
||||
import { screen, waitFor, within } from "@testing-library/react";
|
||||
import userEvent from "@testing-library/user-event";
|
||||
import { Routes, Route } from "react-router-dom";
|
||||
import { renderApp } from "../test/render";
|
||||
@@ -39,6 +39,16 @@ afterEach(() => {
|
||||
vi.restoreAllMocks();
|
||||
});
|
||||
|
||||
test("renders the page heading and sets the document title", async () => {
|
||||
setViewport(true);
|
||||
renderApp(tree(), { route: "/objects" });
|
||||
|
||||
expect(
|
||||
await screen.findByRole("heading", { level: 1, name: /objects/i }),
|
||||
).toBeInTheDocument();
|
||||
await waitFor(() => expect(document.title).toMatch(/objects \| /i));
|
||||
});
|
||||
|
||||
test("the table is the landing view; no detail panel until a row is opened", async () => {
|
||||
setViewport(true);
|
||||
renderApp(tree(), { route: "/objects" });
|
||||
|
||||
@@ -5,6 +5,8 @@ import { X } from "lucide-react";
|
||||
|
||||
import { ObjectsTable } from "./objects-table";
|
||||
import { useMediaQuery } from "../lib/use-media-query";
|
||||
import { useDocumentTitle } from "../lib/use-document-title";
|
||||
import { PageTitle } from "@/components/ui/page-title";
|
||||
|
||||
const ObjectDetailDrawer = lazy(() =>
|
||||
import("./object-detail-drawer").then((m) => ({ default: m.ObjectDetailDrawer })),
|
||||
@@ -23,11 +25,16 @@ export function ObjectsPage() {
|
||||
const open = Boolean(detailMatch ?? editMatch);
|
||||
const isWide = useMediaQuery("(min-width: 1024px)");
|
||||
|
||||
useDocumentTitle(t("nav.objects"));
|
||||
|
||||
const closeDetail = () => navigate(`/objects?${searchParams}`);
|
||||
|
||||
const table = (
|
||||
<div className="overflow-hidden">
|
||||
<ObjectsTable />
|
||||
<div className="flex h-full flex-col overflow-hidden">
|
||||
<PageTitle className="px-4 pt-4 pb-2">{t("nav.objects")}</PageTitle>
|
||||
<div className="flex-1 overflow-hidden">
|
||||
<ObjectsTable />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
|
||||
@@ -1,15 +1,25 @@
|
||||
import { Outlet } from "react-router-dom";
|
||||
import { useTranslation } from "react-i18next";
|
||||
|
||||
import { SearchPanel } from "./search-panel";
|
||||
import { useDocumentTitle } from "../lib/use-document-title";
|
||||
import { PageTitle } from "@/components/ui/page-title";
|
||||
|
||||
export function SearchPage() {
|
||||
const { t } = useTranslation();
|
||||
|
||||
useDocumentTitle(t("nav.search"));
|
||||
|
||||
return (
|
||||
<div className="grid h-full grid-cols-[24rem_1fr]">
|
||||
<div className="overflow-hidden border-r">
|
||||
<SearchPanel />
|
||||
</div>
|
||||
<div className="overflow-hidden">
|
||||
<Outlet />
|
||||
<div className="flex h-full flex-col">
|
||||
<PageTitle className="px-4 pt-4 pb-2">{t("nav.search")}</PageTitle>
|
||||
<div className="grid flex-1 grid-cols-[24rem_1fr] overflow-hidden">
|
||||
<div className="overflow-hidden border-r">
|
||||
<SearchPanel />
|
||||
</div>
|
||||
<div className="overflow-hidden">
|
||||
<Outlet />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -1,15 +1,25 @@
|
||||
import { Outlet } from "react-router-dom";
|
||||
import { useTranslation } from "react-i18next";
|
||||
|
||||
import { VocabularyList } from "./vocabulary-list";
|
||||
import { useDocumentTitle } from "../lib/use-document-title";
|
||||
import { PageTitle } from "@/components/ui/page-title";
|
||||
|
||||
export function VocabulariesPage() {
|
||||
const { t } = useTranslation();
|
||||
|
||||
useDocumentTitle(t("nav.vocabularies"));
|
||||
|
||||
return (
|
||||
<div className="grid h-full grid-cols-[20rem_1fr]">
|
||||
<div className="overflow-hidden border-r">
|
||||
<VocabularyList />
|
||||
</div>
|
||||
<div className="overflow-hidden">
|
||||
<Outlet />
|
||||
<div className="flex h-full flex-col">
|
||||
<PageTitle className="px-4 pt-4 pb-2">{t("nav.vocabularies")}</PageTitle>
|
||||
<div className="grid flex-1 grid-cols-[20rem_1fr] overflow-hidden">
|
||||
<div className="overflow-hidden border-r">
|
||||
<VocabularyList />
|
||||
</div>
|
||||
<div className="overflow-hidden">
|
||||
<Outlet />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
Reference in New Issue
Block a user