Skip to content

Fix: nested rows selectable#3667

Open
laurafg wants to merge 4 commits intomainfrom
fix/nested-rows-selectable
Open

Fix: nested rows selectable#3667
laurafg wants to merge 4 commits intomainfrom
fix/nested-rows-selectable

Conversation

@laurafg
Copy link
Copy Markdown
Contributor

@laurafg laurafg commented Mar 13, 2026

🚪 Why?

Selecting children rows in a DataCollection with nested records was broken in multiple ways: clicking a child checkbox selected the parent instead, children were excluded from bulk actions, "select all" only selected top-level parents (ignoring nested children), and Storybook stories had duplicate IDs causing all items to be checked at once.

🔑 What?

Fix children row selection:

  • Replace onCheckedChange with onSelectItem prop in Row/NestedRow so each row calls handleSelectItemChange with its own item instead of inheriting the parent's closure
  • Update useSelectable to accept and store the item reference directly in handleSelectItemChangeInternal, preventing failed lookups for children not present in data.records
  • Relax the currentPageItemIds filter in itemsStatus/selectedIds to include items with a resolved reference (children are visible on the current page but not in top-level records)

Cascade "select all" to nested children:

  • Add a useEffect in NestedRow that detects when a parent transitions from unselected → selected (e.g., via "select all") and cascades selection to all loaded children
  • Track previous parent selection state with a ref to avoid infinite loops and double-triggers with the existing handleSelectItemWithChildren click handler

Parent indeterminate/checked state from children:

  • Compute parentIndeterminate and allChildrenSelected in NestedRow based on loaded children's selection state
  • When all children are individually selected, auto-select the parent via a useEffect
  • Pass indeterminate/allChildrenSelected props to Row for correct checkbox rendering

Hide checkbox on "load more" rows:

  • Override selectable to () => undefined on LoadMore rows so the column remains rendered (preserving table alignment) but no checkbox is displayed

Fix Storybook mock data:

  • Generate unique IDs for child items in mockData.tsx (${user.id}-child-0, etc.)
  • Fix selectable prop in nested records stories to use item.id instead of a hardcoded empty string

Edge cases handled

Scenario Behavior
Select all (page or all-pages) Parents are selected by useSelectable, then the useEffect in NestedRow cascades to all loaded children
Deselect all handleSelectAll(false) resets the entire items Map to empty, clearing both parents and children in one operation
Click parent checkbox directly handleSelectItemWithChildren cascades immediately on click; the useEffect does not double-trigger because the ref already tracks the parent as selected
Expand a row after select-all When children load, the children dependency changes, the useEffect re-runs, sees the parent is selected, and cascades to newly loaded children
Manually deselect a child Parent becomes indeterminate. The cascade useEffect does not re-select the child because the ref already has wasSelected = true (no transition detected)
Deselect all children individually Parent auto-deselects via the existing allChildrenSelected effect (which detects 0 selected children and does nothing), and the indeterminate logic resets

Known limitation: selection scope is tied to expanded rows

"Select all" can only cascade to children that have been loaded — i.e., the parent row was previously expanded. Children of collapsed/never-expanded rows are not selected because they don't exist in memory yet (they are lazily loaded via NestedDataProvider).

This means both selection and count only reflect what has been expanded:

  • If you click "select all" with 5 parents and 2 of them have been expanded (showing 3 children each), the selection will include 5 parents + 6 children = 11 items. The 3 unexpanded parents' children are not selected.
  • If you then expand one of those 3 remaining parents, the cascade useEffect fires (parent is already selected → new children loaded → auto-selected), and the count increases.

Counter accuracy per mode:

  • Page-only mode: the counter uses checkedCount (actual checked items in state). It accurately reflects what is selected — parents + expanded children. As you expand more rows, children get cascaded and the count goes up incrementally.
  • All-pages mode: the counter uses selectAllTotal - uncheckedCount, where selectAllTotal comes from paginationInfo.total (top-level items only). This formula does not account for children at all, so even after children are loaded and selected, the displayed count can be lower than the actual number of selected items. This is a pre-existing architectural limitation of how useSelectable tracks counts separately from NestedDataProvider.

Copilot AI review requested due to automatic review settings March 13, 2026 11:06
@github-actions github-actions bot added react Changes affect packages/react react-native Changes affect packages/react-native labels Mar 13, 2026
@laurafg laurafg force-pushed the feature/editable-table-selector branch from c5b9588 to 4b47bd1 Compare March 13, 2026 11:07
@github-actions
Copy link
Copy Markdown
Contributor

github-actions bot commented Mar 13, 2026

📦 Alpha Package Version Published

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

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

@github-actions
Copy link
Copy Markdown
Contributor

github-actions bot commented Mar 13, 2026

🔍 Visual review for your branch is published 🔍

Here are the links to:

Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

This PR aims to fix selection behavior for nested rows in the experimental OneDataCollection Table by ensuring nested child records can be selected even when they aren’t part of the top-level data.records for the current page.

Changes:

  • Extend useSelectable to persist an item reference when selecting by record object (to support nested children not found in data.records).
  • Update Table row components to pass the full record into the selection handler (onSelectItem(item, checked)), enabling the hook to store references for nested rows.
  • Adjust Storybook mock data to ensure nested records have unique IDs and update a story selectable function accordingly.

Reviewed changes

Copilot reviewed 8 out of 8 changed files in this pull request and generated 4 comments.

Show a summary per file
File Description
packages/react/src/hooks/datasource/useSelectable/useSelectable.ts Attempts to include nested-row items in page-only selection by using resolved item refs; adds optional itemRef to internal selection updates.
packages/react/src/experimental/OneDataCollection/visualizations/collection/Table/components/RowLoading/index.tsx Renames prop wiring from onCheckedChange to onSelectItem for consistency with new API.
packages/react/src/experimental/OneDataCollection/visualizations/collection/Table/components/Row.tsx Changes selection callback to pass (item, checked) so nested rows can provide a record reference.
packages/react/src/experimental/OneDataCollection/visualizations/collection/Table/components/NestedRow.tsx Updates nested row prop types to the new onSelectItem(item, checked) signature.
packages/react/src/experimental/OneDataCollection/visualizations/collection/Table/components/LoadMore/index.tsx Overrides source.selectable for the Load More row (intended to suppress checkbox rendering).
packages/react/src/experimental/OneDataCollection/visualizations/collection/Table/Table.tsx Passes the new onSelectItem handler through to Row/MotionRow.
packages/react/src/experimental/OneDataCollection/stories/visualizations/editable-table/editable-table.stories.tsx Updates selectable in a story to return item.id (valid selection id).
packages/react/src/experimental/OneDataCollection/stories/mockData.tsx Ensures nested mock records have unique IDs to avoid selection collisions.

Comment on lines +236 to +242
// In page-only mode, include items from the current page OR items
// with a resolved item reference (e.g. children loaded via nested rows
// that aren't in data.records but are visible on the current page).
if (isPageOnlySelection && currentPageItemIds) {
return currentPageItemIds.has(itemState.id)
return (
currentPageItemIds.has(itemState.id) || itemState.item !== undefined
)
Comment on lines 251 to 253
if (isPageOnlySelection && currentPageItemIds) {
return currentPageItemIds.has(id)
return currentPageItemIds.has(id) || itemState.item !== undefined
}
return (
<Row
{...props}
source={{ ...props.source, selectable: () => undefined }}
Comment on lines +236 to +242
// In page-only mode, include items from the current page OR items
// with a resolved item reference (e.g. children loaded via nested rows
// that aren't in data.records but are visible on the current page).
if (isPageOnlySelection && currentPageItemIds) {
return currentPageItemIds.has(itemState.id)
return (
currentPageItemIds.has(itemState.id) || itemState.item !== undefined
)
@github-actions
Copy link
Copy Markdown
Contributor

github-actions bot commented Mar 13, 2026

@github-actions
Copy link
Copy Markdown
Contributor

📱 Expo Go Preview Published

  • Branch: development
  • Message: `fix: children rows not selected for bulk actions in EditableTable

The onCheckedChange closure in Table.tsx captured the parent item and
was inherited by child rows via prop spreading in NestedRow, so clicking
a child checkbox selected the parent instead. Additionally, useSelectable
discarded the item reference for children (not in data.records) and
filtered them out of itemsStatus/selectedIds via currentPageItemIds.

  • Replace onCheckedChange with onSelectItem prop so each Row calls
    handleSelectItemChange with its own item
  • Pass item reference through handleSelectItemChangeInternal so children
    are stored with their actual item object
  • Allow items with resolved references through the page-only selection
    filter (children are on the current page but not in data.records)
  • Hide checkbox on LoadMore rows

Made-with: Cursor`

  • Group ID: 7633516e-fa26-45fa-a700-0fed3337feba
  • Created at: 2026-03-13T11:13:47.440Z

Links

QR Code

Expo Go Preview QR

@laurafg laurafg requested a review from siguenzaraul March 13, 2026 11:26
@laurafg laurafg marked this pull request as ready for review March 13, 2026 11:26
@laurafg laurafg requested a review from a team as a code owner March 13, 2026 11:26
@github-actions github-actions bot removed the react-native Changes affect packages/react-native label Mar 13, 2026
@laurafg laurafg force-pushed the feature/editable-table-selector branch from 4b47bd1 to d1b113c Compare March 16, 2026 10:20
Base automatically changed from feature/editable-table-selector to main March 16, 2026 11:04
laurafg added 4 commits March 16, 2026 12:06
The onCheckedChange closure in Table.tsx captured the parent item and
was inherited by child rows via prop spreading in NestedRow, so clicking
a child checkbox selected the parent instead. Additionally, useSelectable
discarded the item reference for children (not in data.records) and
filtered them out of itemsStatus/selectedIds via currentPageItemIds.

- Replace onCheckedChange with onSelectItem prop so each Row calls
  handleSelectItemChange with its own item
- Pass item reference through handleSelectItemChangeInternal so children
  are stored with their actual item object
- Allow items with resolved references through the page-only selection
  filter (children are on the current page but not in data.records)
- Hide checkbox on LoadMore rows

Made-with: Cursor
When "select all" is clicked, only top-level parents were selected
because useSelectable iterates data.records which doesn't include
lazily-loaded children. Added a useEffect in NestedRow that detects
parent selection transitions and cascades to loaded children.

Made-with: Cursor
@laurafg laurafg force-pushed the fix/nested-rows-selectable branch from 9339a45 to 70b5306 Compare March 16, 2026 11:21
Copilot AI review requested due to automatic review settings March 16, 2026 11:21
@github-actions
Copy link
Copy Markdown
Contributor

✅ No New Circular Dependencies

No new circular dependencies detected. Current count: 0

@laurafg laurafg changed the title Fix/nested rows selectable Fix: nested rows selectable Mar 16, 2026
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

Fixes broken selection behavior for nested rows in the experimental OneDataCollection Table, including child-row selection, select-all cascading, parent indeterminate state, and Storybook data issues.

Changes:

  • Updates Table row components to use an onSelectItem(item, checked) API so each row selects itself (and can cascade to children).
  • Extends nested-row selection logic to compute indeterminate/checked state from children and cascade select-all to loaded children.
  • Adjusts useSelectable to better support nested items and fixes Storybook mock data to avoid duplicate IDs.

Reviewed changes

Copilot reviewed 9 out of 9 changed files in this pull request and generated 3 comments.

Show a summary per file
File Description
packages/react/src/hooks/datasource/useSelectable/useSelectable.ts Adds item reference support during selection and adjusts page-only filtering/select-all behavior.
packages/react/src/experimental/OneDataCollection/visualizations/collection/Table/components/RowLoading/index.tsx Renames row selection prop to onSelectItem.
packages/react/src/experimental/OneDataCollection/visualizations/collection/Table/components/Row.tsx Switches selection callback signature and supports indeterminate/children-selected checkbox rendering.
packages/react/src/experimental/OneDataCollection/visualizations/collection/Table/components/NestedRow.tsx Implements child→parent indeterminate/checked logic and select-all cascading to loaded children.
packages/react/src/experimental/OneDataCollection/visualizations/collection/Table/components/LoadMore/index.tsx Disables checkbox rendering on “Load more” rows while preserving column alignment.
packages/react/src/experimental/OneDataCollection/visualizations/collection/Table/Table.tsx Passes handleSelectItemChange directly via onSelectItem.
packages/react/src/experimental/OneDataCollection/stories/visualizations/table/table.stories.tsx Fixes selectable to return item.id for nested records story.
packages/react/src/experimental/OneDataCollection/stories/visualizations/editable-table/editable-table.stories.tsx Fixes selectable to return item.id for nested records story.
packages/react/src/experimental/OneDataCollection/stories/mockData.tsx Ensures nested mock records have unique IDs to avoid selection collisions.
Comments suppressed due to low confidence (1)

packages/react/src/hooks/datasource/useSelectable/useSelectable.ts:581

  • handleSelectAll no longer reads allSelectedCheck, but it’s still listed in the useCallback dependency array. This creates unnecessary callback churn and makes the dependencies misleading—remove allSelectedCheck (or reintroduce a use if it’s still intended).
    [
      isMultiSelection,
      allSelectedCheck,
      isGrouped,
      data,

Comment on lines 235 to +242
if (itemState.item === undefined) return false
// Filter to only current page items in page-only selection mode
// In page-only mode, include items from the current page OR items
// with a resolved item reference (e.g. children loaded via nested rows
// that aren't in data.records but are visible on the current page).
if (isPageOnlySelection && currentPageItemIds) {
return currentPageItemIds.has(itemState.id)
return (
currentPageItemIds.has(itemState.id) || itemState.item !== undefined
)
Comment on lines 248 to 253
const selectedIds = Array.from(items.entries())
.filter(([id, itemState]) => {
if (!itemState.checked) return false
// Filter to only current page items in page-only selection mode
if (isPageOnlySelection && currentPageItemIds) {
return currentPageItemIds.has(id)
return currentPageItemIds.has(id) || itemState.item !== undefined
}
Comment on lines 422 to +426
const handleSelectItemChangeInternal = useCallback(
(
itemId: SelectionId | readonly SelectionId[],
checked: boolean,
onlyIfNotPreviousState: boolean = false
onlyIfNotPreviousState: boolean = false,
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

react Changes affect packages/react

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants