Persist grid configuration in the URL (BL-16486)#613
Conversation
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>
|
@devin review [Claude Opus 4.8] — triggering a Devin review of this draft PR. |
|
| 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
- 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>
|
[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 ( |
|
@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>
|
[Claude Opus 4.8] Consulted Devin on 2026-07-01 12:01 MDT up to commit 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", |
- 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>
|
[Claude Opus 4.8] Consulted Devin on 2026-07-02 00:08 UTC up to commit 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. |
[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-columnurlKey, e.g.?in=true&lv=4.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 nativehistory.replaceState(no router re-render, so the filter input keeps focus while typing) and re-read onpopstate.localStoragestays the personal default; the URL wins when present; a bare URL is backfilled from the saved layout so it's shareable.*GridControlInternal.tsx(DevExpressFilteringState/SortingState/TableColumnVisibility/TableColumnResizingmade controlled); a shorturlKeyadded to every column.localhostfor easier dev.Tests
gridUrlConfig.test.ts(pure serialization + edge cases) anduseGridConfigInUrl.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
"auto".🤖 Generated with Claude Code
This change is