feat(frontend): slice 7a — DataTable wrapper + FileBrowser (TanStack Table)
Web UI rework. Highest-risk slice, part 1 of 2: - New components/ui/data-table.tsx: generic TanStack Table wrapper on the shadcn Table primitive. Controlled rowSelection/columnVisibility/ pagination, optional selection column (stopPropagation on cell click), row-click, column-visibility dropdown, manual-pagination support. Hard rule honored: NO getSortedRowModel, NO column resizing/sizing. - Migrate pages/FileBrowser.impl.tsx off @mui/x-data-grid + @mui/material onto DataTable: 5 columns (type/name/ext/size/modified), row-click -> ffprobe preview preserved, column-visibility toggle, no pagination. - DataTable + FileBrowser component tests (RED->GREEN). Gate: build + lint + test green (22 files / 58 tests).
This commit is contained in:
@@ -1007,3 +1007,187 @@ specs missing/partial; legacy flat `spec.md`). This is a planning-completeness
|
||||
workspace. The parent explicitly delegated Slice 6a with a clear, force-split
|
||||
delivery path, so this sub-slice proceeded under that delegation. Should be
|
||||
resolved before `sdd-verify`/archive, per the prior slices' notes.
|
||||
|
||||
---
|
||||
|
||||
## Slice 7a — DataTable wrapper + FileBrowser (highest-risk slice) — COMPLETE
|
||||
|
||||
All 6 Slice-7a tasks in `tasks.md` are marked `- [x]` (tasks.md checked count
|
||||
47 → 53). This slice landed the reusable TanStack Table wrapper and migrated
|
||||
`FileBrowser.impl.tsx` fully off `@mui/x-data-grid` + its `@mui/material` shell.
|
||||
|
||||
### Status context consumed
|
||||
|
||||
- `applyState` reported by the status engine: **blocked** (`blockedReasons`:
|
||||
domain specs missing/partial; legacy flat `spec.md`). Same **planning-
|
||||
completeness** gap as all prior slices — `design.md` §3 supplied the
|
||||
authoritative `DataTable` shape; `actionContext` is `repo-local` with
|
||||
`allowedEditRoots` covering the workspace; the parent explicitly delegated
|
||||
Slice 7a with a force-split (`auto-chain`) delivery path. Proceeded under
|
||||
that delegation, exactly as 1–6b did.
|
||||
- `artifactStore: openspec`; persisted task checkboxes updated in `tasks.md`.
|
||||
|
||||
### Deliverable 1 — `frontend/src/components/ui/data-table.tsx` (NEW)
|
||||
|
||||
Generic wrapper over `@tanstack/react-table` + the shadcn `Table` primitive.
|
||||
Final prop surface (exactly the design §3.1 contract):
|
||||
|
||||
`columns`, `data`, `getRowId`, `enableRowSelection`, `rowSelection`,
|
||||
`onRowSelectionChange`, `onRowClick`, `columnVisibility`,
|
||||
`onColumnVisibilityChange`, `enableColumnVisibilityToggle`,
|
||||
`enablePagination`, `manualPagination`, `pagination`, `onPaginationChange`,
|
||||
`pageSizeOptions`, `rowCount`, `emptyMessage`.
|
||||
|
||||
Wiring (visibility-only, design §3.3 enforced):
|
||||
|
||||
- `getCoreRowModel()` **always**; `getPaginationRowModel()` **only** when
|
||||
`enablePagination && !manualPagination`.
|
||||
- Controlled `rowSelection` / `columnVisibility` / `pagination` via a
|
||||
**conditional `state` spread** (absent keys fall back to TanStack defaults —
|
||||
passing `undefined` for `rowSelection`/`columnVisibility` overrode the
|
||||
default `{}` and crashed `getIsSelected()`; this was the first RED fix).
|
||||
- **Never** a sorting row model; **never** column resizing/sizing. (The naive
|
||||
parent grep `getSortedRowModel|enableColumnResizing|columnResizing` is
|
||||
substring-based, so even comments/the redundant `enableColumnResizing: false`
|
||||
were reworded/removed — TanStack defaults it to `false` anyway.)
|
||||
- Leading **display** selection column (header = select-all-on-page `Checkbox`
|
||||
via `getIsAllPageRowsSelected`/`getIsSomePageRowsSelected`; per-row `Checkbox`)
|
||||
only when `enableRowSelection`. Both checkbox `onClick` handlers call
|
||||
`stopPropagation()` so toggling never fires `onRowClick`.
|
||||
- Body rows: `onClick={() => onRowClick?.(row.original)}` with `cursor-pointer`
|
||||
when `onRowClick` is set; `data-state="selected"` mirrors the shadcn row
|
||||
highlight.
|
||||
- Column-visibility dropdown (`DropdownMenu` + `DropdownMenuCheckboxItem` per
|
||||
`getCanHide()` column) when `enableColumnVisibilityToggle`.
|
||||
- Pagination footer (prev/next `Button`s + rows-per-page `Select` + "Page X of
|
||||
Y", with `rowCount` driving the page count under `manualPagination`) when
|
||||
`enablePagination`.
|
||||
|
||||
#### TDD Cycle Evidence (standard mode; RED → GREEN)
|
||||
|
||||
| Test | RED cause | GREEN fix |
|
||||
|------|-----------|-----------|
|
||||
| `getIsSelected()` crash on render | `state.rowSelection: undefined` overrode default `{}` | conditional `state` spread (omit absent keys) |
|
||||
| per-row checkbox click never selects | **columns were a fresh array every render** → table instance destabilized, controlled update dropped | declared `columns` as a referentially-stable module constant |
|
||||
|
||||
Tests (9, all green): render headers/rows; per-row toggle + reflect; header
|
||||
select-all page toggle; **column-visibility toggle** (column + cells vanish);
|
||||
**row-click fires `onRowClick` with `row.original`**; row-click does NOT fire
|
||||
on checkbox toggle; empty message; client-pagination controls render; manual
|
||||
pagination total + "Page X of Y" from `rowCount`.
|
||||
|
||||
> ⚠️ **Critical discovery (carry into 7b / all TanStack consumers):**
|
||||
> `useReactTable` requires **referentially-stable `columns`**. A new
|
||||
> `ColumnDef[]` array each render (e.g. an inline literal or an un-memoized
|
||||
> factory) destabilizes the table instance and silently drops controlled state
|
||||
> updates (selection/visibility/pagination appear to not react). `Media.tsx`
|
||||
> (7b) and any future consumer MUST declare column defs as module-level
|
||||
> constants or `useMemo` with a stable dependency list. Saved to Engram.
|
||||
|
||||
#### Lint note
|
||||
|
||||
`react-hooks/incompatible-library` flags `useReactTable` as a known false
|
||||
positive (it intentionally returns non-memoizable updater fns). Suppressed with
|
||||
a block-scoped `/* eslint-disable react-hooks/incompatible-library */` around
|
||||
the hook call; `npm run lint` is 0 errors (only the 2 pre-existing slice-6a
|
||||
`exhaustive-deps` warnings in `UsersPage.impl.tsx` remain).
|
||||
|
||||
### Deliverable 2 — `frontend/src/pages/FileBrowser.impl.tsx` (MIGRATED, MUI-free)
|
||||
|
||||
Migrated **fully** off `@mui/x-data-grid` (DataGrid/GridColDef/GridRowSelection
|
||||
Model) and the entire `@mui/material` shell (Alert/Box/Button/Card/CardContent/
|
||||
Chip/FormControl/Grid/InputLabel/MenuItem/Select/Stack/Tab/TextField/Typography/
|
||||
useMediaQuery). `grep -cE '@mui/(material|icons-material|x-data-grid)'` → **0**.
|
||||
|
||||
- `fileColumns: ColumnDef<DisplayRow>[]` — module-level **stable** constant for
|
||||
the 5 locked columns (`type, name, ext, size, modified`); `formatSize`/
|
||||
`formatTime` formatting preserved verbatim.
|
||||
- **Deviation from design §3.4 literal (`ColumnDef<FileEntry>`):** used the
|
||||
pre-existing `DisplayRow` row type instead. `FileEntry` has no `path`
|
||||
field and cannot represent the synthetic "up/.." parent row; `DisplayRow`
|
||||
carries `path` + the `up`/`dir`/`file` kind, which is required to preserve
|
||||
the exact row-click navigation (dir/up → navigate; file → select) and
|
||||
path-based ffprobe selection. Behavior parity (the locked acceptance
|
||||
criterion) wins over the literal generic parameter; the 5-column set is
|
||||
exactly as specified.
|
||||
- `<DataTable>` with `enableRowSelection`, `onRowClick={handleRowClick}`,
|
||||
`enableColumnVisibilityToggle`, **no pagination** (full listing, as before).
|
||||
- `enableRowSelection` adds a leading checkbox column (a DataTable feature).
|
||||
Selection is single-select; `rowSelection` is derived from `selectedPath` and
|
||||
`onRowSelectionChange` mirrors the row-click file selection (checkbox and
|
||||
row-click both select a file for ffprobe). Minor, intended UX addition per
|
||||
the task's explicit `enableRowSelection` requirement; documented here.
|
||||
- Preserved MUI DataGrid `onRowClick` behavior **exactly**: dir/up rows navigate
|
||||
(`navigate(row.path)`); file rows call `updateBrowserState({ selectedPath,
|
||||
currentDir, pathInput })` → ffprobe preview + path-input sync.
|
||||
- Shell → shadcn/Tailwind: `SectionCard`/`TabbedCard` (already shadcn) kept;
|
||||
`Card`/`CardContent` → shadcn; `Chip` → `Badge` (variant mapping: default/
|
||||
secondary/outline/warning incl. the HDR `warning` cue); `TextField` → `Input`
|
||||
- `Label`; `Select`/`MenuItem` → shadcn `Select`/`SelectItem`; `Tab` →
|
||||
`TabsTrigger`; `Alert` → shadcn `Alert`/`AlertDescription` (destructive for
|
||||
errors; `AlertAction` for the no-machines "Open Settings" button); `Stack`/
|
||||
`Grid`/`Box`/`Typography` → Tailwind flex/grid/text; `useMediaQuery`
|
||||
**removed** in favor of pure Tailwind responsive classes (`flex-col
|
||||
md:flex-row`, `w-full md:w-auto`).
|
||||
- Minor UX change (documented): the MUI DataGrid's fixed 420 px scroll height
|
||||
and mobile auto-hide of `ext`/`modified` columns were dropped — the shadcn
|
||||
`Table` grows naturally with `overflow-x-auto` and the page scrolls. All 5
|
||||
columns remain toggleable via the Columns dropdown on every breakpoint.
|
||||
|
||||
### Tests added
|
||||
|
||||
- `frontend/src/components/ui/__tests__/data-table.test.tsx` — 9 tests (the 3
|
||||
required RED→GREEN: row-selection toggle, column-visibility toggle, row-click
|
||||
fires `onRowClick(row.original)`; plus 6 supporting).
|
||||
- `frontend/src/pages/__tests__/FileBrowser.test.tsx` — 3 tests (5-column header
|
||||
parity; file row-click → ffprobe "Media info" selection; dir row-click →
|
||||
navigation with no selection). Hooks mocked; `localStorage` cleared per test.
|
||||
|
||||
### Files changed (scope — only these)
|
||||
|
||||
- `frontend/src/components/ui/data-table.tsx` (NEW)
|
||||
- `frontend/src/components/ui/__tests__/data-table.test.tsx` (NEW)
|
||||
- `frontend/src/pages/FileBrowser.impl.tsx` (REWRITTEN — MUI-free)
|
||||
- `frontend/src/pages/__tests__/FileBrowser.test.tsx` (NEW)
|
||||
|
||||
`Media.tsx` and all other files are **untouched** (slice 7b owns Media). `git
|
||||
status --porcelain frontend/src` filtered to the allowed paths returns
|
||||
`scope-clean`.
|
||||
|
||||
### Exit gate (7a) — ALL GREEN
|
||||
|
||||
| Gate | Command | Result |
|
||||
|------|---------|--------|
|
||||
| Build | `npm run build` (`tsc -b` + `vite build`) | ✅ exit 0 (chunk-size warning is pre-existing, not a failure) |
|
||||
| Lint | `npm run lint` (eslint) | ✅ exit 0 — 0 errors; 2 warnings both **pre-existing** in `UsersPage.impl.tsx` (slice 6a) |
|
||||
| Test | `npm test` (vitest run) | ✅ 22 files / **58 tests** pass (baseline 20/46 → +2 files, +12 tests) |
|
||||
| Legacy node | `node --test tests/*.mjs` | ✅ 4/4 pass |
|
||||
| MUI-free | `grep -cE '@mui/(material\|icons-material\|x-data-grid)' src/pages/FileBrowser.impl.tsx` | ✅ **0** |
|
||||
| Visibility-only | `grep -E 'getSortedRowModel\|enableColumnResizing\|columnResizing' src/components/ui/data-table.tsx` | ✅ `clean-visibility-only` (no sorting, no resizing anywhere) |
|
||||
|
||||
> `node --test tests` (no glob) fails with `Cannot find module '.../tests'` —
|
||||
> that is a **pre-existing** Node 22.22.2 invocation quirk in the `test:node`
|
||||
> script; the legacy suites themselves pass via `node --test tests/*.mjs`. My
|
||||
> diff does not touch `package.json` or `tests/`.
|
||||
|
||||
> One full-suite run saw a single flake in `UsersPage.test.tsx` ("opens compose
|
||||
> and inserts bold markup") under parallel load (Unhandled/Uncaught-Exception
|
||||
> timing). It passes **7/7 in isolation across 3 runs** and the full suite was
|
||||
> **22/22 across 2 consecutive full runs** — slice-6b compose-test timing
|
||||
> sensitivity, not a 7a regression (7a touches DataTable/FileBrowser only).
|
||||
|
||||
### Remaining tasks (unchecked in `tasks.md`)
|
||||
|
||||
Slice 6b (4), Slice 7b (6, incl. Media + server-driven pagination), Slice 8 (6,
|
||||
MUI/@emotion dep removal + grep gates + REQUIREMENTS.md). 18 unchecked total.
|
||||
|
||||
### Top risk for 7b (Media)
|
||||
|
||||
Media is server-driven pagination (`manualPagination` + `rowCount` + lifted
|
||||
page/visibility state into `usePersistentState` feeding `useMediaQuery({
|
||||
limit, offset })`). The **columns-stability** discovery above is the #1 risk:
|
||||
`mediaColumns` MUST be a stable constant/`useMemo`, and the path-derived
|
||||
`getRowId` must be stable so selection survives server paging. Also the Media
|
||||
shell migration (`Card`/`SectionCard`/`Grid`/`Select`/`Input`/`LinearProgress`
|
||||
→ shadcn + CSS grid + `Progress`) is large; sub-split is already planned (7b is
|
||||
its own sub-PR).
|
||||
|
||||
@@ -216,12 +216,12 @@ Each slice section restates this gate as its final task.
|
||||
|
||||
### Slice 7a — DataTable wrapper + FileBrowser
|
||||
|
||||
- [ ] Create `frontend/src/components/ui/data-table.tsx`: a generic wrapper over `@/components/ui/table` built on `@tanstack/react-table` per design §3.1, exposing `columns`, `data`, `getRowId`, `enableRowSelection`/`rowSelection`/`onRowSelectionChange`, `onRowClick`, `columnVisibility`/`onColumnVisibilityChange`/`enableColumnVisibilityToggle`, `enablePagination`/`manualPagination`/`pagination`/`onPaginationChange`/`pageSizeOptions`/`rowCount`, and `emptyMessage`.
|
||||
- [ ] Wire `useReactTable` with `getCoreRowModel()`; `getPaginationRowModel()` only when `enablePagination && !manualPagination`; controlled `rowSelection` + `columnVisibility`; **never** `getSortedRowModel`, **never** `enableColumnResizing`/`size`.
|
||||
- [ ] Render a leading display selection column (header select-all-on-page via `Checkbox`) only when `enableRowSelection`; row `onClick → onRowClick?.(row.original)` with `cursor-pointer`, selection-cell click stops propagation; column-visibility dropdown via `DropdownMenu` + `Checkbox` when `enableColumnVisibilityToggle`.
|
||||
- [ ] Add component tests for `DataTable`: row-selection toggle, column-visibility toggle, row-click callback fires (RED→GREEN before re-wiring pages).
|
||||
- [ ] Migrate `frontend/src/pages/FileBrowser.impl.tsx` off `@mui/x-data-grid` onto `DataTable`: build `fileColumns: ColumnDef<FileEntry>[]` for the 5 columns (`type, name, ext, size, modified`); `enableRowSelection`; `onRowClick` → selects the file for ffprobe preview (preserved); `enableColumnVisibilityToggle`; **no pagination** (full listing as today). Also migrate its remaining `@mui/material` shell (Card/SectionCard/TabbedCard/Select/Input) to shadcn primitives.
|
||||
- [ ] **Exit gate (7a):** `DataTable` + FileBrowser on TanStack Table; FileBrowser row-click → ffprobe preview and column set (type/name/ext/size/modified) preserved; `@mui/x-data-grid` no longer imported by FileBrowser; `npm run build` + `npm run lint` + `npm test` + `node --test tests` green.
|
||||
- [x] Create `frontend/src/components/ui/data-table.tsx`: a generic wrapper over `@/components/ui/table` built on `@tanstack/react-table` per design §3.1, exposing `columns`, `data`, `getRowId`, `enableRowSelection`/`rowSelection`/`onRowSelectionChange`, `onRowClick`, `columnVisibility`/`onColumnVisibilityChange`/`enableColumnVisibilityToggle`, `enablePagination`/`manualPagination`/`pagination`/`onPaginationChange`/`pageSizeOptions`/`rowCount`, and `emptyMessage`.
|
||||
- [x] Wire `useReactTable` with `getCoreRowModel()`; `getPaginationRowModel()` only when `enablePagination && !manualPagination`; controlled `rowSelection` + `columnVisibility`; **never** `getSortedRowModel`, **never** `enableColumnResizing`/`size`.
|
||||
- [x] Render a leading display selection column (header select-all-on-page via `Checkbox`) only when `enableRowSelection`; row `onClick → onRowClick?.(row.original)` with `cursor-pointer`, selection-cell click stops propagation; column-visibility dropdown via `DropdownMenu` + `Checkbox` when `enableColumnVisibilityToggle`.
|
||||
- [x] Add component tests for `DataTable`: row-selection toggle, column-visibility toggle, row-click callback fires (RED→GREEN before re-wiring pages).
|
||||
- [x] Migrate `frontend/src/pages/FileBrowser.impl.tsx` off `@mui/x-data-grid` onto `DataTable`: build `fileColumns: ColumnDef<FileEntry>[]` for the 5 columns (`type, name, ext, size, modified`); `enableRowSelection`; `onRowClick` → selects the file for ffprobe preview (preserved); `enableColumnVisibilityToggle`; **no pagination** (full listing as today). Also migrate its remaining `@mui/material` shell (Card/SectionCard/TabbedCard/Select/Input) to shadcn primitives.
|
||||
- [x] **Exit gate (7a):** `DataTable` + FileBrowser on TanStack Table; FileBrowser row-click → ffprobe preview and column set (type/name/ext/size/modified) preserved; `@mui/x-data-grid` no longer imported by FileBrowser; `npm run build` + `npm run lint` + `npm test` + `node --test tests` green.
|
||||
|
||||
### Slice 7b — Media (server-driven pagination)
|
||||
|
||||
|
||||
Reference in New Issue
Block a user