Skip to content

Commit 22b555e

Browse files
committed
fix(tables): decouple master checkbox from cell-range, add allRowsSelected flag
Master checkbox detached from gutter selection state when rows or columns changed after Cmd+A: the predicate matched normalizedSelection bounds exactly (endRow === rows.length-1, endCol === displayColumns.length-1), so any post-selection growth flipped it false while the cell-range overlay still painted every row checked. Replace the structural two-branch predicate with an explicit allRowsSelected flag plus a uniform set-membership check. handleSelectAllRows sets the flag in O(1); handleRowToggle materializes checkedRows when toggling out of "all" mode. Bulk-op read sites (delete, copy, cut, selectedRowCount) honor the flag. Decouple gutter checkbox from cell-range drag: dragging cells no longer fills gutter checkboxes — they reflect explicit row-selection intent only, matching Sheets/Airtable. Cell-range overlay still paints cells.
1 parent 3a79289 commit 22b555e

1 file changed

Lines changed: 52 additions & 32 deletions

File tree

  • apps/sim/app/workspace/[workspaceId]/tables/[tableId]/components/table

apps/sim/app/workspace/[workspaceId]/tables/[tableId]/components/table/table.tsx

Lines changed: 52 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -144,6 +144,7 @@ export function Table({
144144
const [selectionAnchor, setSelectionAnchor] = useState<CellCoord | null>(null)
145145
const [selectionFocus, setSelectionFocus] = useState<CellCoord | null>(null)
146146
const [checkedRows, setCheckedRows] = useState(EMPTY_CHECKED_ROWS)
147+
const [allRowsSelected, setAllRowsSelected] = useState(false)
147148
const [isColumnSelection, setIsColumnSelection] = useState(false)
148149
const lastCheckboxRowRef = useRef<string | null>(null)
149150
const isColumnSelectionRef = useRef(false)
@@ -380,21 +381,14 @@ export function Table({
380381
}, [dropTargetColumnName, dragColumnName, dropSide, displayColumns, columnWidths])
381382

382383
const isAllRowsSelected = useMemo(() => {
383-
if (checkedRows.size > 0 && rows.length > 0 && checkedRows.size >= rows.length) {
384-
for (const row of rows) {
385-
if (!checkedRows.has(row.id)) return false
386-
}
387-
return true
384+
if (rows.length === 0) return false
385+
if (allRowsSelected) return true
386+
if (checkedRows.size < rows.length) return false
387+
for (let i = 0; i < rows.length; i++) {
388+
if (!checkedRows.has(rows[i].id)) return false
388389
}
389-
return (
390-
normalizedSelection !== null &&
391-
rows.length > 0 &&
392-
normalizedSelection.startRow === 0 &&
393-
normalizedSelection.endRow === rows.length - 1 &&
394-
normalizedSelection.startCol === 0 &&
395-
normalizedSelection.endCol === displayColumns.length - 1
396-
)
397-
}, [checkedRows, normalizedSelection, displayColumns.length, rows])
390+
return true
391+
}, [allRowsSelected, checkedRows, rows])
398392

399393
const isAllRowsSelectedRef = useRef(isAllRowsSelected)
400394
isAllRowsSelectedRef.current = isAllRowsSelected
@@ -411,6 +405,9 @@ export function Table({
411405
const checkedRowsRef = useRef(checkedRows)
412406
checkedRowsRef.current = checkedRows
413407

408+
const allRowsSelectedRef = useRef(allRowsSelected)
409+
allRowsSelectedRef.current = allRowsSelected
410+
414411
columnsRef.current = displayColumns
415412
schemaColumnsRef.current = columns
416413
workflowGroupsRef.current = tableWorkflowGroups
@@ -499,10 +496,13 @@ export function Table({
499496
}
500497

501498
const checked = checkedRowsRef.current
499+
const allChecked = allRowsSelectedRef.current
502500
const currentRows = rowsRef.current
503501
let snapshots: DeletedRowSnapshot[] = []
504502

505-
if (checked.size > 0 && checked.has(contextRow.id)) {
503+
if (allChecked) {
504+
snapshots = collectRowSnapshots(currentRows)
505+
} else if (checked.size > 0 && checked.has(contextRow.id)) {
506506
snapshots = collectRowSnapshots(currentRows.filter((r) => checked.has(r.id)))
507507
} else {
508508
const sel = computeNormalizedSelection(selectionAnchorRef.current, selectionFocusRef.current)
@@ -678,6 +678,7 @@ export function Table({
678678
const handleCellMouseDown = useCallback(
679679
(rowIndex: number, colIndex: number, shiftKey: boolean) => {
680680
setCheckedRows((prev) => (prev.size === 0 ? prev : EMPTY_CHECKED_ROWS))
681+
setAllRowsSelected(false)
681682
setIsColumnSelection(false)
682683
lastCheckboxRowRef.current = null
683684
if (shiftKey && selectionAnchorRef.current) {
@@ -713,11 +714,17 @@ export function Table({
713714
? currentRows.findIndex((r) => r.id === lastCheckboxRowRef.current)
714715
: -1
715716

717+
const wasAllSelected = allRowsSelectedRef.current
718+
if (wasAllSelected) {
719+
allRowsSelectedRef.current = false
720+
setAllRowsSelected(false)
721+
}
722+
716723
if (lastIdx !== -1) {
717724
const from = Math.min(lastIdx, rowIndex)
718725
const to = Math.max(lastIdx, rowIndex)
719726
setCheckedRows((prev) => {
720-
const next = new Set(prev)
727+
const next = wasAllSelected ? new Set(currentRows.map((r) => r.id)) : new Set(prev)
721728
for (let i = from; i <= to; i++) {
722729
const r = currentRows[i]
723730
if (r) next.add(r.id)
@@ -726,7 +733,7 @@ export function Table({
726733
})
727734
} else {
728735
setCheckedRows((prev) => {
729-
const next = new Set(prev)
736+
const next = wasAllSelected ? new Set(currentRows.map((r) => r.id)) : new Set(prev)
730737
if (next.has(targetId)) next.delete(targetId)
731738
else next.add(targetId)
732739
return next
@@ -740,6 +747,7 @@ export function Table({
740747
setSelectionAnchor(null)
741748
setSelectionFocus(null)
742749
setCheckedRows((prev) => (prev.size === 0 ? prev : EMPTY_CHECKED_ROWS))
750+
setAllRowsSelected(false)
743751
setIsColumnSelection(false)
744752
lastCheckboxRowRef.current = null
745753
}, [])
@@ -750,6 +758,7 @@ export function Table({
750758

751759
setEditingCell(null)
752760
setCheckedRows((prev) => (prev.size === 0 ? prev : EMPTY_CHECKED_ROWS))
761+
setAllRowsSelected(false)
753762
lastCheckboxRowRef.current = null
754763

755764
if (shiftKey && isColumnSelectionRef.current && selectionAnchorRef.current) {
@@ -769,6 +778,7 @@ export function Table({
769778

770779
setEditingCell(null)
771780
setCheckedRows((prev) => (prev.size === 0 ? prev : EMPTY_CHECKED_ROWS))
781+
setAllRowsSelected(false)
772782
lastCheckboxRowRef.current = null
773783

774784
setSelectionAnchor({ rowIndex: 0, colIndex: startColIndex })
@@ -784,6 +794,7 @@ export function Table({
784794
if (rws.length === 0 || currentCols.length === 0) return
785795
setEditingCell(null)
786796
setCheckedRows((prev) => (prev.size === 0 ? prev : EMPTY_CHECKED_ROWS))
797+
setAllRowsSelected(true)
787798
lastCheckboxRowRef.current = null
788799
suppressFocusScrollRef.current = true
789800
setSelectionAnchor({ rowIndex: 0, colIndex: 0 })
@@ -876,6 +887,7 @@ export function Table({
876887
setSelectionAnchor(null)
877888
setSelectionFocus(null)
878889
setCheckedRows((prev) => (prev.size === 0 ? prev : EMPTY_CHECKED_ROWS))
890+
setAllRowsSelected(false)
879891
setIsColumnSelection(false)
880892
}, [])
881893

@@ -1340,6 +1352,7 @@ export function Table({
13401352
setSelectionAnchor(null)
13411353
setSelectionFocus(null)
13421354
setCheckedRows((prev) => (prev.size === 0 ? prev : EMPTY_CHECKED_ROWS))
1355+
setAllRowsSelected(false)
13431356
setIsColumnSelection(false)
13441357
lastCheckboxRowRef.current = null
13451358
return
@@ -1353,6 +1366,7 @@ export function Table({
13531366
suppressFocusScrollRef.current = true
13541367
setEditingCell(null)
13551368
setCheckedRows((prev) => (prev.size === 0 ? prev : EMPTY_CHECKED_ROWS))
1369+
setAllRowsSelected(false)
13561370
lastCheckboxRowRef.current = null
13571371
setSelectionAnchor({ rowIndex: 0, colIndex: 0 })
13581372
setSelectionFocus({
@@ -1371,6 +1385,7 @@ export function Table({
13711385
if (lastRow < 0) return
13721386
e.preventDefault()
13731387
setCheckedRows((prev) => (prev.size === 0 ? prev : EMPTY_CHECKED_ROWS))
1388+
setAllRowsSelected(false)
13741389
lastCheckboxRowRef.current = null
13751390
setSelectionAnchor({ rowIndex: 0, colIndex: a.colIndex })
13761391
setSelectionFocus({ rowIndex: lastRow, colIndex: a.colIndex })
@@ -1385,24 +1400,29 @@ export function Table({
13851400
if (currentCols.length === 0) return
13861401
e.preventDefault()
13871402
setCheckedRows((prev) => (prev.size === 0 ? prev : EMPTY_CHECKED_ROWS))
1403+
setAllRowsSelected(false)
13881404
lastCheckboxRowRef.current = null
13891405
setIsColumnSelection(false)
13901406
setSelectionAnchor({ rowIndex: a.rowIndex, colIndex: 0 })
13911407
setSelectionFocus({ rowIndex: a.rowIndex, colIndex: currentCols.length - 1 })
13921408
return
13931409
}
13941410

1395-
if ((e.key === 'Delete' || e.key === 'Backspace') && checkedRowsRef.current.size > 0) {
1411+
if (
1412+
(e.key === 'Delete' || e.key === 'Backspace') &&
1413+
(checkedRowsRef.current.size > 0 || allRowsSelectedRef.current)
1414+
) {
13961415
if (editingCellRef.current) return
13971416
if (!canEditRef.current) return
13981417
e.preventDefault()
13991418
const checked = checkedRowsRef.current
1419+
const allChecked = allRowsSelectedRef.current
14001420
const currentRows = rowsRef.current
14011421
const currentCols = columnsRef.current
14021422
const undoCells: Array<{ rowId: string; data: Record<string, unknown> }> = []
14031423
const batchUpdates: Array<{ rowId: string; data: Record<string, unknown> }> = []
14041424
for (const row of currentRows) {
1405-
if (!checked.has(row.id)) continue
1425+
if (!allChecked && !checked.has(row.id)) continue
14061426
const updates: Record<string, unknown> = {}
14071427
const previousData: Record<string, unknown> = {}
14081428
for (const col of currentCols) {
@@ -1482,6 +1502,7 @@ export function Table({
14821502
if (e.key === 'Tab') {
14831503
e.preventDefault()
14841504
setCheckedRows((prev) => (prev.size === 0 ? prev : EMPTY_CHECKED_ROWS))
1505+
setAllRowsSelected(false)
14851506
setIsColumnSelection(false)
14861507
lastCheckboxRowRef.current = null
14871508
setSelectionAnchor(moveCell(anchor, cols.length, totalRows, e.shiftKey ? -1 : 1))
@@ -1492,6 +1513,7 @@ export function Table({
14921513
if (['ArrowUp', 'ArrowDown', 'ArrowLeft', 'ArrowRight'].includes(e.key)) {
14931514
e.preventDefault()
14941515
setCheckedRows((prev) => (prev.size === 0 ? prev : EMPTY_CHECKED_ROWS))
1516+
setAllRowsSelected(false)
14951517
setIsColumnSelection(false)
14961518
lastCheckboxRowRef.current = null
14971519
const focus = selectionFocusRef.current ?? anchor
@@ -1670,14 +1692,15 @@ export function Table({
16701692
if (editingCellRef.current) return
16711693

16721694
const checked = checkedRowsRef.current
1695+
const allChecked = allRowsSelectedRef.current
16731696
const cols = columnsRef.current
16741697
const currentRows = rowsRef.current
16751698

1676-
if (checked.size > 0) {
1699+
if (allChecked || checked.size > 0) {
16771700
e.preventDefault()
16781701
const lines: string[] = []
16791702
for (const row of currentRows) {
1680-
if (!checked.has(row.id)) continue
1703+
if (!allChecked && !checked.has(row.id)) continue
16811704
const cells: string[] = cols.map((col) => {
16821705
const value: unknown = row.data[col.name]
16831706
if (value === null || value === undefined) return ''
@@ -1721,16 +1744,17 @@ export function Table({
17211744
if (!canEditRef.current) return
17221745

17231746
const checked = checkedRowsRef.current
1747+
const allChecked = allRowsSelectedRef.current
17241748
const cols = columnsRef.current
17251749
const currentRows = rowsRef.current
17261750
const undoCells: Array<{ rowId: string; data: Record<string, unknown> }> = []
17271751
const batchUpdates: Array<{ rowId: string; data: Record<string, unknown> }> = []
17281752

1729-
if (checked.size > 0) {
1753+
if (allChecked || checked.size > 0) {
17301754
e.preventDefault()
17311755
const lines: string[] = []
17321756
for (const row of currentRows) {
1733-
if (!checked.has(row.id)) continue
1757+
if (!allChecked && !checked.has(row.id)) continue
17341758
const cells: string[] = cols.map((col) => {
17351759
const value: unknown = row.data[col.name]
17361760
if (value === null || value === undefined) return ''
@@ -2425,6 +2449,8 @@ export function Table({
24252449
const contextRow = contextMenu.isOpen ? contextMenu.row : null
24262450
if (!contextRow) return 1
24272451

2452+
if (allRowsSelected) return Math.max(rows.length, 1)
2453+
24282454
if (checkedRows.size > 0 && checkedRows.has(contextRow.id)) {
24292455
let count = 0
24302456
for (const row of rows) {
@@ -2442,7 +2468,7 @@ export function Table({
24422468
const start = Math.max(0, sel.startRow)
24432469
const end = Math.min(rows.length - 1, sel.endRow)
24442470
return Math.max(end - start + 1, 1)
2445-
}, [contextMenu.isOpen, contextMenu.row, checkedRows, normalizedSelection, rows])
2471+
}, [contextMenu.isOpen, contextMenu.row, allRowsSelected, checkedRows, normalizedSelection, rows])
24462472

24472473
const pendingUpdate = updateRowMutation.isPending ? updateRowMutation.variables : null
24482474

@@ -2756,7 +2782,7 @@ export function Table({
27562782
onContextMenu={handleRowContextMenu}
27572783
onCellMouseDown={handleCellMouseDown}
27582784
onCellMouseEnter={handleCellMouseEnter}
2759-
isRowChecked={checkedRows.has(row.id)}
2785+
isRowChecked={allRowsSelected || checkedRows.has(row.id)}
27602786
onRowToggle={handleRowToggle}
27612787
runningCount={runningByRowId.get(row.id) ?? 0}
27622788
hasWorkflowColumns={hasWorkflowColumns}
@@ -3109,13 +3135,7 @@ const DataRow = React.memo(function DataRow({
31093135
}: DataRowProps) {
31103136
const sel = normalizedSelection
31113137
const isMultiCell = sel !== null && (sel.startRow !== sel.endRow || sel.startCol !== sel.endCol)
3112-
const isRowSelectedByRange =
3113-
sel !== null &&
3114-
rowIndex >= sel.startRow &&
3115-
rowIndex <= sel.endRow &&
3116-
sel.startCol === 0 &&
3117-
sel.endCol === columns.length - 1
3118-
const isRowSelected = isRowChecked || isRowSelectedByRange
3138+
const isRowSelected = isRowChecked
31193139

31203140
return (
31213141
<tr onContextMenu={(e) => onContextMenu(e, row)}>

0 commit comments

Comments
 (0)