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