From 22b555e09b7551583cc711686ee007204aa44b77 Mon Sep 17 00:00:00 2001 From: Waleed Latif Date: Tue, 5 May 2026 20:20:37 -0700 Subject: [PATCH 1/6] fix(tables): decouple master checkbox from cell-range, add allRowsSelected flag MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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. --- .../[tableId]/components/table/table.tsx | 84 ++++++++++++------- 1 file changed, 52 insertions(+), 32 deletions(-) diff --git a/apps/sim/app/workspace/[workspaceId]/tables/[tableId]/components/table/table.tsx b/apps/sim/app/workspace/[workspaceId]/tables/[tableId]/components/table/table.tsx index 1a1993aec6..3a50891699 100644 --- a/apps/sim/app/workspace/[workspaceId]/tables/[tableId]/components/table/table.tsx +++ b/apps/sim/app/workspace/[workspaceId]/tables/[tableId]/components/table/table.tsx @@ -144,6 +144,7 @@ export function Table({ const [selectionAnchor, setSelectionAnchor] = useState(null) const [selectionFocus, setSelectionFocus] = useState(null) const [checkedRows, setCheckedRows] = useState(EMPTY_CHECKED_ROWS) + const [allRowsSelected, setAllRowsSelected] = useState(false) const [isColumnSelection, setIsColumnSelection] = useState(false) const lastCheckboxRowRef = useRef(null) const isColumnSelectionRef = useRef(false) @@ -380,21 +381,14 @@ export function Table({ }, [dropTargetColumnName, dragColumnName, dropSide, displayColumns, columnWidths]) const isAllRowsSelected = useMemo(() => { - if (checkedRows.size > 0 && rows.length > 0 && checkedRows.size >= rows.length) { - for (const row of rows) { - if (!checkedRows.has(row.id)) return false - } - return true + if (rows.length === 0) return false + if (allRowsSelected) return true + if (checkedRows.size < rows.length) return false + for (let i = 0; i < rows.length; i++) { + if (!checkedRows.has(rows[i].id)) return false } - return ( - normalizedSelection !== null && - rows.length > 0 && - normalizedSelection.startRow === 0 && - normalizedSelection.endRow === rows.length - 1 && - normalizedSelection.startCol === 0 && - normalizedSelection.endCol === displayColumns.length - 1 - ) - }, [checkedRows, normalizedSelection, displayColumns.length, rows]) + return true + }, [allRowsSelected, checkedRows, rows]) const isAllRowsSelectedRef = useRef(isAllRowsSelected) isAllRowsSelectedRef.current = isAllRowsSelected @@ -411,6 +405,9 @@ export function Table({ const checkedRowsRef = useRef(checkedRows) checkedRowsRef.current = checkedRows + const allRowsSelectedRef = useRef(allRowsSelected) + allRowsSelectedRef.current = allRowsSelected + columnsRef.current = displayColumns schemaColumnsRef.current = columns workflowGroupsRef.current = tableWorkflowGroups @@ -499,10 +496,13 @@ export function Table({ } const checked = checkedRowsRef.current + const allChecked = allRowsSelectedRef.current const currentRows = rowsRef.current let snapshots: DeletedRowSnapshot[] = [] - if (checked.size > 0 && checked.has(contextRow.id)) { + if (allChecked) { + snapshots = collectRowSnapshots(currentRows) + } else if (checked.size > 0 && checked.has(contextRow.id)) { snapshots = collectRowSnapshots(currentRows.filter((r) => checked.has(r.id))) } else { const sel = computeNormalizedSelection(selectionAnchorRef.current, selectionFocusRef.current) @@ -678,6 +678,7 @@ export function Table({ const handleCellMouseDown = useCallback( (rowIndex: number, colIndex: number, shiftKey: boolean) => { setCheckedRows((prev) => (prev.size === 0 ? prev : EMPTY_CHECKED_ROWS)) + setAllRowsSelected(false) setIsColumnSelection(false) lastCheckboxRowRef.current = null if (shiftKey && selectionAnchorRef.current) { @@ -713,11 +714,17 @@ export function Table({ ? currentRows.findIndex((r) => r.id === lastCheckboxRowRef.current) : -1 + const wasAllSelected = allRowsSelectedRef.current + if (wasAllSelected) { + allRowsSelectedRef.current = false + setAllRowsSelected(false) + } + if (lastIdx !== -1) { const from = Math.min(lastIdx, rowIndex) const to = Math.max(lastIdx, rowIndex) setCheckedRows((prev) => { - const next = new Set(prev) + const next = wasAllSelected ? new Set(currentRows.map((r) => r.id)) : new Set(prev) for (let i = from; i <= to; i++) { const r = currentRows[i] if (r) next.add(r.id) @@ -726,7 +733,7 @@ export function Table({ }) } else { setCheckedRows((prev) => { - const next = new Set(prev) + const next = wasAllSelected ? new Set(currentRows.map((r) => r.id)) : new Set(prev) if (next.has(targetId)) next.delete(targetId) else next.add(targetId) return next @@ -740,6 +747,7 @@ export function Table({ setSelectionAnchor(null) setSelectionFocus(null) setCheckedRows((prev) => (prev.size === 0 ? prev : EMPTY_CHECKED_ROWS)) + setAllRowsSelected(false) setIsColumnSelection(false) lastCheckboxRowRef.current = null }, []) @@ -750,6 +758,7 @@ export function Table({ setEditingCell(null) setCheckedRows((prev) => (prev.size === 0 ? prev : EMPTY_CHECKED_ROWS)) + setAllRowsSelected(false) lastCheckboxRowRef.current = null if (shiftKey && isColumnSelectionRef.current && selectionAnchorRef.current) { @@ -769,6 +778,7 @@ export function Table({ setEditingCell(null) setCheckedRows((prev) => (prev.size === 0 ? prev : EMPTY_CHECKED_ROWS)) + setAllRowsSelected(false) lastCheckboxRowRef.current = null setSelectionAnchor({ rowIndex: 0, colIndex: startColIndex }) @@ -784,6 +794,7 @@ export function Table({ if (rws.length === 0 || currentCols.length === 0) return setEditingCell(null) setCheckedRows((prev) => (prev.size === 0 ? prev : EMPTY_CHECKED_ROWS)) + setAllRowsSelected(true) lastCheckboxRowRef.current = null suppressFocusScrollRef.current = true setSelectionAnchor({ rowIndex: 0, colIndex: 0 }) @@ -876,6 +887,7 @@ export function Table({ setSelectionAnchor(null) setSelectionFocus(null) setCheckedRows((prev) => (prev.size === 0 ? prev : EMPTY_CHECKED_ROWS)) + setAllRowsSelected(false) setIsColumnSelection(false) }, []) @@ -1340,6 +1352,7 @@ export function Table({ setSelectionAnchor(null) setSelectionFocus(null) setCheckedRows((prev) => (prev.size === 0 ? prev : EMPTY_CHECKED_ROWS)) + setAllRowsSelected(false) setIsColumnSelection(false) lastCheckboxRowRef.current = null return @@ -1353,6 +1366,7 @@ export function Table({ suppressFocusScrollRef.current = true setEditingCell(null) setCheckedRows((prev) => (prev.size === 0 ? prev : EMPTY_CHECKED_ROWS)) + setAllRowsSelected(false) lastCheckboxRowRef.current = null setSelectionAnchor({ rowIndex: 0, colIndex: 0 }) setSelectionFocus({ @@ -1371,6 +1385,7 @@ export function Table({ if (lastRow < 0) return e.preventDefault() setCheckedRows((prev) => (prev.size === 0 ? prev : EMPTY_CHECKED_ROWS)) + setAllRowsSelected(false) lastCheckboxRowRef.current = null setSelectionAnchor({ rowIndex: 0, colIndex: a.colIndex }) setSelectionFocus({ rowIndex: lastRow, colIndex: a.colIndex }) @@ -1385,6 +1400,7 @@ export function Table({ if (currentCols.length === 0) return e.preventDefault() setCheckedRows((prev) => (prev.size === 0 ? prev : EMPTY_CHECKED_ROWS)) + setAllRowsSelected(false) lastCheckboxRowRef.current = null setIsColumnSelection(false) setSelectionAnchor({ rowIndex: a.rowIndex, colIndex: 0 }) @@ -1392,17 +1408,21 @@ export function Table({ return } - if ((e.key === 'Delete' || e.key === 'Backspace') && checkedRowsRef.current.size > 0) { + if ( + (e.key === 'Delete' || e.key === 'Backspace') && + (checkedRowsRef.current.size > 0 || allRowsSelectedRef.current) + ) { if (editingCellRef.current) return if (!canEditRef.current) return e.preventDefault() const checked = checkedRowsRef.current + const allChecked = allRowsSelectedRef.current const currentRows = rowsRef.current const currentCols = columnsRef.current const undoCells: Array<{ rowId: string; data: Record }> = [] const batchUpdates: Array<{ rowId: string; data: Record }> = [] for (const row of currentRows) { - if (!checked.has(row.id)) continue + if (!allChecked && !checked.has(row.id)) continue const updates: Record = {} const previousData: Record = {} for (const col of currentCols) { @@ -1482,6 +1502,7 @@ export function Table({ if (e.key === 'Tab') { e.preventDefault() setCheckedRows((prev) => (prev.size === 0 ? prev : EMPTY_CHECKED_ROWS)) + setAllRowsSelected(false) setIsColumnSelection(false) lastCheckboxRowRef.current = null setSelectionAnchor(moveCell(anchor, cols.length, totalRows, e.shiftKey ? -1 : 1)) @@ -1492,6 +1513,7 @@ export function Table({ if (['ArrowUp', 'ArrowDown', 'ArrowLeft', 'ArrowRight'].includes(e.key)) { e.preventDefault() setCheckedRows((prev) => (prev.size === 0 ? prev : EMPTY_CHECKED_ROWS)) + setAllRowsSelected(false) setIsColumnSelection(false) lastCheckboxRowRef.current = null const focus = selectionFocusRef.current ?? anchor @@ -1670,14 +1692,15 @@ export function Table({ if (editingCellRef.current) return const checked = checkedRowsRef.current + const allChecked = allRowsSelectedRef.current const cols = columnsRef.current const currentRows = rowsRef.current - if (checked.size > 0) { + if (allChecked || checked.size > 0) { e.preventDefault() const lines: string[] = [] for (const row of currentRows) { - if (!checked.has(row.id)) continue + if (!allChecked && !checked.has(row.id)) continue const cells: string[] = cols.map((col) => { const value: unknown = row.data[col.name] if (value === null || value === undefined) return '' @@ -1721,16 +1744,17 @@ export function Table({ if (!canEditRef.current) return const checked = checkedRowsRef.current + const allChecked = allRowsSelectedRef.current const cols = columnsRef.current const currentRows = rowsRef.current const undoCells: Array<{ rowId: string; data: Record }> = [] const batchUpdates: Array<{ rowId: string; data: Record }> = [] - if (checked.size > 0) { + if (allChecked || checked.size > 0) { e.preventDefault() const lines: string[] = [] for (const row of currentRows) { - if (!checked.has(row.id)) continue + if (!allChecked && !checked.has(row.id)) continue const cells: string[] = cols.map((col) => { const value: unknown = row.data[col.name] if (value === null || value === undefined) return '' @@ -2425,6 +2449,8 @@ export function Table({ const contextRow = contextMenu.isOpen ? contextMenu.row : null if (!contextRow) return 1 + if (allRowsSelected) return Math.max(rows.length, 1) + if (checkedRows.size > 0 && checkedRows.has(contextRow.id)) { let count = 0 for (const row of rows) { @@ -2442,7 +2468,7 @@ export function Table({ const start = Math.max(0, sel.startRow) const end = Math.min(rows.length - 1, sel.endRow) return Math.max(end - start + 1, 1) - }, [contextMenu.isOpen, contextMenu.row, checkedRows, normalizedSelection, rows]) + }, [contextMenu.isOpen, contextMenu.row, allRowsSelected, checkedRows, normalizedSelection, rows]) const pendingUpdate = updateRowMutation.isPending ? updateRowMutation.variables : null @@ -2756,7 +2782,7 @@ export function Table({ onContextMenu={handleRowContextMenu} onCellMouseDown={handleCellMouseDown} onCellMouseEnter={handleCellMouseEnter} - isRowChecked={checkedRows.has(row.id)} + isRowChecked={allRowsSelected || checkedRows.has(row.id)} onRowToggle={handleRowToggle} runningCount={runningByRowId.get(row.id) ?? 0} hasWorkflowColumns={hasWorkflowColumns} @@ -3109,13 +3135,7 @@ const DataRow = React.memo(function DataRow({ }: DataRowProps) { const sel = normalizedSelection const isMultiCell = sel !== null && (sel.startRow !== sel.endRow || sel.startCol !== sel.endCol) - const isRowSelectedByRange = - sel !== null && - rowIndex >= sel.startRow && - rowIndex <= sel.endRow && - sel.startCol === 0 && - sel.endCol === columns.length - 1 - const isRowSelected = isRowChecked || isRowSelectedByRange + const isRowSelected = isRowChecked return ( onContextMenu(e, row)}> From d1c0eed4768401117f266f3faba1cc2f642078d0 Mon Sep 17 00:00:00 2001 From: Waleed Latif Date: Tue, 5 May 2026 20:31:16 -0700 Subject: [PATCH 2/6] refactor(tables): row selection as discriminated union Collapse `checkedRows: Set` + `allRowsSelected: boolean` into a single `RowSelection = { kind: 'none' | 'some' | 'all' }`. Impossible states (all + non-empty Set) become unrepresentable; predicates like `rowSelectionIncludes` and `rowSelectionIsEmpty` replace ad-hoc checks at every read site. --- .../[tableId]/components/table/table.tsx | 164 +++++++++--------- 1 file changed, 80 insertions(+), 84 deletions(-) diff --git a/apps/sim/app/workspace/[workspaceId]/tables/[tableId]/components/table/table.tsx b/apps/sim/app/workspace/[workspaceId]/tables/[tableId]/components/table/table.tsx index 3a50891699..446ad1219d 100644 --- a/apps/sim/app/workspace/[workspaceId]/tables/[tableId]/components/table/table.tsx +++ b/apps/sim/app/workspace/[workspaceId]/tables/[tableId]/components/table/table.tsx @@ -85,7 +85,37 @@ import { const logger = createLogger('TableView') -const EMPTY_CHECKED_ROWS = new Set() +type RowSelection = { kind: 'none' } | { kind: 'some'; ids: Set } | { kind: 'all' } + +const ROW_SELECTION_NONE: RowSelection = { kind: 'none' } +const ROW_SELECTION_ALL: RowSelection = { kind: 'all' } + +function rowSelectionIncludes(sel: RowSelection, id: string): boolean { + if (sel.kind === 'all') return true + if (sel.kind === 'some') return sel.ids.has(id) + return false +} + +function rowSelectionIsEmpty(sel: RowSelection): boolean { + if (sel.kind === 'none') return true + if (sel.kind === 'some') return sel.ids.size === 0 + return false +} + +function rowSelectionMaterialize(sel: RowSelection, rows: TableRowType[]): Set { + if (sel.kind === 'all') return new Set(rows.map((r) => r.id)) + if (sel.kind === 'some') return new Set(sel.ids) + return new Set() +} + +function rowSelectionCoversAll(sel: RowSelection, rows: TableRowType[]): boolean { + if (rows.length === 0) return false + if (sel.kind === 'all') return true + if (sel.kind === 'none') return false + if (sel.ids.size < rows.length) return false + for (const r of rows) if (!sel.ids.has(r.id)) return false + return true +} const COL_WIDTH_MIN = 80 const COL_WIDTH_AUTO_FIT_MAX = 1000 // Wide enough to host the row-number + per-row run button side by side. @@ -143,8 +173,7 @@ export function Table({ const [expandedCell, setExpandedCell] = useState(null) const [selectionAnchor, setSelectionAnchor] = useState(null) const [selectionFocus, setSelectionFocus] = useState(null) - const [checkedRows, setCheckedRows] = useState(EMPTY_CHECKED_ROWS) - const [allRowsSelected, setAllRowsSelected] = useState(false) + const [rowSelection, setRowSelection] = useState(ROW_SELECTION_NONE) const [isColumnSelection, setIsColumnSelection] = useState(false) const lastCheckboxRowRef = useRef(null) const isColumnSelectionRef = useRef(false) @@ -380,15 +409,10 @@ export function Table({ return null }, [dropTargetColumnName, dragColumnName, dropSide, displayColumns, columnWidths]) - const isAllRowsSelected = useMemo(() => { - if (rows.length === 0) return false - if (allRowsSelected) return true - if (checkedRows.size < rows.length) return false - for (let i = 0; i < rows.length; i++) { - if (!checkedRows.has(rows[i].id)) return false - } - return true - }, [allRowsSelected, checkedRows, rows]) + const isAllRowsSelected = useMemo( + () => rowSelectionCoversAll(rowSelection, rows), + [rowSelection, rows] + ) const isAllRowsSelectedRef = useRef(isAllRowsSelected) isAllRowsSelectedRef.current = isAllRowsSelected @@ -402,11 +426,8 @@ export function Table({ const anchorRowIdRef = useRef(null) const focusRowIdRef = useRef(null) - const checkedRowsRef = useRef(checkedRows) - checkedRowsRef.current = checkedRows - - const allRowsSelectedRef = useRef(allRowsSelected) - allRowsSelectedRef.current = allRowsSelected + const rowSelectionRef = useRef(rowSelection) + rowSelectionRef.current = rowSelection columnsRef.current = displayColumns schemaColumnsRef.current = columns @@ -495,15 +516,14 @@ export function Table({ return } - const checked = checkedRowsRef.current - const allChecked = allRowsSelectedRef.current + const rowSel = rowSelectionRef.current const currentRows = rowsRef.current let snapshots: DeletedRowSnapshot[] = [] - if (allChecked) { + if (rowSel.kind === 'all') { snapshots = collectRowSnapshots(currentRows) - } else if (checked.size > 0 && checked.has(contextRow.id)) { - snapshots = collectRowSnapshots(currentRows.filter((r) => checked.has(r.id))) + } else if (rowSel.kind === 'some' && rowSel.ids.has(contextRow.id)) { + snapshots = collectRowSnapshots(currentRows.filter((r) => rowSel.ids.has(r.id))) } else { const sel = computeNormalizedSelection(selectionAnchorRef.current, selectionFocusRef.current) const contextRowArrayIndex = currentRows.findIndex((r) => r.id === contextRow.id) @@ -677,8 +697,7 @@ export function Table({ const handleCellMouseDown = useCallback( (rowIndex: number, colIndex: number, shiftKey: boolean) => { - setCheckedRows((prev) => (prev.size === 0 ? prev : EMPTY_CHECKED_ROWS)) - setAllRowsSelected(false) + setRowSelection((prev) => (prev.kind === 'none' ? prev : ROW_SELECTION_NONE)) setIsColumnSelection(false) lastCheckboxRowRef.current = null if (shiftKey && selectionAnchorRef.current) { @@ -714,31 +733,22 @@ export function Table({ ? currentRows.findIndex((r) => r.id === lastCheckboxRowRef.current) : -1 - const wasAllSelected = allRowsSelectedRef.current - if (wasAllSelected) { - allRowsSelectedRef.current = false - setAllRowsSelected(false) - } - - if (lastIdx !== -1) { - const from = Math.min(lastIdx, rowIndex) - const to = Math.max(lastIdx, rowIndex) - setCheckedRows((prev) => { - const next = wasAllSelected ? new Set(currentRows.map((r) => r.id)) : new Set(prev) + setRowSelection((prev) => { + const next = rowSelectionMaterialize(prev, currentRows) + if (lastIdx !== -1) { + const from = Math.min(lastIdx, rowIndex) + const to = Math.max(lastIdx, rowIndex) for (let i = from; i <= to; i++) { const r = currentRows[i] if (r) next.add(r.id) } - return next - }) - } else { - setCheckedRows((prev) => { - const next = wasAllSelected ? new Set(currentRows.map((r) => r.id)) : new Set(prev) - if (next.has(targetId)) next.delete(targetId) - else next.add(targetId) - return next - }) - } + } else if (next.has(targetId)) { + next.delete(targetId) + } else { + next.add(targetId) + } + return next.size === 0 ? ROW_SELECTION_NONE : { kind: 'some', ids: next } + }) lastCheckboxRowRef.current = targetId scrollRef.current?.focus({ preventScroll: true }) }, []) @@ -746,8 +756,7 @@ export function Table({ const handleClearSelection = useCallback(() => { setSelectionAnchor(null) setSelectionFocus(null) - setCheckedRows((prev) => (prev.size === 0 ? prev : EMPTY_CHECKED_ROWS)) - setAllRowsSelected(false) + setRowSelection((prev) => (prev.kind === 'none' ? prev : ROW_SELECTION_NONE)) setIsColumnSelection(false) lastCheckboxRowRef.current = null }, []) @@ -757,8 +766,7 @@ export function Table({ if (lastRow < 0) return setEditingCell(null) - setCheckedRows((prev) => (prev.size === 0 ? prev : EMPTY_CHECKED_ROWS)) - setAllRowsSelected(false) + setRowSelection((prev) => (prev.kind === 'none' ? prev : ROW_SELECTION_NONE)) lastCheckboxRowRef.current = null if (shiftKey && isColumnSelectionRef.current && selectionAnchorRef.current) { @@ -777,8 +785,7 @@ export function Table({ if (lastRow < 0) return setEditingCell(null) - setCheckedRows((prev) => (prev.size === 0 ? prev : EMPTY_CHECKED_ROWS)) - setAllRowsSelected(false) + setRowSelection((prev) => (prev.kind === 'none' ? prev : ROW_SELECTION_NONE)) lastCheckboxRowRef.current = null setSelectionAnchor({ rowIndex: 0, colIndex: startColIndex }) @@ -793,8 +800,7 @@ export function Table({ const currentCols = columnsRef.current if (rws.length === 0 || currentCols.length === 0) return setEditingCell(null) - setCheckedRows((prev) => (prev.size === 0 ? prev : EMPTY_CHECKED_ROWS)) - setAllRowsSelected(true) + setRowSelection(ROW_SELECTION_ALL) lastCheckboxRowRef.current = null suppressFocusScrollRef.current = true setSelectionAnchor({ rowIndex: 0, colIndex: 0 }) @@ -886,8 +892,7 @@ export function Table({ setDragColumnName(columnName) setSelectionAnchor(null) setSelectionFocus(null) - setCheckedRows((prev) => (prev.size === 0 ? prev : EMPTY_CHECKED_ROWS)) - setAllRowsSelected(false) + setRowSelection((prev) => (prev.kind === 'none' ? prev : ROW_SELECTION_NONE)) setIsColumnSelection(false) }, []) @@ -1351,8 +1356,7 @@ export function Table({ } setSelectionAnchor(null) setSelectionFocus(null) - setCheckedRows((prev) => (prev.size === 0 ? prev : EMPTY_CHECKED_ROWS)) - setAllRowsSelected(false) + setRowSelection((prev) => (prev.kind === 'none' ? prev : ROW_SELECTION_NONE)) setIsColumnSelection(false) lastCheckboxRowRef.current = null return @@ -1365,8 +1369,7 @@ export function Table({ if (rws.length > 0 && currentCols.length > 0) { suppressFocusScrollRef.current = true setEditingCell(null) - setCheckedRows((prev) => (prev.size === 0 ? prev : EMPTY_CHECKED_ROWS)) - setAllRowsSelected(false) + setRowSelection((prev) => (prev.kind === 'none' ? prev : ROW_SELECTION_NONE)) lastCheckboxRowRef.current = null setSelectionAnchor({ rowIndex: 0, colIndex: 0 }) setSelectionFocus({ @@ -1384,8 +1387,7 @@ export function Table({ const lastRow = rowsRef.current.length - 1 if (lastRow < 0) return e.preventDefault() - setCheckedRows((prev) => (prev.size === 0 ? prev : EMPTY_CHECKED_ROWS)) - setAllRowsSelected(false) + setRowSelection((prev) => (prev.kind === 'none' ? prev : ROW_SELECTION_NONE)) lastCheckboxRowRef.current = null setSelectionAnchor({ rowIndex: 0, colIndex: a.colIndex }) setSelectionFocus({ rowIndex: lastRow, colIndex: a.colIndex }) @@ -1399,8 +1401,7 @@ export function Table({ const currentCols = columnsRef.current if (currentCols.length === 0) return e.preventDefault() - setCheckedRows((prev) => (prev.size === 0 ? prev : EMPTY_CHECKED_ROWS)) - setAllRowsSelected(false) + setRowSelection((prev) => (prev.kind === 'none' ? prev : ROW_SELECTION_NONE)) lastCheckboxRowRef.current = null setIsColumnSelection(false) setSelectionAnchor({ rowIndex: a.rowIndex, colIndex: 0 }) @@ -1410,19 +1411,18 @@ export function Table({ if ( (e.key === 'Delete' || e.key === 'Backspace') && - (checkedRowsRef.current.size > 0 || allRowsSelectedRef.current) + !rowSelectionIsEmpty(rowSelectionRef.current) ) { if (editingCellRef.current) return if (!canEditRef.current) return e.preventDefault() - const checked = checkedRowsRef.current - const allChecked = allRowsSelectedRef.current + const rowSel = rowSelectionRef.current const currentRows = rowsRef.current const currentCols = columnsRef.current const undoCells: Array<{ rowId: string; data: Record }> = [] const batchUpdates: Array<{ rowId: string; data: Record }> = [] for (const row of currentRows) { - if (!allChecked && !checked.has(row.id)) continue + if (!rowSelectionIncludes(rowSel, row.id)) continue const updates: Record = {} const previousData: Record = {} for (const col of currentCols) { @@ -1501,8 +1501,7 @@ export function Table({ if (e.key === 'Tab') { e.preventDefault() - setCheckedRows((prev) => (prev.size === 0 ? prev : EMPTY_CHECKED_ROWS)) - setAllRowsSelected(false) + setRowSelection((prev) => (prev.kind === 'none' ? prev : ROW_SELECTION_NONE)) setIsColumnSelection(false) lastCheckboxRowRef.current = null setSelectionAnchor(moveCell(anchor, cols.length, totalRows, e.shiftKey ? -1 : 1)) @@ -1512,8 +1511,7 @@ export function Table({ if (['ArrowUp', 'ArrowDown', 'ArrowLeft', 'ArrowRight'].includes(e.key)) { e.preventDefault() - setCheckedRows((prev) => (prev.size === 0 ? prev : EMPTY_CHECKED_ROWS)) - setAllRowsSelected(false) + setRowSelection((prev) => (prev.kind === 'none' ? prev : ROW_SELECTION_NONE)) setIsColumnSelection(false) lastCheckboxRowRef.current = null const focus = selectionFocusRef.current ?? anchor @@ -1691,16 +1689,15 @@ export function Table({ if (tag === 'INPUT' || tag === 'TEXTAREA') return if (editingCellRef.current) return - const checked = checkedRowsRef.current - const allChecked = allRowsSelectedRef.current + const rowSel = rowSelectionRef.current const cols = columnsRef.current const currentRows = rowsRef.current - if (allChecked || checked.size > 0) { + if (!rowSelectionIsEmpty(rowSel)) { e.preventDefault() const lines: string[] = [] for (const row of currentRows) { - if (!allChecked && !checked.has(row.id)) continue + if (!rowSelectionIncludes(rowSel, row.id)) continue const cells: string[] = cols.map((col) => { const value: unknown = row.data[col.name] if (value === null || value === undefined) return '' @@ -1743,18 +1740,17 @@ export function Table({ if (editingCellRef.current) return if (!canEditRef.current) return - const checked = checkedRowsRef.current - const allChecked = allRowsSelectedRef.current + const rowSel = rowSelectionRef.current const cols = columnsRef.current const currentRows = rowsRef.current const undoCells: Array<{ rowId: string; data: Record }> = [] const batchUpdates: Array<{ rowId: string; data: Record }> = [] - if (allChecked || checked.size > 0) { + if (!rowSelectionIsEmpty(rowSel)) { e.preventDefault() const lines: string[] = [] for (const row of currentRows) { - if (!allChecked && !checked.has(row.id)) continue + if (!rowSelectionIncludes(rowSel, row.id)) continue const cells: string[] = cols.map((col) => { const value: unknown = row.data[col.name] if (value === null || value === undefined) return '' @@ -2449,12 +2445,12 @@ export function Table({ const contextRow = contextMenu.isOpen ? contextMenu.row : null if (!contextRow) return 1 - if (allRowsSelected) return Math.max(rows.length, 1) + if (rowSelection.kind === 'all') return Math.max(rows.length, 1) - if (checkedRows.size > 0 && checkedRows.has(contextRow.id)) { + if (rowSelection.kind === 'some' && rowSelection.ids.has(contextRow.id)) { let count = 0 for (const row of rows) { - if (checkedRows.has(row.id)) count++ + if (rowSelection.ids.has(row.id)) count++ } return Math.max(count, 1) } @@ -2468,7 +2464,7 @@ export function Table({ const start = Math.max(0, sel.startRow) const end = Math.min(rows.length - 1, sel.endRow) return Math.max(end - start + 1, 1) - }, [contextMenu.isOpen, contextMenu.row, allRowsSelected, checkedRows, normalizedSelection, rows]) + }, [contextMenu.isOpen, contextMenu.row, rowSelection, normalizedSelection, rows]) const pendingUpdate = updateRowMutation.isPending ? updateRowMutation.variables : null @@ -2782,7 +2778,7 @@ export function Table({ onContextMenu={handleRowContextMenu} onCellMouseDown={handleCellMouseDown} onCellMouseEnter={handleCellMouseEnter} - isRowChecked={allRowsSelected || checkedRows.has(row.id)} + isRowChecked={rowSelectionIncludes(rowSelection, row.id)} onRowToggle={handleRowToggle} runningCount={runningByRowId.get(row.id) ?? 0} hasWorkflowColumns={hasWorkflowColumns} From 3fa050d4091e5a4dc1883685ba03926b5e08e3bf Mon Sep 17 00:00:00 2001 From: Waleed Latif Date: Tue, 5 May 2026 20:33:32 -0700 Subject: [PATCH 3/6] fix(tables): clear row selection after context-menu delete handleContextMenuDelete dispatched the delete but left rowSelection at its prior 'all' or 'some' state. After rows clear and a new row arrives (realtime, undo, append), rowSelectionIncludes returned true for it, rendering it checked and flipping the master checkbox back on. --- .../[workspaceId]/tables/[tableId]/components/table/table.tsx | 3 +++ 1 file changed, 3 insertions(+) diff --git a/apps/sim/app/workspace/[workspaceId]/tables/[tableId]/components/table/table.tsx b/apps/sim/app/workspace/[workspaceId]/tables/[tableId]/components/table/table.tsx index 446ad1219d..d4d594791e 100644 --- a/apps/sim/app/workspace/[workspaceId]/tables/[tableId]/components/table/table.tsx +++ b/apps/sim/app/workspace/[workspaceId]/tables/[tableId]/components/table/table.tsx @@ -541,6 +541,9 @@ export function Table({ if (snapshots.length > 0) { setDeletingRows(snapshots) + if (rowSel.kind !== 'none') { + setRowSelection(ROW_SELECTION_NONE) + } } closeContextMenu() From 0f986cf65f569c11b3a115fdb0ab217ae775b17f Mon Sep 17 00:00:00 2001 From: Waleed Latif Date: Tue, 5 May 2026 20:37:36 -0700 Subject: [PATCH 4/6] chore(tables): address review nits on row selection refactor - guard selectedRowCount 'all' branch on contextRow membership in rows - restore blank line between row-selection helpers and constants --- .../tables/[tableId]/components/table/table.tsx | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/apps/sim/app/workspace/[workspaceId]/tables/[tableId]/components/table/table.tsx b/apps/sim/app/workspace/[workspaceId]/tables/[tableId]/components/table/table.tsx index d4d594791e..22f61a3416 100644 --- a/apps/sim/app/workspace/[workspaceId]/tables/[tableId]/components/table/table.tsx +++ b/apps/sim/app/workspace/[workspaceId]/tables/[tableId]/components/table/table.tsx @@ -116,6 +116,7 @@ function rowSelectionCoversAll(sel: RowSelection, rows: TableRowType[]): boolean for (const r of rows) if (!sel.ids.has(r.id)) return false return true } + const COL_WIDTH_MIN = 80 const COL_WIDTH_AUTO_FIT_MAX = 1000 // Wide enough to host the row-number + per-row run button side by side. @@ -2448,7 +2449,9 @@ export function Table({ const contextRow = contextMenu.isOpen ? contextMenu.row : null if (!contextRow) return 1 - if (rowSelection.kind === 'all') return Math.max(rows.length, 1) + if (rowSelection.kind === 'all') { + return rows.some((r) => r.id === contextRow.id) ? Math.max(rows.length, 1) : 1 + } if (rowSelection.kind === 'some' && rowSelection.ids.has(contextRow.id)) { let count = 0 From fa8733c667737e48647baca7fa1cd9e43f16fe4b Mon Sep 17 00:00:00 2001 From: Waleed Latif Date: Tue, 5 May 2026 20:43:03 -0700 Subject: [PATCH 5/6] chore(tables): rename rowSelectionChanged to cellRangeRowChanged The helper compares NormalizedSelection (cell-range) state for a given row, not RowSelection. The old name collided with the new row-selection discriminated union and read ambiguously. --- .../[workspaceId]/tables/[tableId]/components/table/table.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/apps/sim/app/workspace/[workspaceId]/tables/[tableId]/components/table/table.tsx b/apps/sim/app/workspace/[workspaceId]/tables/[tableId]/components/table/table.tsx index 22f61a3416..61f450705a 100644 --- a/apps/sim/app/workspace/[workspaceId]/tables/[tableId]/components/table/table.tsx +++ b/apps/sim/app/workspace/[workspaceId]/tables/[tableId]/components/table/table.tsx @@ -3040,7 +3040,7 @@ interface DataRowProps { workflowNameById: Record } -function rowSelectionChanged( +function cellRangeRowChanged( rowIndex: number, colCount: number, prev: NormalizedSelection | null, @@ -3103,7 +3103,7 @@ function dataRowPropsAreEqual(prev: DataRowProps, next: DataRowProps): boolean { return false } - return !rowSelectionChanged( + return !cellRangeRowChanged( prev.rowIndex, prev.columns.length, prev.normalizedSelection, From e64787e481c3397b4f275f2d53cb3fc9abf2e981 Mon Sep 17 00:00:00 2001 From: Waleed Latif Date: Tue, 5 May 2026 21:27:16 -0700 Subject: [PATCH 6/6] fix(tables): guard context-menu delete on stale rows, preserve selection on cancel - Guard the kind='all' branch on contextRow membership in currentRows (matches the same fix applied to selectedRowCount), so a context menu on a stale row no longer deletes the entire table. - Drop the eager rowSelection clear at modal-open time. The modal's onSuccess already calls handleClearSelection after the mutation resolves, so the post-delete invariant still holds; if the user cancels, the selection is now preserved. --- .../tables/[tableId]/components/table/table.tsx | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/apps/sim/app/workspace/[workspaceId]/tables/[tableId]/components/table/table.tsx b/apps/sim/app/workspace/[workspaceId]/tables/[tableId]/components/table/table.tsx index 61f450705a..cbe1f625f4 100644 --- a/apps/sim/app/workspace/[workspaceId]/tables/[tableId]/components/table/table.tsx +++ b/apps/sim/app/workspace/[workspaceId]/tables/[tableId]/components/table/table.tsx @@ -521,7 +521,9 @@ export function Table({ const currentRows = rowsRef.current let snapshots: DeletedRowSnapshot[] = [] - if (rowSel.kind === 'all') { + const contextRowInRows = currentRows.some((r) => r.id === contextRow.id) + + if (rowSel.kind === 'all' && contextRowInRows) { snapshots = collectRowSnapshots(currentRows) } else if (rowSel.kind === 'some' && rowSel.ids.has(contextRow.id)) { snapshots = collectRowSnapshots(currentRows.filter((r) => rowSel.ids.has(r.id))) @@ -542,9 +544,6 @@ export function Table({ if (snapshots.length > 0) { setDeletingRows(snapshots) - if (rowSel.kind !== 'none') { - setRowSelection(ROW_SELECTION_NONE) - } } closeContextMenu()