Skip to content

Persist grid configuration in the URL (BL-16486)#613

Draft
hatton wants to merge 5 commits into
masterfrom
BL-16486-Grid-Config-URL
Draft

Persist grid configuration in the URL (BL-16486)#613
hatton wants to merge 5 commits into
masterfrom
BL-16486-Grid-Config-URL

Conversation

@hatton

@hatton hatton commented Jun 30, 2026

Copy link
Copy Markdown
Member

[Claude Opus 4.8]

What & why

The grid screens (Book, Language, Country, Uploader) let users sort, filter, show/hide/reorder/resize columns — but that state was ephemeral or in localStorage, so you couldn't bookmark or share a URL that reproduces your view. This adds URL persistence so a grid view can be bookmarked or shared and restored exactly (BL-16486).

How

  • gridUrlConfig.ts (new, pure/no-React): the serialization.
    • cols = the visible columns in display order — one param carrying both visibility and order (listed = shown, unlisted = hidden). Omitted when the view matches the factory default.
    • sort = name:asc|desc; widths = name:px (resized columns only, rounded to whole pixels); and one readable per-column filter param keyed by a short per-column urlKey, e.g. ?in=true&lv=4.
    • Everything is encoded relative to factory defaults and in each column's short urlKey, so URLs stay compact, an absent param means "default", and a link reproduces exactly the columns it names (stable if defaults later change).
  • useGridConfigInUrl.ts (new): local React state drives the controlled DevExpress grid; the URL is mirrored with native history.replaceState (no router re-render, so the filter input keeps focus while typing) and re-read on popstate. localStorage stays the personal default; the URL wins when present; a bare URL is backfilled from the saved layout so it's shareable.
  • Wired into all four *GridControlInternal.tsx (DevExpress FilteringState/SortingState/TableColumnVisibility/TableColumnResizing made controlled); a short urlKey added to every column.
  • Grids skip the login gate on localhost for easier dev.

Tests

  • gridUrlConfig.test.ts (pure serialization + edge cases) and useGridConfigInUrl.test.tsx (hook integration against real jsdom history: write, restore, popstate, bare-URL backfill, precedence, typing-focus regression). Full suite green; ESLint + production build pass.

Notes / limitations

  • Live-tested in a real browser as a logged-out (non-moderator) user on localhost; moderator-only column paths are covered by the (column-agnostic) unit/integration tests, not live.
  • Column-width persistence makes a resized column fixed-pixel (inherent to DevExpress controlled resizing); unresized columns stay "auto".

🤖 Generated with Claude Code


This change is Reviewable

The Book, Language, Country, and Uploader grids now keep their sort,
per-column filters, visible columns + order, and resized widths in the
URL, so a view can be bookmarked or shared and restored exactly.

- New gridUrlConfig.ts: pure (no-React) serialization. `cols` lists the
  visible columns in display order (carrying both visibility and order);
  `sort`, `widths`, and one readable per-column filter param keyed by a
  short per-column `urlKey`. Encoded relative to factory defaults, so a
  default view keeps the URL bare and absent params mean "default".
- New useGridConfigInUrl.ts: local React state drives the controlled
  grid; the URL is mirrored via native history.replaceState (no router
  re-render, so the filter input keeps focus while typing) and re-read on
  popstate. localStorage stays the personal default; the URL wins when
  present; a bare URL is backfilled from the saved layout so it's shareable.
- Wired into all four *GridControlInternal.tsx (DevExpress plugins made
  controlled); added a short urlKey to every column.
- Grids skip the login gate on localhost for easier dev.

Tests: gridUrlConfig.test.ts + useGridConfigInUrl.test.tsx.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@hatton

hatton commented Jun 30, 2026

Copy link
Copy Markdown
Member Author

@devin review

[Claude Opus 4.8] — triggering a Devin review of this draft PR.

@greptile-apps

greptile-apps Bot commented Jun 30, 2026

Copy link
Copy Markdown

Greptile Summary

This PR adds full URL persistence for the four grid screens (Book, Language, Country, Uploader), so a sorted/filtered/reordered column view can be bookmarked or shared. It introduces gridUrlConfig.ts (pure serialization) and useGridConfigInUrl.ts (React hook mirroring state to the address bar via history.replaceState), then wires all four grid controls into controlled DevExpress mode. It also extracts getColumnsVisibleToUser as a shared helper and removes the now-redundant per-component useEffect column-filtering blocks.

  • gridUrlConfig.ts + useGridConfigInUrl.ts: compact, collision-free URL encoding (cols carries visibility+order together; each column gets a short urlKey); native replaceState avoids router re-renders during typing; popstate re-reads the URL; bare-URL backfill makes an existing localStorage layout shareable; unavailable-column sort/filter entries are preserved in state/URL so they survive auth changes without clobbering shared links.
  • Grid controls (GridControlInternal, CountryGridControlInternal, LanguageGridControlInternal, UploaderGridControlInternal): switched from uncontrolled (defaultFilters/defaultSorting/defaultHiddenColumnNames/defaultColumnWidths) to fully controlled DevExpress props; the old per-component column-reconciliation useEffect blocks are replaced by reconcileColumnOrder inside the hook.
  • DataSource.isLocalhost added to relax the login gate on loopback addresses during development, and ChoicesFilterCell/TagExistsFilterCell are made fully controlled so back/forward navigation can't leave filter UI out of sync with actual filter state.

Important Files Changed

Filename Overview
src/components/Grid/gridUrlConfig.ts New pure serialization module for grid URL config — sort/cols/widths/filter encode+decode. Logic is sound and well-tested; minor asymmetry in encodeWidths (allows non-positive numerics through; decodeWidths rejects them) is a cosmetic edge case.
src/components/Grid/useGridConfigInUrl.ts New React hook wiring grid config to URL via native replaceState (no re-render on filter typing). Precedence logic, popstate handling, bare-URL backfill, and unavailable-column preservation are all correctly implemented.
src/components/Grid/GridControlInternal.tsx Migrated from localStorage-only useStorageState + uncontrolled DevExpress to controlled mode via useGridConfigInUrl. The localStorage-reconciliation useEffect is replaced by reconcileColumnOrder inside the hook.
src/components/Grid/GridColumns.tsx Added urlKey field to IGridColumn, getColumnsVisibleToUser helper, bookGridUrlKeys map, and made ChoicesFilterCell/TagExistsFilterCell fully controlled (removed local state copies that could stale-diverge from back/forward navigation).
src/connection/DataSource.ts Added isLocalhost() covering localhost, 127.0.0.1, ::1, and [::1] — correctly constrained to loopback-only to avoid opening the dev gate on LAN-exposed servers.
src/components/Grid/GridPage.tsx Dev-only login bypass added using isLocalhost(); change is minimal and intentionally constrained to loopback hostnames.
src/components/AggregateGrid/AggregateGridPage.tsx Same localhost login bypass as GridPage; identical pattern, minimal change.
src/components/Grid/gridUrlConfig.test.ts Comprehensive pure-serialization tests covering round-trips, edge cases (empty values, hex, negatives, dedup), and all encode/decode helpers.
src/components/Grid/useGridConfigInUrl.test.tsx 547-line integration test suite driving the hook against real jsdom history; covers write, restore, popstate, bare-URL backfill, initialFilters precedence, availableColumnNames gating, and typing-focus regression.
src/components/CountryGrid/CountryGridControlInternal.tsx Migrated to controlled DevExpress grid via useGridConfigInUrl; mirrors GridControlInternal pattern consistently.
src/components/LanguageGrid/LanguageGridControlInternal.tsx Migrated to controlled DevExpress grid via useGridConfigInUrl; mirrors GridControlInternal pattern consistently.
src/components/UploaderGrid/UploaderGridControlInternal.tsx Migrated to controlled DevExpress grid via useGridConfigInUrl; mirrors GridControlInternal pattern consistently.

Reviews (5): Last reviewed commit: "Fix stale filter-cell display after back..." | Re-trigger Greptile

Comment thread src/components/Grid/gridUrlConfig.ts Outdated
Comment thread src/components/Grid/useGridConfigInUrl.ts
- parseGridConfigFromSearch: drop the misleading non-null assertions after
  toName(); use type-guard predicates so the .filter() actually narrows the
  element type (no runtime change).
- useGridConfigInUrl backfill: check initial.filters with .length, matching
  sortings/widths, so a key-present-but-empty param (e.g. ?ti=) doesn't
  spuriously suppress the bare-URL backfill.
- Login-gate dev bypass: treat all loopback hostnames (localhost, 127.0.0.1,
  ::1) as local, not just "localhost". LAN addresses still require login.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@hatton

hatton commented Jun 30, 2026

Copy link
Copy Markdown
Member Author

[Claude Opus 4.8] Re: the localhost-only dev bypass (Greptile, GridPage.tsx / AggregateGridPage.tsx) — addressed in 4baf00a: both gates now treat all loopback hostnames (localhost, 127.0.0.1, ::1) as local. I deliberately did not include LAN addresses: when the dev server is bound to a LAN IP it's network-exposed, so I'd rather keep the login requirement there.

@hatton

hatton commented Jun 30, 2026

Copy link
Copy Markdown
Member Author

@coderabbitai review

[Claude Opus 4.8] — requesting a CodeRabbit review of this draft PR.

- Ignore URL sort/filter on columns a user can't see (moderator-/login-gated),
  and preserve such config in raw state + URL so it re-appears if their access
  widens (e.g. auth resolves). Adds getColumnsVisibleToUser + availableColumnNames.
- Mirror seeded initialFilters into a bare URL on mount so the shown view is shareable.
- Treat a bare/empty filter param (?ti=) as no filter, so it can't clobber initialFilters.
- Extract isLocalhost() (DataSource), applyUrlKeys(), and a single buildStateFromConfig()
  precedence pipeline shared by mount + popstate; drop duplicated per-grid role predicate.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Comment thread src/components/Grid/GridColumns.tsx
@hatton

hatton commented Jul 1, 2026

Copy link
Copy Markdown
Member Author

[Claude Opus 4.8]

Consulted Devin on 2026-07-01 12:01 MDT up to commit 8364907cbdeac8c8512269a44a0c1afb97af27f9.

Result: 1 Bug, 0 Investigate flags, 8 Informational (skipped). The one bug ("Dropdown and checkbox filter cells show stale values after browser back/forward navigation", GridColumns.tsx:542/:597) is posted as a resolvable file-level review thread above.

hatton and others added 2 commits July 1, 2026 16:20
- Collapse redundant `columns` useState+useEffect into a direct
  `const columns = visibleColumnDefinitions` in all four grid controls
  (removes an extra render and a one-frame empty grid).
- isLocalhost(): also accept the bracketed IPv6 loopback "[::1]" that
  window.location.hostname actually returns.
- Refresh stale "hidden" references in reserved-param comments; the
  hidden param was folded into `cols`, so reserved keys are sort/cols/widths.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Devin flagged that the dropdown (ChoicesFilterCell) and checkbox
(TagExistsFilterCell) custom filter cells seeded a private useState from
props.filter?.value only on mount. Now that filters are restored from the
URL on back/forward, that private copy went stale: the control showed the
old selection while the grid data reflected the URL-restored filter.

Make both cells fully controlled by props.filter so the shown state always
matches the grid's active filter (also removes the now-redundant local state).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@hatton

hatton commented Jul 2, 2026

Copy link
Copy Markdown
Member Author

[Claude Opus 4.8]

Consulted Devin on 2026-07-02 00:08 UTC up to commit ffc0be5d06aec002b93b790d0202b049aadbbc1e.

Result: 0 Bugs, 0 Investigate flags, 10 Informational (skipped). The prior bug ("Dropdown and checkbox filter cells show stale values after browser back/forward") is now marked Resolved by Devin and its thread has been resolved. Re-review clean — bots quiet.

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