@@ -85,7 +85,38 @@ import {
8585
8686const logger = createLogger ( 'TableView' )
8787
88- const EMPTY_CHECKED_ROWS = new Set < string > ( )
88+ type RowSelection = { kind : 'none' } | { kind : 'some' ; ids : Set < string > } | { kind : 'all' }
89+
90+ const ROW_SELECTION_NONE : RowSelection = { kind : 'none' }
91+ const ROW_SELECTION_ALL : RowSelection = { kind : 'all' }
92+
93+ function rowSelectionIncludes ( sel : RowSelection , id : string ) : boolean {
94+ if ( sel . kind === 'all' ) return true
95+ if ( sel . kind === 'some' ) return sel . ids . has ( id )
96+ return false
97+ }
98+
99+ function rowSelectionIsEmpty ( sel : RowSelection ) : boolean {
100+ if ( sel . kind === 'none' ) return true
101+ if ( sel . kind === 'some' ) return sel . ids . size === 0
102+ return false
103+ }
104+
105+ function rowSelectionMaterialize ( sel : RowSelection , rows : TableRowType [ ] ) : Set < string > {
106+ if ( sel . kind === 'all' ) return new Set ( rows . map ( ( r ) => r . id ) )
107+ if ( sel . kind === 'some' ) return new Set ( sel . ids )
108+ return new Set < string > ( )
109+ }
110+
111+ function rowSelectionCoversAll ( sel : RowSelection , rows : TableRowType [ ] ) : boolean {
112+ if ( rows . length === 0 ) return false
113+ if ( sel . kind === 'all' ) return true
114+ if ( sel . kind === 'none' ) return false
115+ if ( sel . ids . size < rows . length ) return false
116+ for ( const r of rows ) if ( ! sel . ids . has ( r . id ) ) return false
117+ return true
118+ }
119+
89120const COL_WIDTH_MIN = 80
90121const COL_WIDTH_AUTO_FIT_MAX = 1000
91122// Wide enough to host the row-number + per-row run button side by side.
@@ -143,7 +174,7 @@ export function Table({
143174 const [ expandedCell , setExpandedCell ] = useState < EditingCell | null > ( null )
144175 const [ selectionAnchor , setSelectionAnchor ] = useState < CellCoord | null > ( null )
145176 const [ selectionFocus , setSelectionFocus ] = useState < CellCoord | null > ( null )
146- const [ checkedRows , setCheckedRows ] = useState ( EMPTY_CHECKED_ROWS )
177+ const [ rowSelection , setRowSelection ] = useState < RowSelection > ( ROW_SELECTION_NONE )
147178 const [ isColumnSelection , setIsColumnSelection ] = useState ( false )
148179 const lastCheckboxRowRef = useRef < string | null > ( null )
149180 const isColumnSelectionRef = useRef ( false )
@@ -379,22 +410,10 @@ export function Table({
379410 return null
380411 } , [ dropTargetColumnName , dragColumnName , dropSide , displayColumns , columnWidths ] )
381412
382- 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
388- }
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 ] )
413+ const isAllRowsSelected = useMemo (
414+ ( ) => rowSelectionCoversAll ( rowSelection , rows ) ,
415+ [ rowSelection , rows ]
416+ )
398417
399418 const isAllRowsSelectedRef = useRef ( isAllRowsSelected )
400419 isAllRowsSelectedRef . current = isAllRowsSelected
@@ -408,8 +427,8 @@ export function Table({
408427 const anchorRowIdRef = useRef < string | null > ( null )
409428 const focusRowIdRef = useRef < string | null > ( null )
410429
411- const checkedRowsRef = useRef ( checkedRows )
412- checkedRowsRef . current = checkedRows
430+ const rowSelectionRef = useRef ( rowSelection )
431+ rowSelectionRef . current = rowSelection
413432
414433 columnsRef . current = displayColumns
415434 schemaColumnsRef . current = columns
@@ -498,12 +517,16 @@ export function Table({
498517 return
499518 }
500519
501- const checked = checkedRowsRef . current
520+ const rowSel = rowSelectionRef . current
502521 const currentRows = rowsRef . current
503522 let snapshots : DeletedRowSnapshot [ ] = [ ]
504523
505- if ( checked . size > 0 && checked . has ( contextRow . id ) ) {
506- snapshots = collectRowSnapshots ( currentRows . filter ( ( r ) => checked . has ( r . id ) ) )
524+ const contextRowInRows = currentRows . some ( ( r ) => r . id === contextRow . id )
525+
526+ if ( rowSel . kind === 'all' && contextRowInRows ) {
527+ snapshots = collectRowSnapshots ( currentRows )
528+ } else if ( rowSel . kind === 'some' && rowSel . ids . has ( contextRow . id ) ) {
529+ snapshots = collectRowSnapshots ( currentRows . filter ( ( r ) => rowSel . ids . has ( r . id ) ) )
507530 } else {
508531 const sel = computeNormalizedSelection ( selectionAnchorRef . current , selectionFocusRef . current )
509532 const contextRowArrayIndex = currentRows . findIndex ( ( r ) => r . id === contextRow . id )
@@ -677,7 +700,7 @@ export function Table({
677700
678701 const handleCellMouseDown = useCallback (
679702 ( rowIndex : number , colIndex : number , shiftKey : boolean ) => {
680- setCheckedRows ( ( prev ) => ( prev . size === 0 ? prev : EMPTY_CHECKED_ROWS ) )
703+ setRowSelection ( ( prev ) => ( prev . kind === 'none' ? prev : ROW_SELECTION_NONE ) )
681704 setIsColumnSelection ( false )
682705 lastCheckboxRowRef . current = null
683706 if ( shiftKey && selectionAnchorRef . current ) {
@@ -713,33 +736,30 @@ export function Table({
713736 ? currentRows . findIndex ( ( r ) => r . id === lastCheckboxRowRef . current )
714737 : - 1
715738
716- if ( lastIdx !== - 1 ) {
717- const from = Math . min ( lastIdx , rowIndex )
718- const to = Math . max ( lastIdx , rowIndex )
719- setCheckedRows ( ( prev ) => {
720- const next = new Set ( prev )
739+ setRowSelection ( ( prev ) => {
740+ const next = rowSelectionMaterialize ( prev , currentRows )
741+ if ( lastIdx !== - 1 ) {
742+ const from = Math . min ( lastIdx , rowIndex )
743+ const to = Math . max ( lastIdx , rowIndex )
721744 for ( let i = from ; i <= to ; i ++ ) {
722745 const r = currentRows [ i ]
723746 if ( r ) next . add ( r . id )
724747 }
725- return next
726- } )
727- } else {
728- setCheckedRows ( ( prev ) => {
729- const next = new Set ( prev )
730- if ( next . has ( targetId ) ) next . delete ( targetId )
731- else next . add ( targetId )
732- return next
733- } )
734- }
748+ } else if ( next . has ( targetId ) ) {
749+ next . delete ( targetId )
750+ } else {
751+ next . add ( targetId )
752+ }
753+ return next . size === 0 ? ROW_SELECTION_NONE : { kind : 'some' , ids : next }
754+ } )
735755 lastCheckboxRowRef . current = targetId
736756 scrollRef . current ?. focus ( { preventScroll : true } )
737757 } , [ ] )
738758
739759 const handleClearSelection = useCallback ( ( ) => {
740760 setSelectionAnchor ( null )
741761 setSelectionFocus ( null )
742- setCheckedRows ( ( prev ) => ( prev . size === 0 ? prev : EMPTY_CHECKED_ROWS ) )
762+ setRowSelection ( ( prev ) => ( prev . kind === 'none' ? prev : ROW_SELECTION_NONE ) )
743763 setIsColumnSelection ( false )
744764 lastCheckboxRowRef . current = null
745765 } , [ ] )
@@ -749,7 +769,7 @@ export function Table({
749769 if ( lastRow < 0 ) return
750770
751771 setEditingCell ( null )
752- setCheckedRows ( ( prev ) => ( prev . size === 0 ? prev : EMPTY_CHECKED_ROWS ) )
772+ setRowSelection ( ( prev ) => ( prev . kind === 'none' ? prev : ROW_SELECTION_NONE ) )
753773 lastCheckboxRowRef . current = null
754774
755775 if ( shiftKey && isColumnSelectionRef . current && selectionAnchorRef . current ) {
@@ -768,7 +788,7 @@ export function Table({
768788 if ( lastRow < 0 ) return
769789
770790 setEditingCell ( null )
771- setCheckedRows ( ( prev ) => ( prev . size === 0 ? prev : EMPTY_CHECKED_ROWS ) )
791+ setRowSelection ( ( prev ) => ( prev . kind === 'none' ? prev : ROW_SELECTION_NONE ) )
772792 lastCheckboxRowRef . current = null
773793
774794 setSelectionAnchor ( { rowIndex : 0 , colIndex : startColIndex } )
@@ -783,7 +803,7 @@ export function Table({
783803 const currentCols = columnsRef . current
784804 if ( rws . length === 0 || currentCols . length === 0 ) return
785805 setEditingCell ( null )
786- setCheckedRows ( ( prev ) => ( prev . size === 0 ? prev : EMPTY_CHECKED_ROWS ) )
806+ setRowSelection ( ROW_SELECTION_ALL )
787807 lastCheckboxRowRef . current = null
788808 suppressFocusScrollRef . current = true
789809 setSelectionAnchor ( { rowIndex : 0 , colIndex : 0 } )
@@ -875,7 +895,7 @@ export function Table({
875895 setDragColumnName ( columnName )
876896 setSelectionAnchor ( null )
877897 setSelectionFocus ( null )
878- setCheckedRows ( ( prev ) => ( prev . size === 0 ? prev : EMPTY_CHECKED_ROWS ) )
898+ setRowSelection ( ( prev ) => ( prev . kind === 'none' ? prev : ROW_SELECTION_NONE ) )
879899 setIsColumnSelection ( false )
880900 } , [ ] )
881901
@@ -1339,7 +1359,7 @@ export function Table({
13391359 }
13401360 setSelectionAnchor ( null )
13411361 setSelectionFocus ( null )
1342- setCheckedRows ( ( prev ) => ( prev . size === 0 ? prev : EMPTY_CHECKED_ROWS ) )
1362+ setRowSelection ( ( prev ) => ( prev . kind === 'none' ? prev : ROW_SELECTION_NONE ) )
13431363 setIsColumnSelection ( false )
13441364 lastCheckboxRowRef . current = null
13451365 return
@@ -1352,7 +1372,7 @@ export function Table({
13521372 if ( rws . length > 0 && currentCols . length > 0 ) {
13531373 suppressFocusScrollRef . current = true
13541374 setEditingCell ( null )
1355- setCheckedRows ( ( prev ) => ( prev . size === 0 ? prev : EMPTY_CHECKED_ROWS ) )
1375+ setRowSelection ( ( prev ) => ( prev . kind === 'none' ? prev : ROW_SELECTION_NONE ) )
13561376 lastCheckboxRowRef . current = null
13571377 setSelectionAnchor ( { rowIndex : 0 , colIndex : 0 } )
13581378 setSelectionFocus ( {
@@ -1370,7 +1390,7 @@ export function Table({
13701390 const lastRow = rowsRef . current . length - 1
13711391 if ( lastRow < 0 ) return
13721392 e . preventDefault ( )
1373- setCheckedRows ( ( prev ) => ( prev . size === 0 ? prev : EMPTY_CHECKED_ROWS ) )
1393+ setRowSelection ( ( prev ) => ( prev . kind === 'none' ? prev : ROW_SELECTION_NONE ) )
13741394 lastCheckboxRowRef . current = null
13751395 setSelectionAnchor ( { rowIndex : 0 , colIndex : a . colIndex } )
13761396 setSelectionFocus ( { rowIndex : lastRow , colIndex : a . colIndex } )
@@ -1384,25 +1404,28 @@ export function Table({
13841404 const currentCols = columnsRef . current
13851405 if ( currentCols . length === 0 ) return
13861406 e . preventDefault ( )
1387- setCheckedRows ( ( prev ) => ( prev . size === 0 ? prev : EMPTY_CHECKED_ROWS ) )
1407+ setRowSelection ( ( prev ) => ( prev . kind === 'none' ? prev : ROW_SELECTION_NONE ) )
13881408 lastCheckboxRowRef . current = null
13891409 setIsColumnSelection ( false )
13901410 setSelectionAnchor ( { rowIndex : a . rowIndex , colIndex : 0 } )
13911411 setSelectionFocus ( { rowIndex : a . rowIndex , colIndex : currentCols . length - 1 } )
13921412 return
13931413 }
13941414
1395- if ( ( e . key === 'Delete' || e . key === 'Backspace' ) && checkedRowsRef . current . size > 0 ) {
1415+ if (
1416+ ( e . key === 'Delete' || e . key === 'Backspace' ) &&
1417+ ! rowSelectionIsEmpty ( rowSelectionRef . current )
1418+ ) {
13961419 if ( editingCellRef . current ) return
13971420 if ( ! canEditRef . current ) return
13981421 e . preventDefault ( )
1399- const checked = checkedRowsRef . current
1422+ const rowSel = rowSelectionRef . current
14001423 const currentRows = rowsRef . current
14011424 const currentCols = columnsRef . current
14021425 const undoCells : Array < { rowId : string ; data : Record < string , unknown > } > = [ ]
14031426 const batchUpdates : Array < { rowId : string ; data : Record < string , unknown > } > = [ ]
14041427 for ( const row of currentRows ) {
1405- if ( ! checked . has ( row . id ) ) continue
1428+ if ( ! rowSelectionIncludes ( rowSel , row . id ) ) continue
14061429 const updates : Record < string , unknown > = { }
14071430 const previousData : Record < string , unknown > = { }
14081431 for ( const col of currentCols ) {
@@ -1481,7 +1504,7 @@ export function Table({
14811504
14821505 if ( e . key === 'Tab' ) {
14831506 e . preventDefault ( )
1484- setCheckedRows ( ( prev ) => ( prev . size === 0 ? prev : EMPTY_CHECKED_ROWS ) )
1507+ setRowSelection ( ( prev ) => ( prev . kind === 'none' ? prev : ROW_SELECTION_NONE ) )
14851508 setIsColumnSelection ( false )
14861509 lastCheckboxRowRef . current = null
14871510 setSelectionAnchor ( moveCell ( anchor , cols . length , totalRows , e . shiftKey ? - 1 : 1 ) )
@@ -1491,7 +1514,7 @@ export function Table({
14911514
14921515 if ( [ 'ArrowUp' , 'ArrowDown' , 'ArrowLeft' , 'ArrowRight' ] . includes ( e . key ) ) {
14931516 e . preventDefault ( )
1494- setCheckedRows ( ( prev ) => ( prev . size === 0 ? prev : EMPTY_CHECKED_ROWS ) )
1517+ setRowSelection ( ( prev ) => ( prev . kind === 'none' ? prev : ROW_SELECTION_NONE ) )
14951518 setIsColumnSelection ( false )
14961519 lastCheckboxRowRef . current = null
14971520 const focus = selectionFocusRef . current ?? anchor
@@ -1669,15 +1692,15 @@ export function Table({
16691692 if ( tag === 'INPUT' || tag === 'TEXTAREA' ) return
16701693 if ( editingCellRef . current ) return
16711694
1672- const checked = checkedRowsRef . current
1695+ const rowSel = rowSelectionRef . current
16731696 const cols = columnsRef . current
16741697 const currentRows = rowsRef . current
16751698
1676- if ( checked . size > 0 ) {
1699+ if ( ! rowSelectionIsEmpty ( rowSel ) ) {
16771700 e . preventDefault ( )
16781701 const lines : string [ ] = [ ]
16791702 for ( const row of currentRows ) {
1680- if ( ! checked . has ( row . id ) ) continue
1703+ if ( ! rowSelectionIncludes ( rowSel , 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 ''
@@ -1720,17 +1743,17 @@ export function Table({
17201743 if ( editingCellRef . current ) return
17211744 if ( ! canEditRef . current ) return
17221745
1723- const checked = checkedRowsRef . current
1746+ const rowSel = rowSelectionRef . current
17241747 const cols = columnsRef . current
17251748 const currentRows = rowsRef . current
17261749 const undoCells : Array < { rowId : string ; data : Record < string , unknown > } > = [ ]
17271750 const batchUpdates : Array < { rowId : string ; data : Record < string , unknown > } > = [ ]
17281751
1729- if ( checked . size > 0 ) {
1752+ if ( ! rowSelectionIsEmpty ( rowSel ) ) {
17301753 e . preventDefault ( )
17311754 const lines : string [ ] = [ ]
17321755 for ( const row of currentRows ) {
1733- if ( ! checked . has ( row . id ) ) continue
1756+ if ( ! rowSelectionIncludes ( rowSel , row . id ) ) continue
17341757 const cells : string [ ] = cols . map ( ( col ) => {
17351758 const value : unknown = row . data [ col . name ]
17361759 if ( value === null || value === undefined ) return ''
@@ -2425,10 +2448,14 @@ export function Table({
24252448 const contextRow = contextMenu . isOpen ? contextMenu . row : null
24262449 if ( ! contextRow ) return 1
24272450
2428- if ( checkedRows . size > 0 && checkedRows . has ( contextRow . id ) ) {
2451+ if ( rowSelection . kind === 'all' ) {
2452+ return rows . some ( ( r ) => r . id === contextRow . id ) ? Math . max ( rows . length , 1 ) : 1
2453+ }
2454+
2455+ if ( rowSelection . kind === 'some' && rowSelection . ids . has ( contextRow . id ) ) {
24292456 let count = 0
24302457 for ( const row of rows ) {
2431- if ( checkedRows . has ( row . id ) ) count ++
2458+ if ( rowSelection . ids . has ( row . id ) ) count ++
24322459 }
24332460 return Math . max ( count , 1 )
24342461 }
@@ -2442,7 +2469,7 @@ export function Table({
24422469 const start = Math . max ( 0 , sel . startRow )
24432470 const end = Math . min ( rows . length - 1 , sel . endRow )
24442471 return Math . max ( end - start + 1 , 1 )
2445- } , [ contextMenu . isOpen , contextMenu . row , checkedRows , normalizedSelection , rows ] )
2472+ } , [ contextMenu . isOpen , contextMenu . row , rowSelection , normalizedSelection , rows ] )
24462473
24472474 const pendingUpdate = updateRowMutation . isPending ? updateRowMutation . variables : null
24482475
@@ -2756,7 +2783,7 @@ export function Table({
27562783 onContextMenu = { handleRowContextMenu }
27572784 onCellMouseDown = { handleCellMouseDown }
27582785 onCellMouseEnter = { handleCellMouseEnter }
2759- isRowChecked = { checkedRows . has ( row . id ) }
2786+ isRowChecked = { rowSelectionIncludes ( rowSelection , row . id ) }
27602787 onRowToggle = { handleRowToggle }
27612788 runningCount = { runningByRowId . get ( row . id ) ?? 0 }
27622789 hasWorkflowColumns = { hasWorkflowColumns }
@@ -3012,7 +3039,7 @@ interface DataRowProps {
30123039 workflowNameById : Record < string , string >
30133040}
30143041
3015- function rowSelectionChanged (
3042+ function cellRangeRowChanged (
30163043 rowIndex : number ,
30173044 colCount : number ,
30183045 prev : NormalizedSelection | null ,
@@ -3075,7 +3102,7 @@ function dataRowPropsAreEqual(prev: DataRowProps, next: DataRowProps): boolean {
30753102 return false
30763103 }
30773104
3078- return ! rowSelectionChanged (
3105+ return ! cellRangeRowChanged (
30793106 prev . rowIndex ,
30803107 prev . columns . length ,
30813108 prev . normalizedSelection ,
@@ -3109,13 +3136,7 @@ const DataRow = React.memo(function DataRow({
31093136} : DataRowProps ) {
31103137 const sel = normalizedSelection
31113138 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
3139+ const isRowSelected = isRowChecked
31193140
31203141 return (
31213142 < tr onContextMenu = { ( e ) => onContextMenu ( e , row ) } >
0 commit comments