Skip to content

refactor(tables): decouple UI display from DB position#4448

Merged
waleedlatif1 merged 4 commits intostagingfrom
waleedlatif1/tables-decouple-phase-1
May 5, 2026
Merged

refactor(tables): decouple UI display from DB position#4448
waleedlatif1 merged 4 commits intostagingfrom
waleedlatif1/tables-decouple-phase-1

Conversation

@waleedlatif1
Copy link
Copy Markdown
Collaborator

Summary

  • Gutter numbering, selection coordinates, paste targets, keyboard nav, and DOM lookups all derive from the rendered array index instead of row.position. Position becomes an internal sort key the UI never surfaces.
  • checkedRows is rowId-keyed so check state survives sort/filter changes and realtime row inserts.
  • Selection is rowId-stable: same-length sort changes remap anchor/focus to the new visual index instead of leaving them on a different row; out-of-bounds clears.
  • Insert/duplicate/paste/append still call the API with position, but the next-position math uses Math.max over loaded rows (last visual row's position is not the largest under non-position sorts).
  • Deletes the gap-fill loop, PositionGapRows, positionMap, and min/maxPositionRef.
  • Supersedes fix(tables): row gutter click toggles select; select-all works under sort/filter #4446 — that PR's three band-aids (gutter click, Math.max select-all, membership check) are subsumed by the refactor.

Type of Change

  • Refactor

Testing

Tested manually across the verification matrix: gutter numbering under sort/filter/sort+filter, single + range selection, shift-click checkbox range, Cmd+A, keyboard nav, copy/paste 3×3, insert above/below, duplicate, Shift+Enter append, undo/redo, expanded-cell popover under sort, pagination + sort. `bun run lint` and `bunx tsc --noEmit` clean.

Known limitation (documented, out of scope): inserting under an active data sort places the new row wherever the sort dictates, not visually adjacent to the anchor. Matches Airtable / Notion behavior.

Checklist

  • Code follows project style guidelines
  • Self-reviewed my changes
  • Tests added/updated and passing
  • No new warnings introduced
  • I confirm that I have read and agree to the terms outlined in the Contributor License Agreement (CLA)

waleedlatif1 and others added 3 commits May 4, 2026 22:46
Delete the PositionGapRows component, the gap-fill loop, and the
GAP_CHECKBOX_CLASS / GAP_ROW_LIMIT / PositionGapRowsProps surface area.
The server's recompactPositions() guarantees positions are 0..N-1
contiguous in the unfiltered view, so the phantom-row machinery has
been defending against a state that essentially never happens.

DataRow now receives an arrayIndex prop and renders {arrayIndex + 1}
in the gutter. Selection coordinates still flow through row.position;
that switches in phase 2.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Decouple Tables UI selection coordinates from DB position:
- rowIndex semantics shift from row.position to array index across
  selection state, mouse handlers, keyboard nav, paste, scrollIntoView
- checkedRows: Set<number> (position) → Set<string> (rowId), survives
  sort/filter and realtime row inserts
- lastCheckboxRowRef stores rowId; shift-click range resolves to current
  array indices for visual-order ranges
- Drop positionMap/maxPosition derived state in favor of direct rowsRef
  reads
- ExpandedCellPopover anchors via data-row-id (row-id-stable) instead
  of data-row (array index)
- collectRowSnapshots accepts Iterable<TableRowType> directly
- Add bounds-validation effect to clamp anchor/focus when rows.length
  shrinks (sort change, pagination, realtime delete)
- Drop redundant arrayIndex prop on DataRow (rowIndex now equals it)

Server-side position math stays at API boundary only: insertRow,
duplicateRow, shift-Enter append, paste create-batch, undo snapshots.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
…ia reduce

- Track anchor/focus by rowId so same-length sort changes remap selection
  to the new visual index instead of leaving it on a different row.
- Replace last-row position lookups with Math.max reduce in paste's
  create-batch and append-row's undo snapshot — under non-position sorts,
  the last visual row's position is not the largest.
- Trim a navigation-noise comment and tighten two over-explanatory ones.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
@cursor
Copy link
Copy Markdown

cursor Bot commented May 5, 2026

PR Summary

Medium Risk
Refactors core table interaction logic (selection, checkbox ranges, copy/paste, insert/duplicate/delete) to stop relying on DB row.position, which could regress keyboard/mouse behaviors under sort/filter/pagination if any index/id mapping is off.

Overview
Decouples the table UI from DB row.position by driving gutter numbering, selection coordinates, keyboard navigation, paste targets, and most DOM lookups off the rendered row array index instead.

Row checkbox state is now keyed by row.id (and shift-range selection is computed via current visual indices) so checked rows survive sort/filter/realtime inserts. Selection anchor/focus is additionally remapped by row id when the rows array changes to keep the same rows selected across reorderings.

Removes position-based helpers (positionMap, max-position refs) and the PositionGapRows gap-filler rendering, and updates collectRowSnapshots to snapshot from row objects rather than position keys; ExpandedCellPopover now anchors via data-row-id.

Reviewed by Cursor Bugbot for commit 633ddba. Configure here.

@vercel
Copy link
Copy Markdown

vercel Bot commented May 5, 2026

The latest updates on your projects. Learn more about Vercel for GitHub.

1 Skipped Deployment
Project Deployment Actions Updated (UTC)
docs Skipped Skipped May 5, 2026 5:47pm

Request Review

@greptile-apps
Copy link
Copy Markdown
Contributor

greptile-apps Bot commented May 5, 2026

Greptile Summary

This PR refactors the table component to decouple all UI display logic from row.position, using rendered array indices for gutter numbering, selection coordinates, and DOM lookups, while position becomes a pure DB sort key. Checkbox state and selection anchors are now keyed by row ID and remapped on sort/filter changes via a new useEffect that tracks anchorRowIdRef/focusRowIdRef.

  • checkedRows and lastCheckboxRowRef migrated from Set<number> (positions) to Set<string> (row IDs); shift-click range and clear-on-select-all both updated consistently, including a new lastCheckboxRowRef.current = null reset in handleSelectAll.
  • Selection remap effect ([rows] dep) reads anchorRowIdRef/focusRowIdRef — written only when selectionAnchor/selectionFocus change — so it correctly identifies the pre-sort row IDs and finds their new indices without conflating separate update cycles.
  • PositionGapRows, positionMap, maxPosition/maxPositionRef all removed; paste-create positions use a Math.max reduce over currentRows captured once outside the loop (with an inline comment preserving the invariant).

Confidence Score: 5/5

Safe to merge — the refactor is internally consistent, all callsites updated, and the remap-effect design correctly handles concurrent sort/selection changes.

The core remap mechanism is sound: anchorRowIdRef/focusRowIdRef are only written when selection changes, so the remap effect always reads the pre-sort IDs and never confuses two back-to-back updates. Every callsite that previously read positionMap or maxPositionRef has been updated to use rowsRef.current, and the paste-create position formula correctly captures lastRowPosition once outside the loop.

No files require special attention.

Important Files Changed

Filename Overview
apps/sim/app/workspace/[workspaceId]/tables/[tableId]/components/table/table.tsx Major refactor replacing position-keyed state with row-ID-keyed state; removes positionMap/PositionGapRows, adds ID-stable remap effect, and correctly updates all selection/copy/paste/delete handlers to use array indices.
apps/sim/app/workspace/[workspaceId]/tables/[tableId]/components/table/cells/expanded-cell-popover.tsx Single-line change: switches DOM lookup from data-row (position) to data-row-id (row ID) so the popover anchor survives sort/filter changes during its lifetime.
apps/sim/app/workspace/[workspaceId]/tables/[tableId]/components/table/utils.ts collectRowSnapshots simplified to accept Iterable directly, eliminating the position-map indirection; no logic changes to other utilities.

Flowchart

%%{init: {'theme': 'neutral'}}%%
flowchart TD
    A[rows changes\ne.g. sort/filter] --> B[remap useEffect fires]
    B --> C{isColumnSelection?}
    C -- yes --> D[skip — column effect\nhandles focus pin]
    C -- no --> E{anchor rowIndex\nmatches anchorRowIdRef?}
    E -- no, ID mismatch --> F[findIndex by ID\nin new rows]
    F --> G{found?}
    G -- yes --> H[setSelectionAnchor\nnew index]
    G -- no --> I[setSelectionAnchor null]
    E -- out of bounds --> I
    E -- matches --> J[no-op]
    H --> K[anchorRowIdRef effect\nupdates ref for next cycle]
    I --> K

    L[user clicks cell / checkbox] --> M[setSelectionAnchor / setCheckedRows\nwith array index / row ID]
    M --> N[anchorRowIdRef effect\nstores rows index to ID mapping]
    N --> O[remap effect ready\nfor next rows change]
Loading

Reviews (2): Last reviewed commit: "fix(tables): guard rowId remap effect an..." | Re-trigger Greptile

- Skip the validation effect when rows is empty (transient state during
  initial load of a new sort/filter before keepPreviousData populates) so
  selection survives uncached query changes.
- Skip when isColumnSelection is true; the column-selection pinning effect
  owns focus.rowIndex for those, and remapping would shrink a full-column
  range to wherever the captured endpoints happened to land after reorder.
- Comment lastRowPosition's hoist invariant so a future refactor doesn't
  move it inside the loop and produce colliding positions.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
@waleedlatif1
Copy link
Copy Markdown
Collaborator Author

@greptile

@waleedlatif1
Copy link
Copy Markdown
Collaborator Author

@cursor review

Copy link
Copy Markdown

@cursor cursor Bot left a comment

Choose a reason for hiding this comment

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

✅ Bugbot reviewed your changes and found no new issues!

Comment @cursor review or bugbot run to trigger another review on this PR

Reviewed by Cursor Bugbot for commit 633ddba. Configure here.

@waleedlatif1 waleedlatif1 merged commit ab551cc into staging May 5, 2026
14 checks passed
@waleedlatif1 waleedlatif1 deleted the waleedlatif1/tables-decouple-phase-1 branch May 5, 2026 18:09
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