From 9a4e61a7f5b439e78a1c2ab2f4d6d84c7591b548 Mon Sep 17 00:00:00 2001 From: Waleed Latif Date: Mon, 4 May 2026 22:34:44 -0700 Subject: [PATCH] fix(tables): row gutter click toggles select; select-all works under sort/filter MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Move row-toggle onMouseDown from inner checkbox div to entire gutter so clicking anywhere in the gutter cell toggles the row, matching boolean cell behavior. Stop propagation on the per-row run button so it doesn't double as a row toggle. - Compute maxPosition via Math.max over loaded rows instead of rows[last].position. Under sort, the last visual row's position is not the largest position, so select-all/Ctrl+A produced a range that missed rows whose position fell outside [0, lastVisualRow.position]. - Add minPosition for the same reason and use it as the select-all anchor — under filter, position 0 may not be loaded, so anchoring at 0 produced a range that visually appeared empty. - Re-derive isAllRowsSelected from "every loaded row's position is within selection range" rather than checking exact start/end equality against maxPosition, so the top-left checkbox highlight is correct under any sort/filter state. Co-Authored-By: Claude Opus 4.7 --- .../[tableId]/components/table/table.tsx | 79 ++++++++++++------- 1 file changed, 51 insertions(+), 28 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 08ba169d1e..c9a0807d46 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 @@ -321,7 +321,24 @@ export function Table({ ) const hasWorkflowGroup = headerGroups.some((g) => g.kind === 'workflow') - const maxPosition = useMemo(() => (rows.length > 0 ? rows[rows.length - 1].position : -1), [rows]) + const maxPosition = useMemo(() => { + if (rows.length === 0) return -1 + let max = rows[0].position + for (let i = 1; i < rows.length; i++) { + if (rows[i].position > max) max = rows[i].position + } + return max + }, [rows]) + const minPosition = useMemo(() => { + if (rows.length === 0) return -1 + let min = rows[0].position + for (let i = 1; i < rows.length; i++) { + if (rows[i].position < min) min = rows[i].position + } + return min + }, [rows]) + const minPositionRef = useRef(minPosition) + minPositionRef.current = minPosition const maxPositionRef = useRef(maxPosition) maxPositionRef.current = maxPosition @@ -405,15 +422,20 @@ export function Table({ } return true } - return ( - normalizedSelection !== null && - maxPosition >= 0 && - normalizedSelection.startRow === 0 && - normalizedSelection.endRow === maxPosition && - normalizedSelection.startCol === 0 && - normalizedSelection.endCol === displayColumns.length - 1 - ) - }, [checkedRows, normalizedSelection, maxPosition, displayColumns.length, rows]) + if ( + normalizedSelection === null || + rows.length === 0 || + normalizedSelection.startCol !== 0 || + normalizedSelection.endCol !== displayColumns.length - 1 + ) { + return false + } + const { startRow, endRow } = normalizedSelection + for (const row of rows) { + if (row.position < startRow || row.position > endRow) return false + } + return true + }, [checkedRows, normalizedSelection, displayColumns.length, rows]) const isAllRowsSelectedRef = useRef(isAllRowsSelected) isAllRowsSelectedRef.current = isAllRowsSelected @@ -798,7 +820,7 @@ export function Table({ setEditingCell(null) setCheckedRows((prev) => (prev.size === 0 ? prev : EMPTY_CHECKED_ROWS)) suppressFocusScrollRef.current = true - setSelectionAnchor({ rowIndex: 0, colIndex: 0 }) + setSelectionAnchor({ rowIndex: minPositionRef.current, colIndex: 0 }) setSelectionFocus({ rowIndex: maxPositionRef.current, colIndex: currentCols.length - 1, @@ -1305,7 +1327,7 @@ export function Table({ suppressFocusScrollRef.current = true setEditingCell(null) setCheckedRows((prev) => (prev.size === 0 ? prev : EMPTY_CHECKED_ROWS)) - setSelectionAnchor({ rowIndex: 0, colIndex: 0 }) + setSelectionAnchor({ rowIndex: minPositionRef.current, colIndex: 0 }) setSelectionFocus({ rowIndex: maxPositionRef.current, colIndex: currentCols.length - 1, @@ -2985,15 +3007,15 @@ const PositionGapRows = React.memo( const isGapChecked = checkedRows.has(position) return ( - + { + if (e.button !== 0) return + onRowToggle(position, e.shiftKey) + }} + >
-
{ - if (e.button !== 0) return - onRowToggle(position, e.shiftKey) - }} - > +
onContextMenu(e, row)}> - + { + if (e.button !== 0) return + onRowToggle(rowIndex, e.shiftKey) + }} + >
-
{ - if (e.button !== 0) return - onRowToggle(rowIndex, e.shiftKey) - }} - > +
0 ? `Stop ${runningCount} running` : 'Run row'} title={runningCount > 0 ? `Stop ${runningCount} running` : 'Run row'} className='ml-auto flex h-[20px] w-[20px] shrink-0 items-center justify-center rounded text-[var(--text-primary)] transition-colors hover-hover:bg-[var(--surface-2)]' + onMouseDown={(e) => e.stopPropagation()} onClick={() => { if (runningCount > 0) { onStopRow(row.id)