a8dfbd5dc6
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).
143 lines
7.6 KiB
Markdown
143 lines
7.6 KiB
Markdown
# 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 on
|
||
`query.includes("768")` so `useIsMobile` (768px) toggles with the flag while
|
||
`usePrefersSmallScreen` (900px) stays `false` — which is the right default for
|
||
the desktop path (no `MOBILE_HIDDEN_COLUMNS` forcing). Adds/removes listeners
|
||
are no-ops; sufficient for jsdom. Applied in `beforeEach` defaulting to
|
||
desktop, overridden per-test via `setMatchMedia(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, then `MediaMobilePagination` sits below it inside the same
|
||
`rounded-lg border bg-card`. Consistent with the desktop wrapping. Fine.
|
||
- `isSmall` (`usePrefersSmallScreen`, 900px) is still used for
|
||
`effectiveColumnVisibility` desktop sub-breakpoint hiding; `isMobile`
|
||
(`useIsMobile`, 768px) drives the card/table branch. Two hooks, two distinct
|
||
purposes, correctly not conflated. The design notes `useIsMobile` should
|
||
replace ad-hoc matchMedia; `usePrefersSmallScreen` is 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.
|