Skip to content

feat(table): adopt glide-data-grid — spreadsheet-grade selection, copy-as-CSV, editing - #206

Merged
obvious-autobuild-staging[bot] merged 6 commits into
masterfrom
feat/table-datagrid
Sep 8, 2026
Merged

feat(table): adopt glide-data-grid — spreadsheet-grade selection, copy-as-CSV, editing#206
obvious-autobuild-staging[bot] merged 6 commits into
masterfrom
feat/table-datagrid

Conversation

@obvious-autobuild-staging

@obvious-autobuild-staging obvious-autobuild-staging Bot commented Sep 8, 2026

Copy link
Copy Markdown
Contributor

Why

The CSV table's selection model is the product's core editing surface, and David's bar is spreadsheet-grade: drag-select a rectangular range, extend it with Shift+Arrow, hold multi-range selections, copy any selection as clean CSV, and fill. The hand-rolled DOM table (PR #204) and the react-data-grid interim plan both fall short — RDG's range selection is partial (no shift+click extension, no keyboard ranges, no complete selection model; adazzle issues #1133/#2260). This PR moves the table to @glideapps/glide-data-grid 6.0.3 (MIT, canvas-rendered, React), where rectangular and multi-range selection are the library's core, per the amended spec (art_Z56fIBHg, snapshot d63dd7a0).

What

Data layer (ported from PR #205 @ cf191ff, not rebuilt):

  • app/src/lib/csvTable.tsserializeCsvTable(headers, rows): RFC 4180 writer that quotes and escapes any cell containing comma, quote, CR, or LF, plus exact byte-span row splices for edits, with the full round-trip test suite against the parser.
  • app/src/lib/tableView.ts — keeps what Glide does not do for us: view/source index mapping, debounced filtering, and the BigInt-safe comparator (16+ digit integers sort exactly, not through Number).
  • app/src/lib/tableSelection.ts (new) — serializes any selection shape (rectangular ranges, multi-range, whole rows, whole columns) to CSV through the same writer; ranges take precedence, the source-row gutter is excluded, and the table's delimiter is preserved.

Component (CsvTable.tsx, shared by both instances):

  • Input table: editable, spreadsheet-style commits. Batched Glide edits are sorted by view row, spliced into the raw CSV at exact byte spans (cumulative length deltas), and fed through onCellCommit → the shared input handler → PR feat(output): editable JSON output with guarded regeneration #198 guarded reconversion. Raw text and table view can never disagree.
  • Output table: strictly read-only — no onCellsEdited, no editing affordances.
  • Selection is controlled and keyed by source row index, so selection survives filter and sort changes; the gutter always shows the source row number.
  • Search stays ours: 150ms debounced input, live N of M count chip, ×/Esc clear, empty-filter state with a clear action, feeding filtered rows to the grid.
  • Sort state is ours too (Glide does not sort): single-column click cycle asc → desc → natural with header glyphs, numeric-aware comparator applied to derived rows.
  • Glide's native copy emits TSV — a capture-phase window copy interceptor replaces it with RFC 4180 CSV of the active selection whenever the grid holds focus (the cell editor still owns the clipboard during text edits).

Test strategy for a canvas grid: jsdom cannot exercise canvas, so DataEditorCore is swapped for app/src/test/glideDataEditorMock.tsx, which renders the same data through the component's real view logic and re-exposes the interaction callbacks as DOM buttons. Everything that is our code (toolbar, filtering, sorting state, selection bookkeeping, selection→CSV serialization, edit-commit splices, output read-only) is unit-tested — 414 tests green, including the 10k-row count chip and the App-level paste/edit routing suites. Genuine canvas gestures are validated with browser dogfood evidence below, not faked.

Visual density (spec gate): values read from master's CsvTable.tsx and applied through Glide's theme: 24px rows, 26px headers, 8px horizontal cell padding, 12px cell text / 11px semibold monospace uppercase headers, 110px minimum data columns, 40px source-row gutter, right-aligned row numbers, monospace numeric cells. Side-by-side before/after screenshots are attached in the QA evidence.

Accessibility (honest): the canvas grid has weaker screen-reader semantics than the DOM ARIA table it replaces — no per-cell role="gridcell" traversal. Keyboard navigation, focus management, and aria-rowcount on the grid surface are wired; per-cell ARIA was deliberately not invented. This is a real trade-off of the library swap, priced for the selection model David required.

Prerender: the canvas renders nothing in prerendered HTML by design; npm run build + prerender + verify-seo.sh pass (40,446 bytes of prerendered markup), and first paint shows the table without layout flash (PR #203 semantics).

Removed: the hand-rolled DOM grid internals and their DOM-specific selection/sort tests. PR #204's UX contracts survive, reimplemented through the library.

How to Review

  • Start with app/src/lib/tableSelection.ts (selection→CSV semantics) and the edit-commit splice in CsvTable.tsx (sorted batched edits → byte-span splices → guarded reconversion) — that's where correctness lives.
  • app/src/test/glideDataEditorMock.tsx is test infrastructure, not product code; it restates the consumed prop subset strongly and casts once at the mock boundary.
  • The five app-level test files changed only their assertions (canvas headers render uppercase; row-data assertions instead of raw header text).
  • Deliberately out of scope: output editing, structural row/column add-remove, multi-line cell editors.

Test Evidence

Visual density parity: desktop 1440px and 375px before/after vs master (24px rows, 26px header, muted uppercase headers, 40px gutter, monospace numerics)
Visual density parity: desktop 1440px and 375px before/after vs master (24px rows, 26px header, muted uppercase headers, 40px gutter, monospace numerics) — before
Visual density parity: desktop 1440px and 375px before/after vs master (24px rows, 26px header, muted uppercase headers, 40px gutter, monospace numerics) — after
Visual density parity: desktop 1440px and 375px before/after vs master (24px rows, 26px header, muted uppercase headers, 40px gutter, monospace numerics) — before
Visual density parity: desktop 1440px and 375px before/after vs master (24px rows, 26px header, muted uppercase headers, 40px gutter, monospace numerics) — after

Hard-bar gestures: drag-rect selection, Ctrl+C copy, Shift+Arrow extension, Ctrl+click multi-range, fill-handle drag, raw source updated
Hard-bar gestures: drag-rect selection, Ctrl+C copy, Shift+Arrow extension, Ctrl+click multi-range, fill-handle drag, raw source updated — recording

Cell edit commits through guarded reconversion; Raw view reflects the edit
Cell edit commits through guarded reconversion; Raw view reflects the edit — recording

Search filter, sort, column selection, row selection, copy-as-CSV flow
Search filter, sort, column selection, row selection, copy-as-CSV flow — recording

  • Local: full Vitest suite (414 tests), lint, tsc -b --noEmit, production build with committed app/dist, verify-seo.sh — all green.
  • Browser dogfood (WebM, against this head SHA): filter → sort → column select → row select → copy-as-CSV, and edit a cell → reconvert → Raw view shows the same edit. Drag-select rectangle, Shift+Arrow extension, multi-range, and fill are exercised there too, per the spec's gesture bar.
  • Density parity: side-by-side before (master) / after (this branch) screenshots at desktop and 375px.

🔗 Obvious Project · 🧵 Obvious Thread

…rom #205)

Swap the hand-rolled table internals for @glideapps/glide-data-grid 6.0.3
(canvas-rendered, first-class rectangular + multi-range selection). Port
the RFC 4180 serializer and its round-trip suite from feat/table-editing
@ cf191ff, and keep the BigInt-safe comparator / filtering helpers in
tableView.ts — Glide does not sort or filter for us. New dependency-free
tableSelection.ts serializes row / column / rectangular selections into
CSV through the same writer.
CsvTable runs on DataEditorCore: source-row identity for row selection,
single-column numeric-aware sort state, 150ms debounced search with a
count chip, header-click column selection, and a window copy interceptor
that replaces Glide's native TSV with RFC 4180 CSV of the selection.
jsdom cannot exercise canvas, so tests swap DataEditorCore for a mock
that drives the component's real handlers; canvas gestures get browser
evidence instead of faked unit tests.
Input-pane edits commit through onCellCommit: each batched Glide edit
splices its exact row span out of the raw CSV and feeds the shared
input handler, so raw text and table view never disagree and PR #198's
dirty/revert/discard semantics stay intact. Output table remains
strictly read-only. Changelog entry 9 announces the rebuilt table;
dist rebuilt and committed.
Permalink, pane-state, paste-routing, and output-editing suites render
the real App, so they import the shared glide mock and assert on row
data (canvas headers render uppercase) instead of raw header text.
…w rows

The stacked mobile layout can hand the table pane less height than the
grid's natural size (the split container shrinks; master's DOM table
overflowed visibly in the same case). Floor the wrapper at the header
plus up to 12 rows via minHeight — flex-1 still fills taller panes and
Glide scrolls internally — so a fresh 375px load paints the table
instead of a header sliver.
Two real-grid-only editor defects the jsdom mock could not catch:

1. Glide mounts its cell-editor overlay through document.getElementById('portal'),
   which did not exist — every activation logged "Cannot open Data Grid overlay
   editor, because portal not found". Added <div id="portal"> as the last child
   of app/index.html.

2. DataEditorCore builds no cell-renderer map on its own, so the overlay shell
   mounted with no text editor inside. Pass renderers={AllCellRenderers}.

Verified live on the dev server: two-click activation opens the portal
textarea, typing + Enter commits through the guarded reconversion, and Raw
view reflects the edit. Full suite 415 green, lint/tsc clean, dist rebuilt,
verify-seo.sh passed.
@obvious-autobuild-staging
obvious-autobuild-staging Bot marked this pull request as ready for review September 8, 2026 02:08
@obvious-autobuild-staging
obvious-autobuild-staging Bot merged commit 85651b0 into master Sep 8, 2026
2 checks passed
@obvious-autobuild-staging
obvious-autobuild-staging Bot deleted the feat/table-datagrid branch September 8, 2026 02:11
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant