From 0daf47d7c0c96d32f66ec9aac1ffe828e775f016 Mon Sep 17 00:00:00 2001 From: Harry Randall Date: Tue, 22 Sep 2026 15:01:38 +1000 Subject: [PATCH] fix: stabilise admin loading and catalogue views --- .../courses/{ => (directory)}/loading.tsx | 0 .../admin/courses/{ => (directory)}/page.tsx | 0 .../courses/[year]/(directory)/loading.tsx | 7 + .../courses/[year]/{ => (directory)}/page.tsx | 2 + .../majors/{ => (directory)}/loading.tsx | 0 .../admin/majors/{ => (directory)}/page.tsx | 0 .../majors/[year]/(directory)/loading.tsx | 5 + .../majors/[year]/{ => (directory)}/page.tsx | 2 + .../minors/{ => (directory)}/loading.tsx | 0 .../admin/minors/{ => (directory)}/page.tsx | 0 .../minors/[year]/(directory)/loading.tsx | 5 + .../minors/[year]/{ => (directory)}/page.tsx | 2 + .../programmes/{ => (directory)}/loading.tsx | 0 .../programmes/{ => (directory)}/page.tsx | 0 .../programmes/[year]/(directory)/loading.tsx | 7 + .../[year]/{ => (directory)}/page.tsx | 2 + .../{ => (directory)}/loading.tsx | 0 .../{ => (directory)}/page.tsx | 0 .../[year]/(directory)/loading.tsx | 11 + .../[year]/{ => (directory)}/page.tsx | 2 + apps/web/lib/assistant/draft-store.ts | 27 +-- apps/web/lib/coursemap/catalogue-kinds.ts | 17 ++ apps/web/tests/assistant-panel.test.tsx | 24 +++ apps/web/tests/breadcrumbs.test.tsx | 13 ++ apps/web/tests/catalogue-empty.test.tsx | 21 ++ apps/web/tests/operations-sync-views.test.tsx | 5 +- .../admin/catalogue-table/catalogue-empty.tsx | 8 +- .../catalogue-table/catalogue-loading.tsx | 42 +++- .../catalogue-table.module.css | 18 ++ .../admin/catalogue-table/catalogue-table.tsx | 3 +- .../ui/admin/catalogue/catalogue-pages.tsx | 27 +-- .../ui/admin/operations/discovery-detail.tsx | 4 +- .../ui/admin/operations/discovery-list.tsx | 32 +-- .../ui/admin/operations/operations-pages.tsx | 46 ++++- .../ui/admin/operations/operations-tabs.tsx | 7 +- apps/web/ui/admin/operations/sync-detail.tsx | 8 +- apps/web/ui/admin/operations/sync-list.tsx | 41 ++-- .../common/catalogue-illustration.module.css | 189 ------------------ apps/web/ui/common/catalogue-illustration.tsx | 23 +-- apps/web/ui/common/pagination.tsx | 11 +- apps/web/ui/help/help-guides.tsx | 2 +- apps/web/ui/shell/breadcrumbs.tsx | 13 +- scripts/check-repository.mjs | 72 +++++++ 43 files changed, 390 insertions(+), 308 deletions(-) rename apps/web/app/admin/courses/{ => (directory)}/loading.tsx (100%) rename apps/web/app/admin/courses/{ => (directory)}/page.tsx (100%) create mode 100644 apps/web/app/admin/courses/[year]/(directory)/loading.tsx rename apps/web/app/admin/courses/[year]/{ => (directory)}/page.tsx (99%) rename apps/web/app/admin/majors/{ => (directory)}/loading.tsx (100%) rename apps/web/app/admin/majors/{ => (directory)}/page.tsx (100%) create mode 100644 apps/web/app/admin/majors/[year]/(directory)/loading.tsx rename apps/web/app/admin/majors/[year]/{ => (directory)}/page.tsx (99%) rename apps/web/app/admin/minors/{ => (directory)}/loading.tsx (100%) rename apps/web/app/admin/minors/{ => (directory)}/page.tsx (100%) create mode 100644 apps/web/app/admin/minors/[year]/(directory)/loading.tsx rename apps/web/app/admin/minors/[year]/{ => (directory)}/page.tsx (99%) rename apps/web/app/admin/programmes/{ => (directory)}/loading.tsx (100%) rename apps/web/app/admin/programmes/{ => (directory)}/page.tsx (100%) create mode 100644 apps/web/app/admin/programmes/[year]/(directory)/loading.tsx rename apps/web/app/admin/programmes/[year]/{ => (directory)}/page.tsx (99%) rename apps/web/app/admin/specialisations/{ => (directory)}/loading.tsx (100%) rename apps/web/app/admin/specialisations/{ => (directory)}/page.tsx (100%) create mode 100644 apps/web/app/admin/specialisations/[year]/(directory)/loading.tsx rename apps/web/app/admin/specialisations/[year]/{ => (directory)}/page.tsx (99%) create mode 100644 apps/web/tests/catalogue-empty.test.tsx diff --git a/apps/web/app/admin/courses/loading.tsx b/apps/web/app/admin/courses/(directory)/loading.tsx similarity index 100% rename from apps/web/app/admin/courses/loading.tsx rename to apps/web/app/admin/courses/(directory)/loading.tsx diff --git a/apps/web/app/admin/courses/page.tsx b/apps/web/app/admin/courses/(directory)/page.tsx similarity index 100% rename from apps/web/app/admin/courses/page.tsx rename to apps/web/app/admin/courses/(directory)/page.tsx diff --git a/apps/web/app/admin/courses/[year]/(directory)/loading.tsx b/apps/web/app/admin/courses/[year]/(directory)/loading.tsx new file mode 100644 index 00000000..66f3d6f0 --- /dev/null +++ b/apps/web/app/admin/courses/[year]/(directory)/loading.tsx @@ -0,0 +1,7 @@ +import { CatalogueLoading } from "@/ui/admin/catalogue-table/catalogue-loading"; + +export default function Loading() { + return ( + + ); +} diff --git a/apps/web/app/admin/courses/[year]/page.tsx b/apps/web/app/admin/courses/[year]/(directory)/page.tsx similarity index 99% rename from apps/web/app/admin/courses/[year]/page.tsx rename to apps/web/app/admin/courses/[year]/(directory)/page.tsx index 5f11ec61..d05f4d78 100644 --- a/apps/web/app/admin/courses/[year]/page.tsx +++ b/apps/web/app/admin/courses/[year]/(directory)/page.tsx @@ -1,6 +1,8 @@ import type { SearchParams } from "@/ui/admin/catalogue/catalogue-pages"; import { CatalogueYearRoute } from "@/ui/admin/catalogue/catalogue-route-pages"; + export const dynamic = "force-dynamic"; + export default async function Page({ params, searchParams, diff --git a/apps/web/app/admin/majors/loading.tsx b/apps/web/app/admin/majors/(directory)/loading.tsx similarity index 100% rename from apps/web/app/admin/majors/loading.tsx rename to apps/web/app/admin/majors/(directory)/loading.tsx diff --git a/apps/web/app/admin/majors/page.tsx b/apps/web/app/admin/majors/(directory)/page.tsx similarity index 100% rename from apps/web/app/admin/majors/page.tsx rename to apps/web/app/admin/majors/(directory)/page.tsx diff --git a/apps/web/app/admin/majors/[year]/(directory)/loading.tsx b/apps/web/app/admin/majors/[year]/(directory)/loading.tsx new file mode 100644 index 00000000..aa5600be --- /dev/null +++ b/apps/web/app/admin/majors/[year]/(directory)/loading.tsx @@ -0,0 +1,5 @@ +import { CatalogueLoading } from "@/ui/admin/catalogue-table/catalogue-loading"; + +export default function Loading() { + return ; +} diff --git a/apps/web/app/admin/majors/[year]/page.tsx b/apps/web/app/admin/majors/[year]/(directory)/page.tsx similarity index 99% rename from apps/web/app/admin/majors/[year]/page.tsx rename to apps/web/app/admin/majors/[year]/(directory)/page.tsx index 1e0ff5cf..63dd1811 100644 --- a/apps/web/app/admin/majors/[year]/page.tsx +++ b/apps/web/app/admin/majors/[year]/(directory)/page.tsx @@ -1,6 +1,8 @@ import type { SearchParams } from "@/ui/admin/catalogue/catalogue-pages"; import { CatalogueYearRoute } from "@/ui/admin/catalogue/catalogue-route-pages"; + export const dynamic = "force-dynamic"; + export default async function Page({ params, searchParams, diff --git a/apps/web/app/admin/minors/loading.tsx b/apps/web/app/admin/minors/(directory)/loading.tsx similarity index 100% rename from apps/web/app/admin/minors/loading.tsx rename to apps/web/app/admin/minors/(directory)/loading.tsx diff --git a/apps/web/app/admin/minors/page.tsx b/apps/web/app/admin/minors/(directory)/page.tsx similarity index 100% rename from apps/web/app/admin/minors/page.tsx rename to apps/web/app/admin/minors/(directory)/page.tsx diff --git a/apps/web/app/admin/minors/[year]/(directory)/loading.tsx b/apps/web/app/admin/minors/[year]/(directory)/loading.tsx new file mode 100644 index 00000000..7b9506c3 --- /dev/null +++ b/apps/web/app/admin/minors/[year]/(directory)/loading.tsx @@ -0,0 +1,5 @@ +import { CatalogueLoading } from "@/ui/admin/catalogue-table/catalogue-loading"; + +export default function Loading() { + return ; +} diff --git a/apps/web/app/admin/minors/[year]/page.tsx b/apps/web/app/admin/minors/[year]/(directory)/page.tsx similarity index 99% rename from apps/web/app/admin/minors/[year]/page.tsx rename to apps/web/app/admin/minors/[year]/(directory)/page.tsx index 673b30a3..00a42ba6 100644 --- a/apps/web/app/admin/minors/[year]/page.tsx +++ b/apps/web/app/admin/minors/[year]/(directory)/page.tsx @@ -1,6 +1,8 @@ import type { SearchParams } from "@/ui/admin/catalogue/catalogue-pages"; import { CatalogueYearRoute } from "@/ui/admin/catalogue/catalogue-route-pages"; + export const dynamic = "force-dynamic"; + export default async function Page({ params, searchParams, diff --git a/apps/web/app/admin/programmes/loading.tsx b/apps/web/app/admin/programmes/(directory)/loading.tsx similarity index 100% rename from apps/web/app/admin/programmes/loading.tsx rename to apps/web/app/admin/programmes/(directory)/loading.tsx diff --git a/apps/web/app/admin/programmes/page.tsx b/apps/web/app/admin/programmes/(directory)/page.tsx similarity index 100% rename from apps/web/app/admin/programmes/page.tsx rename to apps/web/app/admin/programmes/(directory)/page.tsx diff --git a/apps/web/app/admin/programmes/[year]/(directory)/loading.tsx b/apps/web/app/admin/programmes/[year]/(directory)/loading.tsx new file mode 100644 index 00000000..ad1b04e3 --- /dev/null +++ b/apps/web/app/admin/programmes/[year]/(directory)/loading.tsx @@ -0,0 +1,7 @@ +import { CatalogueLoading } from "@/ui/admin/catalogue-table/catalogue-loading"; + +export default function Loading() { + return ( + + ); +} diff --git a/apps/web/app/admin/programmes/[year]/page.tsx b/apps/web/app/admin/programmes/[year]/(directory)/page.tsx similarity index 99% rename from apps/web/app/admin/programmes/[year]/page.tsx rename to apps/web/app/admin/programmes/[year]/(directory)/page.tsx index e2a914b2..c0eeb402 100644 --- a/apps/web/app/admin/programmes/[year]/page.tsx +++ b/apps/web/app/admin/programmes/[year]/(directory)/page.tsx @@ -1,6 +1,8 @@ import type { SearchParams } from "@/ui/admin/catalogue/catalogue-pages"; import { CatalogueYearRoute } from "@/ui/admin/catalogue/catalogue-route-pages"; + export const dynamic = "force-dynamic"; + export default async function Page({ params, searchParams, diff --git a/apps/web/app/admin/specialisations/loading.tsx b/apps/web/app/admin/specialisations/(directory)/loading.tsx similarity index 100% rename from apps/web/app/admin/specialisations/loading.tsx rename to apps/web/app/admin/specialisations/(directory)/loading.tsx diff --git a/apps/web/app/admin/specialisations/page.tsx b/apps/web/app/admin/specialisations/(directory)/page.tsx similarity index 100% rename from apps/web/app/admin/specialisations/page.tsx rename to apps/web/app/admin/specialisations/(directory)/page.tsx diff --git a/apps/web/app/admin/specialisations/[year]/(directory)/loading.tsx b/apps/web/app/admin/specialisations/[year]/(directory)/loading.tsx new file mode 100644 index 00000000..94a80755 --- /dev/null +++ b/apps/web/app/admin/specialisations/[year]/(directory)/loading.tsx @@ -0,0 +1,11 @@ +import { CatalogueLoading } from "@/ui/admin/catalogue-table/catalogue-loading"; + +export default function Loading() { + return ( + + ); +} diff --git a/apps/web/app/admin/specialisations/[year]/page.tsx b/apps/web/app/admin/specialisations/[year]/(directory)/page.tsx similarity index 99% rename from apps/web/app/admin/specialisations/[year]/page.tsx rename to apps/web/app/admin/specialisations/[year]/(directory)/page.tsx index 70f3691f..db5c2b26 100644 --- a/apps/web/app/admin/specialisations/[year]/page.tsx +++ b/apps/web/app/admin/specialisations/[year]/(directory)/page.tsx @@ -1,6 +1,8 @@ import type { SearchParams } from "@/ui/admin/catalogue/catalogue-pages"; import { CatalogueYearRoute } from "@/ui/admin/catalogue/catalogue-route-pages"; + export const dynamic = "force-dynamic"; + export default async function Page({ params, searchParams, diff --git a/apps/web/lib/assistant/draft-store.ts b/apps/web/lib/assistant/draft-store.ts index 17034fcd..9242b05a 100644 --- a/apps/web/lib/assistant/draft-store.ts +++ b/apps/web/lib/assistant/draft-store.ts @@ -7,29 +7,31 @@ export function createAssistantDraftStore(key: string) { let snapshot = empty; let loaded = false; const listeners = new Set<() => void>(); - function getSnapshot() { - if (!loaded && typeof window !== "undefined") { - try { - snapshot = readAssistantHistory(localStorage.getItem(key)); - } catch { - snapshot = empty; - } - loaded = true; + function loadStoredSnapshot() { + if (loaded) return; + try { + snapshot = readAssistantHistory(localStorage.getItem(key)); + } catch { + snapshot = empty; } - return snapshot; + loaded = true; } function notify() { listeners.forEach((listener) => listener()); } return { - getSnapshot, + // Keep the first browser snapshot equal to the server snapshot. Storage is + // read when React subscribes after hydration, then React's subscription + // check applies the restored drafts without changing the server markup. + getSnapshot: () => snapshot, getServerSnapshot: () => empty, subscribe(listener: () => void) { listeners.add(listener); + loadStoredSnapshot(); function onStorage(event: StorageEvent) { if (event.key === key || event.key === null) { loaded = false; - getSnapshot(); + loadStoredSnapshot(); notify(); } } @@ -40,7 +42,8 @@ export function createAssistantDraftStore(key: string) { }; }, update(change: (previous: AssistantDraft[]) => AssistantDraft[]) { - snapshot = change(getSnapshot()); + loadStoredSnapshot(); + snapshot = change(snapshot); try { localStorage.setItem(key, JSON.stringify(snapshot)); } catch { diff --git a/apps/web/lib/coursemap/catalogue-kinds.ts b/apps/web/lib/coursemap/catalogue-kinds.ts index 2695215a..40d616c4 100644 --- a/apps/web/lib/coursemap/catalogue-kinds.ts +++ b/apps/web/lib/coursemap/catalogue-kinds.ts @@ -47,6 +47,16 @@ export function adminCatalogueVersionPath( return `${adminCatalogueRecordPath(kind, year, code)}/changelog/${versionOrdinal}`; } +export const ADMIN_CATALOGUE_OPERATIONS_PATH = "/admin/operations/catalogue"; + +export function adminCatalogueSyncPath(syncId: string) { + return `${ADMIN_CATALOGUE_OPERATIONS_PATH}/syncs/${syncId}`; +} + +export function adminCatalogueDiscoveryPath(checkId: number) { + return `${ADMIN_CATALOGUE_OPERATIONS_PATH}/discovery/${checkId}`; +} + export function publicCatalogueRecordPath( kind: CatalogueKind, year: number, @@ -80,6 +90,13 @@ export type CatalogueDirectoryRecord = { } | null; }; +export type CatalogueTableLayout = + | "public-courses" + | "users" + | "directory" + | "operations-syncs" + | "operations-discovery"; + export type CatalogueDirectoryPage = { kind: CatalogueKind; academicYear: number; diff --git a/apps/web/tests/assistant-panel.test.tsx b/apps/web/tests/assistant-panel.test.tsx index 36a5031d..c67f594f 100644 --- a/apps/web/tests/assistant-panel.test.tsx +++ b/apps/web/tests/assistant-panel.test.tsx @@ -8,6 +8,7 @@ import { ASSISTANT_PREVIEW_RESPONSE } from "@/lib/assistant/history"; import { SidebarProvider, SidebarMenu } from "@coursemap/ui/primitives/sidebar"; import { AssistantRecentChat } from "@/ui/assistant/assistant-recent-chat"; import { assistantTitle, readAssistantHistory } from "@/lib/assistant/history"; +import { createAssistantDraftStore } from "@/lib/assistant/draft-store"; import { AssistantProvider, useAssistant, @@ -50,6 +51,29 @@ vi.mock("sonner", () => ({ toast: { info: vi.fn() } })); vi.mock("@coursemap/ui/hooks/use-mobile", () => ({ useIsMobile: () => false })); +test("restores browser drafts only after the hydration snapshot", () => { + localStorage.setItem( + "coursemap:compass:drafts:test", + JSON.stringify([ + { + id: "saved", + draft: "Continue planning", + model: "", + updatedAt: "2026-09-22T00:00:00.000Z", + messages: [], + }, + ]), + ); + const store = createAssistantDraftStore("coursemap:compass:drafts:test"); + expect(store.getServerSnapshot()).toEqual([]); + expect(store.getSnapshot()).toEqual([]); + const unsubscribe = store.subscribe(() => {}); + expect(store.getSnapshot()).toEqual([ + expect.objectContaining({ id: "saved", draft: "Continue planning" }), + ]); + unsubscribe(); +}); + test("keeps a draft when closed and reopened, and clears it for a new chat", () => { const close = vi.fn(); const { rerender } = render( diff --git a/apps/web/tests/breadcrumbs.test.tsx b/apps/web/tests/breadcrumbs.test.tsx index 7aa8b49e..b5524975 100644 --- a/apps/web/tests/breadcrumbs.test.tsx +++ b/apps/web/tests/breadcrumbs.test.tsx @@ -149,3 +149,16 @@ test("shows three short breadcrumbs until width requires collapsing the middle", resize(600); expect(within(trail).getByRole("link", { name: "Courses" })).toBeVisible(); }); + +test("does not repeat a catalogue section on its year directory", () => { + measureAt(600); + render(); + const trail = screen.getByRole("navigation", { name: "Breadcrumb" }); + expect(within(trail).getAllByRole("listitem")).toHaveLength(2); + expect(within(trail).getByRole("link", { name: "Admin" })).toBeVisible(); + expect(within(trail).getByRole("link", { name: "Courses" })).toHaveAttribute( + "aria-current", + "page", + ); + expect(trail).not.toHaveTextContent("2026"); +}); diff --git a/apps/web/tests/catalogue-empty.test.tsx b/apps/web/tests/catalogue-empty.test.tsx new file mode 100644 index 00000000..a22921c0 --- /dev/null +++ b/apps/web/tests/catalogue-empty.test.tsx @@ -0,0 +1,21 @@ +import { render } from "@testing-library/react"; +import { expect, test } from "vitest"; +import { CatalogueEmpty } from "@/ui/admin/catalogue-table/catalogue-empty"; + +test("uses stable artwork for catalogue empty states", () => { + const { container, rerender } = render( + , + ); + expect(container.querySelector("svg")).toBeInTheDocument(); + expect(container).not.toHaveTextContent("0."); + + rerender( + , + ); + expect(container.querySelector("svg")).toBeInTheDocument(); + expect(container.querySelectorAll("svg")).toHaveLength(1); +}); diff --git a/apps/web/tests/operations-sync-views.test.tsx b/apps/web/tests/operations-sync-views.test.tsx index a82f38e7..f71ea429 100644 --- a/apps/web/tests/operations-sync-views.test.tsx +++ b/apps/web/tests/operations-sync-views.test.tsx @@ -173,7 +173,7 @@ test("the sync list carries the technical columns an operator needs", () => { test("an empty list says what fills it rather than showing an empty table", () => { renderSyncList(syncPage({ rows: [], total: 0 })); - expect(screen.getByText("No syncs match")).toBeTruthy(); + expect(screen.getByText("No syncs yet")).toBeTruthy(); expect(screen.queryByRole("table")).toBeNull(); }); @@ -217,6 +217,7 @@ test("an incomplete listing check says so, because it cannot retire anything", ( render(); expect(screen.getByText("Partial")).toBeTruthy(); expect( - screen.getByRole("link", { name: "course" }).getAttribute("href"), + screen.getByRole("link", { name: "Courses" }).getAttribute("href"), ).toBe("/admin/operations/catalogue/discovery/7"); + expect(screen.getByText("120 discovered")).toBeTruthy(); }); diff --git a/apps/web/ui/admin/catalogue-table/catalogue-empty.tsx b/apps/web/ui/admin/catalogue-table/catalogue-empty.tsx index bef74758..5c17f5ec 100644 --- a/apps/web/ui/admin/catalogue-table/catalogue-empty.tsx +++ b/apps/web/ui/admin/catalogue-table/catalogue-empty.tsx @@ -9,7 +9,6 @@ export function CatalogueEmpty({ title, description, filtered = false, - imports = false, error = false, clearHref, onSync, @@ -18,7 +17,6 @@ export function CatalogueEmpty({ title: string; description: string; filtered?: boolean; - imports?: boolean; error?: boolean; clearHref?: string; onSync?: () => void; @@ -26,11 +24,7 @@ export function CatalogueEmpty({ }) { return (
- {error ? ( - - ) : ( - - )} + {error ? : }

{filtered ? "No matches this time." : title}

diff --git a/apps/web/ui/admin/catalogue-table/catalogue-loading.tsx b/apps/web/ui/admin/catalogue-table/catalogue-loading.tsx index b2aa4392..51508a17 100644 --- a/apps/web/ui/admin/catalogue-table/catalogue-loading.tsx +++ b/apps/web/ui/admin/catalogue-table/catalogue-loading.tsx @@ -18,7 +18,12 @@ import { * replaces it and the whole list reflows on arrival. */ export type CatalogueLoadingLayout = - "public-courses" | "users" | "directory" | "import-records"; + | "public-courses" + | "users" + | "directory" + | "import-records" + | "operations-syncs" + | "operations-discovery"; /** * A skeleton cell per real cell. The kind decides the shape, so a placeholder @@ -47,6 +52,27 @@ function columnsFor(noun: string, layout: CatalogueLoadingLayout): Column[] { { label: "Updated", kind: "text" }, { label: "Actions", kind: "actions" }, ]; + if (layout === "operations-syncs") + return [ + { label: "Record", kind: "identity" }, + { label: "Year", kind: "text" }, + { label: "Status", kind: "text" }, + { label: "Trigger", kind: "text" }, + { label: "Started", kind: "text" }, + { label: "Duration", kind: "text" }, + { label: "Model", kind: "text" }, + { label: "Cost", kind: "text" }, + ]; + if (layout === "operations-discovery") + return [ + { label: "Listing", kind: "identity" }, + { label: "Year", kind: "text" }, + { label: "Status", kind: "text" }, + { label: "Complete", kind: "text" }, + { label: "Discovered", kind: "text" }, + { label: "Started", kind: "text" }, + { label: "Duration", kind: "text" }, + ]; if (layout === "import-records") return [ { label: "Import", kind: "identity" }, @@ -174,12 +200,24 @@ export function CatalogueTableLoading({ export function CatalogueLoading({ noun, layout, + hideAcademicYear = false, }: { noun: string; layout: CatalogueLoadingLayout; + hideAcademicYear?: boolean; }) { + const breadcrumbSegmentLabels = hideAcademicYear + ? Object.fromEntries( + Array.from({ length: 11 }, (_, index) => [String(2020 + index), null]), + ) + : undefined; return ( - +

Loading {noun}

diff --git a/apps/web/ui/admin/catalogue-table/catalogue-table.module.css b/apps/web/ui/admin/catalogue-table/catalogue-table.module.css index 82f100f2..a9920e90 100644 --- a/apps/web/ui/admin/catalogue-table/catalogue-table.module.css +++ b/apps/web/ui/admin/catalogue-table/catalogue-table.module.css @@ -186,6 +186,24 @@ a.title:focus-visible { 170px 170px 60px; } +/* Operations syncs: record, year, status, trigger, started, duration, model, + cost. Wider than the catalogue layouts because every column here is a fact + rather than a label. */ +.shell[data-layout="operations-syncs"] tr { + min-width: 1020px; + grid-template-columns: + minmax(220px, 1.2fr) 72px 150px 100px 170px + 92px minmax(150px, 1fr) 96px; +} +/* Operations discovery: kind, year, status, completeness, discovered, + started, duration. */ +.shell[data-layout="operations-discovery"] tr { + min-width: 880px; + grid-template-columns: + minmax(180px, 1fr) 72px 130px 110px 110px 170px + 92px; +} + @media (pointer: coarse) { .actions button { min-height: 44px; diff --git a/apps/web/ui/admin/catalogue-table/catalogue-table.tsx b/apps/web/ui/admin/catalogue-table/catalogue-table.tsx index 6d99022f..db95d128 100644 --- a/apps/web/ui/admin/catalogue-table/catalogue-table.tsx +++ b/apps/web/ui/admin/catalogue-table/catalogue-table.tsx @@ -14,6 +14,7 @@ import { Globe, Scale, } from "lucide-react"; +import type { CatalogueTableLayout } from "@/lib/coursemap/catalogue-kinds"; import styles from "./catalogue-table.module.css"; export { @@ -38,7 +39,7 @@ export function DataTableShell({ footer?: ReactNode; selectable?: boolean; imports?: boolean; - layout?: "public-courses" | "users" | "directory"; + layout?: CatalogueTableLayout; }) { return (
; const params = await searchParams; const labels = CATALOGUE_KIND_LABELS[kind]; - const page = loadCatalogueDirectoryPage({ + const page = await loadCatalogueDirectoryPage({ kind, academicYear, query: first(params.q) ?? "", page: Number(first(params.page)) || 1, }); return ( - +

{labels.plural}

- - } - > - - +
); } - -async function DirectoryContent({ - page, -}: { - page: ReturnType; -}) { - const resolved = await page; - return ; -} diff --git a/apps/web/ui/admin/operations/discovery-detail.tsx b/apps/web/ui/admin/operations/discovery-detail.tsx index 50fa7daf..aa5d8b15 100644 --- a/apps/web/ui/admin/operations/discovery-detail.tsx +++ b/apps/web/ui/admin/operations/discovery-detail.tsx @@ -1,3 +1,4 @@ +import { ADMIN_CATALOGUE_OPERATIONS_PATH } from "@/lib/coursemap/catalogue-kinds"; import { ArrowLeft } from "lucide-react"; import Link from "next/link"; import { @@ -8,7 +9,6 @@ import { import { Badge } from "@coursemap/ui/components/badge"; import type { DiscoveryCheckDetail } from "@/lib/coursemap/admin-operations"; import { formatDuration, formatTimestamp } from "./operations-format"; -import { CATALOGUE_OPERATIONS_PATH } from "./operations-tabs"; /** * One listing check. Only a complete check can retire a record, so its @@ -23,7 +23,7 @@ export function DiscoveryDetailView({