improvement(tables): tables multi-select, keyboard shortcuts, and docs#3615
improvement(tables): tables multi-select, keyboard shortcuts, and docs#3615waleedlatif1 merged 11 commits intostagingfrom
Conversation
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
|
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. |
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
Greptile SummaryThis PR replaces the old drag-based row selection model with a spreadsheet-style checkbox multi-select system, adds several new keyboard shortcuts ( Key observations:
Confidence Score: 3/5
Important Files Changed
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]
|
apps/sim/app/workspace/[workspaceId]/tables/[tableId]/components/table/table.tsx
Show resolved
Hide resolved
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
|
@greptile |
apps/sim/app/workspace/[workspaceId]/tables/[tableId]/components/table/table.tsx
Outdated
Show resolved
Hide resolved
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>
|
@greptile |
apps/sim/app/workspace/[workspaceId]/tables/[tableId]/components/table/table.tsx
Outdated
Show resolved
Hide resolved
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>
Summary of changes since initial reviewAll three inline review comments have been addressed and resolved:
Additionally, the following issue was raised in the PR summary review but not as an inline comment:
|
|
@greptile |
apps/sim/app/workspace/[workspaceId]/tables/[tableId]/components/table/table.tsx
Outdated
Show resolved
Hide resolved
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>
Additional fixes since last review
|
|
@greptile |
apps/sim/app/workspace/[workspaceId]/tables/[tableId]/components/table/table.tsx
Show resolved
Hide resolved
apps/sim/app/workspace/[workspaceId]/tables/[tableId]/components/table/table.tsx
Show resolved
Hide resolved
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>
Fixes since last review
All 6 review threads are resolved. Lint passes clean. |
|
@greptile |
apps/sim/app/workspace/[workspaceId]/tables/[tableId]/components/table/table.tsx
Show resolved
Hide resolved
… mode Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
|
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. |
|
@greptile |
| 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')) |
There was a problem hiding this comment.
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.
| 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 |
There was a problem hiding this comment.
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:
| 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. |
There was a problem hiding this comment.
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:
| 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**. |
Summary
EMPTY_CHECKED_ROWSconstant avoids unnecessary re-renders,PositionGapRowshas custom memo comparator,isAllRowsSelectediterates rows not positions.Test plan
/docs/en/tables