Skip to content

improvement(tables): tables multi-select, keyboard shortcuts, and docs#3615

Merged
waleedlatif1 merged 11 commits intostagingfrom
improvement/tables
Mar 17, 2026
Merged

improvement(tables): tables multi-select, keyboard shortcuts, and docs#3615
waleedlatif1 merged 11 commits intostagingfrom
improvement/tables

Conversation

@waleedlatif1
Copy link
Collaborator

@waleedlatif1 waleedlatif1 commented Mar 16, 2026

Summary

  • tables-style checkbox multi-select: Clicking a checkbox toggles that row independently without deselecting others. Shift+click does range selection from the last clicked checkbox. The entire checkbox cell is a single click target (no split behavior between padding and checkbox).
  • New keyboard shortcuts: Space (expand row), Shift+Space (toggle row), Cmd/Ctrl+Arrow (jump to edge), Cmd/Ctrl+Shift+Arrow (extend to edge), Cmd/Ctrl+X (cut with undo), Cmd/Ctrl+Y (redo alternative).
  • Performance: EMPTY_CHECKED_ROWS constant avoids unnecessary re-renders, PositionGapRows has custom memo comparator, isAllRowsSelected iterates rows not positions.
  • Tables docs page: New documentation with column types, row operations, full keyboard shortcuts table, API reference link, and FAQ.

Test plan

  • Click a checkbox — row highlights, other selections preserved
  • Click another checkbox — both rows highlighted
  • Shift+click a checkbox — range from last click to current selected
  • Click a cell — checkbox selections clear, cell selection works
  • Arrow keys, Tab, Escape all clear checkbox selections
  • Cmd+A selects all rows via checkboxes
  • Space opens row expand modal
  • Shift+Space toggles row from keyboard
  • Cmd+Arrow jumps to edge of table
  • Cmd+X cuts and clears cells (with undo support)
  • Delete/Backspace clears checked rows' cells
  • Copy works with checked rows
  • Context menu delete works with checked rows
  • Header checkbox select-all/deselect-all works
  • Docs page renders correctly at /docs/en/tables

waleedlatif1 and others added 2 commits March 16, 2026 16:48
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
@cursor
Copy link

cursor bot commented Mar 16, 2026

You have used all Bugbot PR reviews included in your free trial for your GitHub account on this workspace.

To continue using Bugbot reviews, enable Bugbot for your team in the Cursor dashboard.

@vercel
Copy link

vercel bot commented Mar 16, 2026

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

Project Deployment Actions Updated (UTC)
docs Ready Ready Preview, Comment Mar 17, 2026 1:59am

Request Review

@waleedlatif1 waleedlatif1 changed the title improvement(tables): Clay-style multi-select, keyboard shortcuts, and docs improvement(tables): tables multi-select, keyboard shortcuts, and docs Mar 16, 2026
@greptile-apps
Copy link
Contributor

greptile-apps bot commented Mar 16, 2026

Greptile Summary

This PR replaces the old drag-based row selection model with a spreadsheet-style checkbox multi-select system, adds several new keyboard shortcuts (Space, Shift+Space, Cmd+Arrow, Cmd+Shift+Arrow, Cmd+X, Cmd+Y), includes performance tweaks (EMPTY_CHECKED_ROWS constant, custom PositionGapRows memo comparator), and ships a new tables documentation page. The changes are substantial but well-structured, and all previously identified issues in the review thread have been addressed.

Key observations:

  • Shift+Space can add gap-row (non-existent) positions to checkedRows: the handler doesn't verify positionMap.has(a.rowIndex) before toggling the position, causing phantom checked state on gap rows and inflating checkedRows.size, which can cause isAllRowsSelected to return a wrong value.
  • Individual mutateRef.current calls per row in cut/delete paths: both handleCut (checkbox path) and the Delete/Backspace handler fire one mutation per checked row instead of using batchUpdateRef.current, which could be slow for large selections and leaves the table in a partially-modified state if any request fails mid-loop.
  • Cmd+ArrowDown can land on a gap row: totalRows - 1 = maxPosition may be a position with no actual row data if recent rows were deleted, so "jump to edge" can drop the cursor into an empty, non-editable slot.
  • Docs are accurate and comprehensive; a small clarification about row deletion vs. cell-clearing with Delete/Backspace would help users avoid confusion.

Confidence Score: 3/5

  • Safe to merge with minor fixes — the checkbox multi-select logic is correct and all previous review issues were addressed, but the gap-row Shift+Space bug and per-row mutations in cut/delete should be cleaned up before this ships to a production audience.
  • The core selection model, range logic, and keyboard shortcuts are implemented correctly. All items from the previous review thread were addressed. Three remaining issues lower the score: (1) Shift+Space on a gap row pollutes checkedRows with a phantom position, causing isAllRowsSelected to miscalculate and showing a checked checkbox on an empty row; (2) cut and bulk-delete fire O(n) individual network mutations instead of one batch call, which degrades under load; (3) Cmd+ArrowDown may land on a deleted-position gap rather than the last real row. None of these are blockers for initial rollout, but (1) is a visible correctness bug.
  • apps/sim/app/workspace/[workspaceId]/tables/[tableId]/components/table/table.tsx — specifically the Shift+Space handler (line 722), Delete/Backspace checked-rows handler (line 740), handleCut checkbox path (line 966), and Cmd+ArrowDown jump (line 844).

Important Files Changed

Filename Overview
apps/sim/app/workspace/[workspaceId]/tables/[tableId]/components/table/table.tsx Large refactor replacing drag-based row selection with checkbox multi-select; adds checkedRows state, handleRowToggle, handleCut, Cmd+Arrow jump, Space/Shift+Space shortcuts. Individual mutateRef.current calls per checked row in cut/delete paths (instead of batch), Shift+Space can add gap positions to checkedRows, and Cmd+ArrowDown may land on a gap row.
apps/docs/content/docs/en/tables/index.mdx New tables documentation page covering column types, row operations, keyboard shortcuts, API access, and FAQ. Accurate and comprehensive; minor gap: "Deleting Rows" section doesn't distinguish between row deletion and cell-clearing via Delete/Backspace.
apps/docs/content/docs/en/meta.json Adds "tables" entry to the docs navigation meta — trivial, correct placement between "knowledgebase" and "variables".

Flowchart

%%{init: {'theme': 'neutral'}}%%
flowchart TD
    A[User Interaction] --> B{Interaction Type}

    B -->|Click Cell| C[handleCellMouseDown]
    C --> C1[Clear checkedRows]
    C1 --> C2[Set selectionAnchor / selectionFocus]

    B -->|Click Checkbox| D[handleRowToggle]
    D --> D1{Shift key?}
    D1 -->|Yes + lastCheckboxRow| D2[Range-select via positionMap]
    D1 -->|No| D3[Toggle single row in checkedRows]
    D2 --> D4[Update checkedRows + lastCheckboxRowRef]
    D3 --> D4
    D4 --> D5[Clear selectionAnchor + selectionFocus]

    B -->|Keyboard| E{Key}
    E -->|Shift+Space| F[Toggle anchor row in checkedRows]
    E -->|Cmd+A| G[Select all rows via checkedRows]
    E -->|Space| H[Open row expand modal]
    E -->|Cmd+Arrow| I[Jump to table edge]
    E -->|Arrow keys| J[Clear checkedRows, move anchor]
    E -->|Tab| K[Clear checkedRows, move to next cell]
    E -->|Escape| L[Clear both selectionAnchor and checkedRows]
    E -->|Delete / Backspace| M{checkedRows.size > 0?}
    M -->|Yes| N[Clear all columns for each checked row]
    M -->|No| O[Clear selected cell range columns]

    B -->|Clipboard Cut| P[handleCut]
    P --> P1{checkedRows.size > 0?}
    P1 -->|Yes| P2[Copy all cols of checked rows to clipboard + clear cells]
    P1 -->|No| P3[Copy selected range to clipboard + clear cells]
    P2 --> P4[pushUndo clear-cells]
    P3 --> P4

    B -->|Context Menu Delete| Q[handleContextMenuDelete]
    Q --> Q1{checkedRows has right-clicked row?}
    Q1 -->|Yes| Q2[Delete all checked rows]
    Q1 -->|No| Q3{Row in cell selection?}
    Q3 -->|Yes| Q4[Delete selection range rows]
    Q3 -->|No| Q5[Delete single row]
Loading

Comments Outside Diff (1)

  1. apps/sim/app/workspace/[workspaceId]/tables/[tableId]/components/table/table.tsx, line 829-861 (link)

    P2 Cmd+Arrow jumps to totalRows - 1 which may be a gap row

    totalRows = mp + 1 where mp = maxPositionRef.current (the highest occupied position in the table). When the user presses Cmd+ArrowDown, the anchor jumps to position totalRows - 1 = mp. If mp is a gap position (the last real row has a lower position and higher positions have been deleted), the anchor lands on a visually empty row with no data. Subsequent cell editing (Enter/F2) will then call positionMapRef.current.get(anchor.rowIndex) and get undefined, silently failing.

    Cmd+ArrowRight is fine (cols.length - 1 is always a real column), but Cmd+ArrowDown may benefit from jumping to the highest existing row position instead:

    case 'ArrowDown':
      if (jump) {
        const existingPositions = Array.from(positionMapRef.current.keys())
        newRow = existingPositions.length > 0 ? Math.max(...existingPositions) : newRow
      } else {
        newRow = Math.min(totalRows - 1, newRow + 1)
      }
      break

    The docs say "Jump to edge of table" — landing on a gap row is inconsistent with that promise.

Last reviewed commit: 4c16333

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
@waleedlatif1
Copy link
Collaborator Author

@greptile

onRowMouseDown, onRowMouseEnter, and onClearSelection were no longer
used after the checkbox td was unified to use onRowToggle exclusively.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
@waleedlatif1
Copy link
Collaborator Author

@greptile

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
These callbacks were no longer passed to any child component after
the refactor to checkbox-only onRowToggle.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
@waleedlatif1
Copy link
Collaborator Author

Summary of changes since initial review

All three inline review comments have been addressed and resolved:

  1. handleCopy gap positions (f326b05) — Added if (!row) continue to skip gap positions in the checked-rows copy path, matching handleCut.

  2. Dead props in DataRow/PositionGapRows (e25770b) — Removed onRowMouseDown, onRowMouseEnter, and onClearSelection from both component interfaces, destructuring, memo comparators, and parent call sites.

  3. Shift+Space mixed selection state (116648b) — Added setSelectionAnchor(null) and setSelectionFocus(null) to the Shift+Space handler, matching the click-based handleRowToggle path.

Additionally, the following issue was raised in the PR summary review but not as an inline comment:

  1. Dead handleRowMouseDown and handleRowMouseEnter in parent component (4ea4490) — These two useCallback definitions were still in the parent Table component but no longer passed to any child after the refactor. Removed (26 lines deleted).

@waleedlatif1
Copy link
Collaborator Author

@greptile

Both shortcuts should work without a pre-selected cell. Cmd+A selects
all rows via checkboxes; Escape clears all selection state.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Previously Shift+Space cleared selectionAnchor, making all subsequent
keyboard shortcuts (arrows, enter, tab) inoperable until a cell was
clicked. Now it preserves the anchor and only clears focus, so users
can arrow-navigate and Shift+Space to toggle multiple rows in sequence.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
@waleedlatif1
Copy link
Collaborator Author

Additional fixes since last review

  1. Cmd+A requires prior cell selection (29c765c) — Moved Cmd+A and Escape above the anchor guard so they work without a pre-selected cell.

  2. Dead handleRowMouseDown / handleRowMouseEnter in parent (4ea4490) — Removed these unused useCallback definitions that were left behind after the refactor to onRowToggle.

  3. Shift+Space killed keyboard navigation (3e51e1c) — Previously Shift+Space cleared selectionAnchor, making all subsequent keyboard shortcuts (arrows, enter, tab) inoperable until the user clicked a cell. Now it preserves the anchor and only clears focus, so users can arrow-navigate and Shift+Space to toggle multiple rows in sequence.

@waleedlatif1
Copy link
Collaborator Author

@greptile

Previously the loop added every integer from..to, which in sparse
tables (with deleted rows) could add thousands of phantom gap positions.
Now iterates positionMap to only add positions with actual rows.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Cmd+A clears the anchor, so Delete/Backspace (which was behind the
anchor guard) silently did nothing after selecting all rows. Moved
the checked-rows branch of Delete/Backspace above the anchor guard.
Also fixed shift-click range to only add real row positions from
positionMap, avoiding phantom gap entries in sparse tables.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
@waleedlatif1
Copy link
Collaborator Author

Fixes since last review

  1. Shift-click range created phantom gap positions (57d7f76) — In sparse tables with deleted rows, the shift-click range loop added every integer from..to, creating thousands of phantom positions in checkedRows. Now iterates positionMap to only add positions with actual rows.

  2. Delete/Backspace did nothing after Cmd+A (57d7f76) — Cmd+A clears the anchor, and Delete/Backspace was behind the anchor guard, so it silently no-oped after select-all. Moved the checked-rows Delete branch above the anchor guard so it works without an anchor.

  3. Shift+Space anchor behavior — Intentionally kept the anchor after Shift+Space (unlike click-based toggle which clears it). This is deliberate: clearing the anchor would break keyboard flow, preventing subsequent arrow keys or Shift+Space presses. The small anchor overlay alongside checked rows is the trade-off for keyboard-driven multi-select (arrow down → Shift+Space → arrow down → Shift+Space).

All 6 review threads are resolved. Lint passes clean.

@waleedlatif1
Copy link
Collaborator Author

@greptile

… mode

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
@cursor
Copy link

cursor bot commented Mar 17, 2026

You have used all Bugbot PR reviews included in your free trial for your GitHub account on this workspace.

To continue using Bugbot reviews, enable Bugbot for your team in the Cursor dashboard.

@waleedlatif1
Copy link
Collaborator Author

@greptile

Comment on lines +966 to +988
if (checked.size > 0) {
e.preventDefault()
const sorted = Array.from(checked).sort((a, b) => a - b)
const lines: string[] = []
for (const pos of sorted) {
const row = pMap.get(pos)
if (!row) continue
const cells: string[] = cols.map((col) => {
const value: unknown = row.data[col.name]
if (value === null || value === undefined) return ''
return typeof value === 'object' ? JSON.stringify(value) : String(value)
})
lines.push(cells.join('\t'))
const updates: Record<string, unknown> = {}
const previousData: Record<string, unknown> = {}
for (const col of cols) {
previousData[col.name] = row.data[col.name] ?? null
updates[col.name] = null
}
undoCells.push({ rowId: row.id, data: previousData })
mutateRef.current({ rowId: row.id, data: updates })
}
e.clipboardData?.setData('text/plain', lines.join('\n'))
Copy link
Contributor

Choose a reason for hiding this comment

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

P2 Individual mutations per row in handleCut (checkbox path)

In the checkbox cut path, each row is mutated via a separate mutateRef.current(...) call inside the loop. For a large checkbox selection (e.g., 50+ rows), this fires 50+ concurrent network requests. The paste handler already uses batchUpdateRef.current for multi-row updates.

More critically, if the request to clear row N succeeds but the request to clear row N+1 fails (network drop mid-operation), the undo stack records all rows as cleared — so undo will correctly restore both rows. However, the table is left visually inconsistent (some cells blank, some not) until the failed mutations settle.

Consider collecting the updates and calling batchUpdateRef.current instead:

// After building undoCells and updates in the loop:
const batchUpdates = []
for (const pos of sorted) {
  const row = pMap.get(pos)
  if (!row) continue
  // ... build previousData + updates
  undoCells.push({ rowId: row.id, data: previousData })
  batchUpdates.push({ rowId: row.id, data: updates })
}
if (batchUpdates.length > 0) {
  batchUpdateRef.current({ updates: batchUpdates })
}
e.clipboardData?.setData('text/plain', lines.join('\n'))

The same applies to the Delete/Backspace handler at line 740–763, which also fires individual mutations per checked row.

Comment on lines +722 to +737
if (e.key === ' ' && e.shiftKey) {
const a = selectionAnchorRef.current
if (!a || editingCellRef.current) return
e.preventDefault()
setSelectionFocus(null)
setCheckedRows((prev) => {
const next = new Set(prev)
if (next.has(a.rowIndex)) {
next.delete(a.rowIndex)
} else {
next.add(a.rowIndex)
}
return next
})
lastCheckboxRowRef.current = a.rowIndex
return
Copy link
Contributor

Choose a reason for hiding this comment

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

P1 Shift+Space can add gap-row positions to checkedRows

a.rowIndex is the row coordinate from selectionAnchor, which is valid for any grid position — including gap positions that have no corresponding entry in positionMap. When the user navigates to a gap row and presses Shift+Space, that position is unconditionally added to checkedRows:

setCheckedRows((prev) => {
  const next = new Set(prev)
  if (next.has(a.rowIndex)) {
    next.delete(a.rowIndex)
  } else {
    next.add(a.rowIndex)   // gap position added here
  }
  return next
})

The downstream operations (copy, cut, delete) all gate on pMap.get(pos) and skip gaps, so no data corruption occurs. However, the gap row's checkbox will render as visually checked (because checkedRows.has(position) is true in PositionGapRows), and the gap position inflates checkedRows.size. This means isAllRowsSelected can under-count (checkedRows.size >= rows.length might be true while some real rows aren't checked because the set is padded with phantom positions).

Consider guarding with a positionMap lookup:

Suggested change
if (e.key === ' ' && e.shiftKey) {
const a = selectionAnchorRef.current
if (!a || editingCellRef.current) return
e.preventDefault()
setSelectionFocus(null)
setCheckedRows((prev) => {
const next = new Set(prev)
if (next.has(a.rowIndex)) {
next.delete(a.rowIndex)
} else {
next.add(a.rowIndex)
}
return next
})
lastCheckboxRowRef.current = a.rowIndex
return
if (e.key === ' ' && e.shiftKey) {
const a = selectionAnchorRef.current
if (!a || editingCellRef.current) return
e.preventDefault()
setSelectionFocus(null)
const pMap = positionMapRef.current
if (!pMap.has(a.rowIndex)) return
setCheckedRows((prev) => {


### Deleting Rows

Right-click a selected row (or group of selected rows) and choose **Delete row** from the context menu.
Copy link
Contributor

Choose a reason for hiding this comment

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

P2 Docs omit keyboard shortcut for row deletion

The "Deleting Rows" section says to use the context menu, but the keyboard shortcuts table (Clipboard section) lists Delete/Backspace as "Clear selected cells." Users who select rows via checkboxes and press Delete will find that it clears cell contents (not delete the rows). The distinction between cell-clearing and row-deletion is easy to miss.

Consider adding a note here:

Suggested change
Right-click a selected row (or group of selected rows) and choose **Delete row** from the context menu.
Right-click a selected row (or group of selected rows) and choose **Delete row** from the context menu. To clear cell contents without deleting the rows, select them and press **Delete** or **Backspace**.

@waleedlatif1 waleedlatif1 merged commit b930ee3 into staging Mar 17, 2026
11 checks passed
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