Skip to content

fix(spa): skeleton only the table on a list filter change (#508)#509

Merged
MartinCastroAlvarez merged 1 commit into
mainfrom
fix/list-filter-skeleton-508-005434
May 27, 2026
Merged

fix(spa): skeleton only the table on a list filter change (#508)#509
MartinCastroAlvarez merged 1 commit into
mainfrom
fix/list-filter-skeleton-508-005434

Conversation

@MartinCastroAlvarez
Copy link
Copy Markdown
Owner

Summary

Closes #508. Changing a list_filter (or page / search / ordering) blanked the whole changelist to a full-page skeleton until the new rows arrived — the title, search box, filter chips and Customize button all vanished. Now the chrome stays put and only the table shows a loading skeleton, matching Django's changelist.

Root cause

useListuseSwrCache, whose cache key encodes the filters. A filter change → new key → the hook reset data to the new key's cache (null for an unseen combo) → ListPage's loading && !data full-page skeleton fired. That reset is correct for a detail view (object→object must not flash the previous record, #416) but wrong for a list.

Fix

  • Add a keepPreviousData option to useSwrCache: on a key change, keep the previous value on screen and show a foreground load instead of blanking — still adopting the new key's cached value immediately when present.
  • useList opts in; useDetail keeps the reset (so Detail→detail navigation shows stale content instead of a skeleton during load #416 stays fixed).
  • Remount ListPage per model (<KeyedListPage>) so retained-data is scoped to same-model filter/page/search changes; a model switch resets cleanly (no stale columns or carried-over selection).
  • The Table primitive already renders skeleton rows under the real headers when loading.

No backend / contract / dep changes.

Role

Author. Eligible for same-session review/merge once CI is green per repo multi-lane authorization.

Test plan

  • swr-cache.test.ts: default blanks on key change (Detail→detail navigation shows stale content instead of a skeleton during load #416); keepPreviousData retains previous data + foreground load; adopts the new key's cache immediately when present. (8 passed)
  • Full @dar/data vitest suite green (31 passed).
  • @dar/web + @dar/data typecheck clean; eslint --max-warnings 0 clean.
  • Manual: first load → full skeleton; change a filter → header stays, table skeletons; switch model → clean reset.

🤖 Generated with Claude Code

Changing a list_filter (or page / search / ordering) replaced the whole
changelist with a full-page skeleton until the new rows arrived — the
title, search box, filter chips and Customize button all vanished,
making each filter interaction feel like a hard reload. Django's
changelist only re-renders the results table.

useList is backed by useSwrCache, whose cache key encodes the filters.
A filter change → new key → the hook reset data to the new key's cache
(null for an unseen combo), and ListPage shows its full-page skeleton
whenever `loading && !data`, so the page blanked.

That key-reset is right for a detail view (object→object must not flash
the previous record, #416) but wrong for a list, where the chrome and
columns are filter-independent. Add a `keepPreviousData` option to
useSwrCache: on a key change it keeps the previous value on screen and
shows a foreground load instead of blanking (adopting the new key's
cache immediately when it exists). useList opts in; useDetail keeps the
reset. ListPage is remounted per model so the retained-data behaviour
is scoped to same-model filter/page changes and a model switch still
resets cleanly.

Result: the header stays put and only the Table shows skeleton rows
(its built-in loading state) on a filter change.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Copy link
Copy Markdown
Owner Author

@MartinCastroAlvarez MartinCastroAlvarez left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Review — Reviewer lane (independent; did not author). Clear to merge.

Design: correct on both axes. keepPreviousData: true keeps the prior rows + page chrome while a within-model filter/page/search change loads (table-only skeleton), and KeyedListPage keys ListPage on app/model so a model switch remounts — stale rows from the previous model can never flash, and per-model selection state resets cleanly.

Regression safety: the #416 detail behavior is preserved — detail doesn't opt into keepPreviousData, so a new uncached key still blanks (no flash of the wrong record). The third test (adopts the new key cached value immediately) confirms a cached key shows at once rather than holding stale data.

Security: none. keepPreviousData only retains the same user's, same-model, permission-filtered rows across a filter/page change; model changes remount; Cache-Control: no-store is untouched.

Tests: 3 targeted cases in swr-cache.test.ts covering default-blank (#416), keep-previous (#368), and immediate-cache-adoption. Good coverage of the data-layer behavior that drives the skeleton.

Verification basis: code + test inspection; CodeQL green. UX not browser-verified and the frontend suite isn't server-enforced yet (pending #506), but the change is unit-covered and low-risk. Merging per the ship-ASAP authorization.

@MartinCastroAlvarez MartinCastroAlvarez merged commit eeef954 into main May 27, 2026
3 checks passed
@MartinCastroAlvarez MartinCastroAlvarez deleted the fix/list-filter-skeleton-508-005434 branch May 27, 2026 23:02
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.

List: changing a filter blanks the whole page to a skeleton (should skeleton only the table)

2 participants