Skip to content

feat(OneDataCollection): keep every loaded children page live and replaceable - #5245

Merged
factmarc10010 merged 1 commit into
mainfrom
fix/nested-children-pagination-per-page
Aug 26, 2026
Merged

feat(OneDataCollection): keep every loaded children page live and replaceable#5245
factmarc10010 merged 1 commit into
mainfrom
fix/nested-children-pagination-per-page

Conversation

@factmarc10010

@factmarc10010 factmarc10010 commented Aug 25, 2026

Copy link
Copy Markdown
Contributor

What

A nested row now keeps one subscription per children page, and children are stored per page rather than in a single flat list. A page that re-emits replaces its own slice; the flat list is recomposed in page order.

Also here: the "See more" label reads from collections.table.seeMoreChildren instead of being a hardcoded English string, and the story mock returns a real page instead of echoing the whole array back.

Screen.Recording.2026-08-25.at.17.24.44.mov

Why

useLoadChildren held a single subscriptionRef and accumulated every emission into one list. Both halves break as soon as a second page exists:

  • loadChildren unsubscribes the previous page before requesting the next one, so only the last page can still receive updates from the consumer.
  • processChildrenData did [...children, ...loadedChildren] with children captured in its closure. The subscription's next handler holds one instance of that callback, so every later emission rebuilds from the list as it was when that page subscribed.

Together that means, for any consumer whose children can change after they load:

Upstream change What the user saw
A row on an earlier page is renamed or enriched Never updates — frozen at the snapshot taken when the next page loaded
A row is inserted, shifting the page boundary The row straddling the boundary renders twice, and the row it displaced disappears
Rows are removed They stay on screen until a filter change resets the whole row

None of it was reachable from the consumer side: the frozen baseline lives in this hook.

The reason it went unnoticed is the last commit in the diff. The only place children pagination was exercised — the OneDataCollection story mock — returned item.children in full on every call with hasMore pinned to true, so "See more" appended the same records forever. That shape can't surface any of the three rows above.

How

  • pagesRef: Map<page, R[]> — the records of each requested page. processChildrenData takes the page it is processing, sets that entry, and recomposes the flat list by page order. It no longer depends on children, so its identity is stable and there is no captured baseline.
  • subscriptionsRef: Map<page, Subscription>loadChildren replaces only the subscription for the page it is about to request. Earlier pages keep listening. Unmount and the filters/sortings reset unsubscribe all of them.
  • frontierRefhasMore and currentPage describe the frontier, so only the highest page loaded writes them. Without this, an earlier page re-emitting would rewind the cursor and re-offer "See more".
  • Page 0 holds the children restored from the cache on a remount: their subscriptions died with the previous mount, so nothing can re-emit them, and they sort before page 1.

Compatibility. A consumer that returns no paginationInfo has a single page, which is replaced on every emission — the same effective behaviour as before. A consumer that returns a constant currentPage (the old mock's shape) still accumulates, because the requested page comes from the pagination state handed back to it, not from the response.

Testing

useLoadChildren.test.tsx is new — the hook had no unit test. 8 cases, each driving a long-lived observable per page so a specific page can be re-emitted, which is exactly what the flat accumulator could not express.

Against main, 4 of the 8 fail: every loaded page staying subscribed, an update to an earlier page landing, a boundary shift not duplicating, and the filters reset unsubscribing every page. The other 4 pass on both and are there as guards on behaviour this must not change (page-by-page append, the frontier owning the cursor, a duplicate same-page request superseding itself, removals dropping out).

Full unit suite green (pnpm vitest:ci), except 6 pre-existing failures in F0DataChart/__tests__/BarChart.test.tsx that also fail on a clean origin/main. tsc --noEmit and oxlint clean.

Note for consumers — the flagged API change

The breaking-API bot flags the new collections.table.seeMoreChildren key and asks for a major. It ships as a minor — hence feat, even though the substance of the change is a fix:

  • Runtime does not break. withDefaults in i18n-provider already fills a consumer's gaps from the English defaults, recursively — it exists precisely so a dictionary written against an older version degrades to English instead of rendering undefined.
  • Types do break, on purpose. TranslationShape makes every key required, so a consumer that maintains a full dictionary gets a tsc error naming the key. That error is the mechanism by which consumers learn there is something new to translate; making the shape partial would trade it for silently shipping English.
  • Precedent. The three most recent key additions shipped as plain feat(...)periods.* (feat(DateNavigator): select consumer-defined periods #5161), lockColumn/unlockColumn (feat(OneDataCollection): freeze table columns from settings #5188, the adjacent block in collections.table), dropWidgetToDiscuss (feat(ai): drop a dashboard widget into the chat to quote it #5126). A major per label would contradict all three.

A patch would have been the wrong signal in the other direction: it would break a consumer's typecheck without the version anticipating it. So feat it is — one line to add per consumer on bump, which is how every other key has landed.

Note on the a11y report

The 6 axe findings are pre-existing and unrelated: they are on empty-states.stories.tsx, which this PR does not touch, and the same nested-interactive / target-size pairs appear on #5231 and #5225. They are in scope only because this PR edits __stories__/mockData.tsx, in the same folder. The flagged stories render an empty or failed collection, so no children row — and therefore no code path from this diff — is involved.

@github-actions github-actions Bot added fix react Changes affect packages/react labels Aug 25, 2026
@github-actions

github-actions Bot commented Aug 25, 2026

Copy link
Copy Markdown
Contributor

🔍 Review policy: Feature

The PR title starts with feat, so this is a feature: it needs one approval from f0-devs AND one from f0-designers (rule 3).

Required approvals

Team Why Status
@factorialco/f0-devs Features need a dev approval ✅ approved by @sauldom102
@factorialco/f0-designers Features need a design approval ⏳ pending
How this was decided
  • PRs touching only sds/ modules require their owners and nothing else.
  • Otherwise, docs-only changes (*.md, *.mdx, *.stories.tsx, anything in __stories__/) → one f0-general approval.
  • Otherwise, feat: titles → one f0-devs and one f0-designers approval. Not a feature? Fix the title prefix.
  • Anything else → one f0-devs approval.
  • Add the needs-design-review label to also request a design approval on any PR.
  • Creating a new sds/ module (new package.yml) additionally requires an f0-general approval.

Policy source: ownership/review-policy.ts · Team members: ownership/teams.yml

@github-actions

Copy link
Copy Markdown
Contributor

✅ No New Circular Dependencies

No new circular dependencies detected. Current count: 0

@github-actions

github-actions Bot commented Aug 25, 2026

Copy link
Copy Markdown
Contributor

🔍 Visual review for your branch is published 🔍

Here are the links to:

@github-actions

github-actions Bot commented Aug 25, 2026

Copy link
Copy Markdown
Contributor

📦 Alpha Package Version Published

Use pnpm i github:factorialco/f0#npm/alpha-pr-5245 to install the package

Use pnpm i github:factorialco/f0#c61f0e4640d392b5e407045ca1250608da3ffd14 to install this specific commit

@github-actions

Copy link
Copy Markdown
Contributor

⚠️ Breaking public API changes (1)

These public exports were renamed/removed, or had a property/parameter removed, retyped, or newly required compared to main — that breaks consumers. Adding new exports or new optional props is always safe and is not flagged. If a breaking change is intentional, note it in the PR description and use a feat!:/BREAKING CHANGE commit so the release is a major bump.

Comparing f0, experimental and ai against main. Adding components, types, or optional props is safe. This check is non-blocking.

🌐 Translations

  • ➕ Translation keys were added: collections.table.seeMoreChildren

    The translations dictionary is embedded in many public exports (buildTranslations, defaultTranslations, i18n props, …), so its changes are summarized here once instead of being flagged on every export. Consumers maintaining full translation objects must update them.

⚠️ Could not analyze component-status (no-base) — a build may have failed; results may be incomplete.

@github-actions

github-actions Bot commented Aug 25, 2026

Copy link
Copy Markdown
Contributor

♿ Accessibility (axe) — components changed in this PR

7 issues across 4 stories — all non-blocking (todo).

Story Rule WCAG Impact Nodes Mode
Patterns/Data Collection/Miscellaneous / With Progress Bar color-contrast WCAG 1.4.3 AA (2.0) serious 2 🟡 todo
Patterns/Data Collection/Empty State / Error Example nested-interactive WCAG 4.1.2 A (2.0) serious 1 🟡 todo
Patterns/Data Collection/Empty State / Error Example target-size WCAG 2.5.8 AA (2.2) serious 2 🟡 todo
Patterns/Data Collection/Empty State / Custom Messages And Actions nested-interactive WCAG 4.1.2 A (2.0) serious 1 🟡 todo
Patterns/Data Collection/Empty State / Custom Messages And Actions target-size WCAG 2.5.8 AA (2.2) serious 1 🟡 todo
Patterns/Data Collection/Empty State / Empty To Data Example nested-interactive WCAG 4.1.2 A (2.0) serious 1 🟡 todo
Patterns/Data Collection/Empty State / Empty To Data Example target-size WCAG 2.5.8 AA (2.2) serious 1 🟡 todo

Scope: only stories in the files/component folders this PR changed. It can't yet flag downstream ripple from shared-code/token changes, or diff against main (planned: base-vs-head delta).

@github-actions

github-actions Bot commented Aug 25, 2026

Copy link
Copy Markdown
Contributor

Coverage Report for packages/react

Status Category Percentage Covered / Total
🔵 Lines 68.44% 28748 / 42003
🔵 Statements 67.42% 30411 / 45105
🔵 Functions 61.27% 6855 / 11187
🔵 Branches 61.07% 21463 / 35140
File Coverage
File Stmts Branches Functions Lines Uncovered Lines
Changed Files
packages/react/src/experimental/OneTable/TableCell/NestedCell/index.tsx 87.87% 79.54% 92.85% 87.87% 89-90, 164-165
packages/react/src/lib/providers/i18n/i18n-provider-defaults.ts 100% 100% 100% 100%
packages/react/src/patterns/OneDataCollection/__stories__/mockData.tsx 37.59% 36.89% 34.34% 38.61% 329-357, 401, 554-590, 598-641, 650-651, 659-1004, 1048-1052, 1058-1093, 1123, 1128-1131, 1149-1199, 1204-1234, 1238-1244, 1248-1255, 1262-1270, 1456, 1459, 1478, 1502, 1508-1529, 1553-1577, 1612, 1629-1707, 1791-1796, 1801-1806, 1812-1813, 1820-1857, 1885-1909, 1922-1953, 1972, 1980-2120, 2189, 2224-2252, 2264-2276
packages/react/src/patterns/OneDataCollection/visualizations/collection/Table/hooks/useLoadChildren.ts 90.32% 76.08% 87.5% 92.04% 50, 211, 231-232, 251, 253, 260-264
Generated in workflow #17471 for commit 0fa0d9c by the Vitest Coverage Report Action

@factmarc10010

Copy link
Copy Markdown
Contributor Author

The comments regarding Accesiblity were not added in this PR

…laceable

A nested row kept one subscription for all its children pages and accumulated
emissions into a single flat list. Both halves broke once a second page existed:
loading it unsubscribed the first, and the reducer appended to whatever the list
held when that page subscribed. So the rows loaded earlier froze at that
snapshot, a reordered slice duplicated one of them, and rows removed upstream
stayed on screen until a filter change reset the row.

Keep children per requested page and one subscription per page. A page that
re-emits replaces its own slice, the flat list is recomposed in page order, and
only the highest page loaded owns `hasMore`/`currentPage` so an earlier page
cannot rewind the cursor.

The "See more" label was a hardcoded English string; it now reads from
`collections.table.seeMoreChildren`.

The story mock echoed the whole children array back with `hasMore` pinned true,
so the only place this was exercised appended duplicates by design and hid all
of the above. It now returns a real page.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@factmarc10010
factmarc10010 force-pushed the fix/nested-children-pagination-per-page branch from deac915 to 0fa0d9c Compare August 25, 2026 15:34
@factmarc10010 factmarc10010 changed the title fix(OneDataCollection): keep every loaded children page live and replaceable feat(OneDataCollection): keep every loaded children page live and replaceable Aug 25, 2026
@github-actions github-actions Bot added feat and removed fix labels Aug 25, 2026
@factmarc10010
factmarc10010 marked this pull request as ready for review August 25, 2026 15:35
@factmarc10010
factmarc10010 requested a review from a team as a code owner August 25, 2026 15:35
@factmarc10010
factmarc10010 merged commit 5d81b32 into main Aug 26, 2026
51 of 53 checks passed
@factmarc10010
factmarc10010 deleted the fix/nested-children-pagination-per-page branch August 26, 2026 08:20
@eliseo-juan eliseo-juan mentioned this pull request Aug 26, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

breaking changes feat react Changes affect packages/react

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants