Delete the old top-level page files whose content was migrated into service-page tabs in slices 5-9: - pages/Media.tsx, Applications.tsx (-> MediaTab) - pages/FileBrowser.tsx, FileBrowser.impl.tsx (-> FilesTab) - pages/Actions.tsx (-> ActionsTab) - pages/Users.tsx, UsersPage.impl.tsx (replaced by Authentik tabs) - components/BackupsPage.tsx (-> JobsTab) - components/ObservabilityPage.tsx (split into Alerts/Links/Metrics tabs) - hooks/useUsers.ts (orphaned after Users page deletion) - the corresponding page test files (Media, FileBrowser, Applications, Actions, UsersPage) that tested the deleted pages directly. The service-tab components are the live implementations; ServicePage renders them. No live code references the deleted files. Docs: append an Information Architecture section to REQUIREMENTS.md documenting the services-as-hub model (nav shape, service-page tabs, service type registry, Users->Authentik, Observability split, legacy route 404s, empty state). Add a CHANGELOG entry under [Unreleased]. 92 frontend tests pass (was 112; -20 deleted page tests); 271 backend tests pass; lint/build green. Refs openspec/changes/services-as-hub-ia/ (tasks slice 11).
7.6 KiB
Slice 3 Review — Media table mobile layout (mobile-responsive-parity)
Reviewer: fresh adversarial pass. Scope: unstaged diff on
frontend/src/pages/Media.tsx and frontend/src/pages/__tests__/Media.test.tsx.
Verification commands run
| Command | Result |
|---|---|
npm run lint |
pass (0 errors; 2 pre-existing warnings in UsersPage.impl.tsx, unrelated to Slice 3) |
npm run build |
pass (tsc -b + vite, built in 854ms) |
npm run test |
pass (25 files, 94 tests) |
Point-by-point
1. Desktop non-regression — CONFIRMED CORRECT
The diff is a clean branch-add, not a rewrite. The DataTable block was moved
into the else of isMobile ? <mobile> : <DataTable> with every prop byte-for-
byte identical to the pre-change version (Media.tsx:671-697):
columns, data, getRowId, enableRowSelection, rowSelection,
onRowSelectionChange, onRowClick, enableColumnVisibilityToggle,
columnVisibility, onColumnVisibilityChange, enablePagination,
manualPagination, pagination, onPaginationChange, pageSizeOptions,
rowCount, emptyMessage. The wrapping <div className="rounded-lg border bg-card"> and the status?.exists gate are preserved on both branches. No
desktop prop was dropped, renamed, or reordered. R3.6 / R10.1 satisfied.
2. Mobile card fields — CONFIRMED CORRECT
mediaCardFields (Media.tsx:83-96) matches the real MediaItem type
(types/index.ts:274), not the design doc's illustrative field names:
title(string) — primary ✓size→r.size || "-"(string, null-safe) ✓hdr→r.hdr || "-"(string, null-safe) ✓library→r.library || "-"(string, null-safe) ✓year(number | null) →r.year != null ? String(r.year) : "-"✓ explicitly null-safe
5 fields total (1 primary + 4), inside the spec's 3–5 range (R3.2). No
undefined access possible — every field guards against empty/null. The design
example used size_display/is_hdr/library_name (illustrative); the worker
correctly used the real keys. Good.
3. Pagination duplication — NOT A BUG; acceptable tech debt
MediaMobilePagination (Media.tsx:107-188) duplicates DataTablePagination
(data-table.tsx). I verified the semantics match exactly:
| Concern | DataTable | MediaMobilePagination | Match |
|---|---|---|---|
| Rows count | rowCount ?? 0 (manual) |
totalRows = total (queryResult?.total ?? 0) |
✓ |
| pageCount | Math.max(1, Math.ceil(rowCount/pageSize)) |
totalPages = Math.max(1, Math.ceil(total/pageSize)) (Media.tsx:403) |
✓ |
| Page-size change | table.setPageSize() → resets pageIndex:0 |
onPaginationChange(() => ({pageIndex:0, pageSize:Number(value)})) |
✓ |
| Prev disabled | !getCanPreviousPage() = pageIndex>0 inverted |
pageIndex <= 0 |
✓ |
| Next disabled | !getCanNextPage() = pageIndex>=pageCount-1 inverted |
pageIndex >= pageCount - 1 |
✓ |
| Page indicator | Page {pageIndex+1} of {pageCount} |
same | ✓ |
No off-by-one, no missing clamp, no stale state. The mobile component reads
pageIndex/pageSize derived the same way as the controlled pagination
state fed to DataTable (Media.tsx:355-356), so the two paths can't drift on
values.
Could they reuse DataTable's pagination by extracting it? That would require
editing the shared data-table.tsx (export DataTablePagination or split a
TablePagination), which is explicitly out of scope for Slice 3 and would risk
R3.6/R10.1 (the shared component powers the desktop path). Acceptable to defer
to a follow-up refactor slice. Non-blocking smell, not a must-fix.
4. Row click navigation — CONFIRMED CORRECT
handleRowClick (Media.tsx:398-400) is passed unchanged to
MobileCardRow.onRowClick (Media.tsx:659). MobileCardRow makes the whole
card a <button type="button"> with onClick={() => onRowClick(row)}
(mobile-card.tsx), so a tap navigates to /files?path=<encoded>. The test
"navigates to the file browser when a card is tapped on mobile" asserts
navigate is called once with the encoded path. ✓
5. Column-visibility toggle hidden below md (R3.5) — CONFIRMED CORRECT
On the mobile branch only MobileCardRow renders; no DataTable, so the
Columns DropdownMenu never mounts. Tested explicitly:
hides the column-visibility toggle below md asserts
queryByRole("button", { name: /Columns/ }) is null. The desktop test asserts
the same button is present at desktop width. ✓ R3.5 satisfied both ways.
6. Test quality — GOOD
- matchMedia mock (
Media.test.tsx:159-183): correct. It discriminates onquery.includes("768")souseIsMobile(768px) toggles with the flag whileusePrefersSmallScreen(900px) staysfalse— which is the right default for the desktop path (noMOBILE_HIDDEN_COLUMNSforcing). Adds/removes listeners are no-ops; sufficient for jsdom. Applied inbeforeEachdefaulting to desktop, overridden per-test viasetMatchMedia(true). - Desktop test asserts BOTH a DataTable column header (
Title) AND the Columns toggle button. ✓ - The 5 new tests assert real behavior: card titles + field labels render, no column headers leak, pagination renders (2 rows, Page 1 of 1, Previous disabled), card tap navigates, desktop renders DataTable. None are tautological.
7. enableRowSelection on mobile — NOT A REGRESSION (minor spec note)
The mobile card does not render a selection checkbox; MobileCardRow has no
selection affordance. However, rowSelection/setRowSelection in Media.tsx
is vestigial: grepping the file, the state is declared (Media.tsx:326) and
passed to DataTable, but nothing in Media.tsx consumes it — there is no batch
action, bulk-delete, or "selected count" UI wired to it. So dropping selection
on mobile breaks no actual workflow, because no batch workflow exists on desktop
either. R3.3's literal "selection semantics preserved on the card" is loosely
violated, but the spec's "(tap target = the whole card where applicable)"
clause and the absence of any selection consumer make navigation the correct
primary mobile interaction. Non-blocking note. If a batch action is ever
added to Media later, mobile selection will need an explicit follow-up.
Other observations (non-blocking)
- The mobile card is wrapped in
<div className="p-4">inside the bordered card, thenMediaMobilePaginationsits below it inside the samerounded-lg border bg-card. Consistent with the desktop wrapping. Fine. isSmall(usePrefersSmallScreen, 900px) is still used foreffectiveColumnVisibilitydesktop sub-breakpoint hiding;isMobile(useIsMobile, 768px) drives the card/table branch. Two hooks, two distinct purposes, correctly not conflated. The design notesuseIsMobileshould replace ad-hoc matchMedia;usePrefersSmallScreenis a separate 900px concern left intact — acceptable, not in Slice 3's scope to consolidate.- Lint warnings are in
UsersPage.impl.tsx, pre-existing, unrelated.
Verdict: commit
No blockers. Desktop path is byte-identical (non-regression confirmed), mobile fields are null-safe and type-correct, pagination duplication is semantically equivalent (tech debt, not a bug), navigation preserved, column-visibility correctly hidden, tests assert real behavior on both breakpoints, and lint/build/test are green. The two non-blocking notes (pagination duplication; vestigial selection not surfaced on mobile) are appropriate follow-up items, not commit gates.
Acceptance
- criterion-1 (implement the change without widening scope): satisfied. Only the two Slice 3 files changed; DataTable and other slices untouched; no scope creep into shared-component refactors.