@@ -85,7 +85,37 @@ 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+ }
89119const COL_WIDTH_MIN = 80
90120const COL_WIDTH_AUTO_FIT_MAX = 1000
91121// Wide enough to host the row-number + per-row run button side by side.
@@ -143,8 +173,7 @@ export function Table({
143173 const [ expandedCell , setExpandedCell ] = useState < EditingCell | null > ( null )
144174 const [ selectionAnchor , setSelectionAnchor ] = useState < CellCoord | null > ( null )
145175 const [ selectionFocus , setSelectionFocus ] = useState < CellCoord | null > ( null )
146- const [ checkedRows , setCheckedRows ] = useState ( EMPTY_CHECKED_ROWS )
147- const [ allRowsSelected , setAllRowsSelected ] = useState ( false )
176+ const [ rowSelection , setRowSelection ] = useState < RowSelection > ( ROW_SELECTION_NONE )
148177 const [ isColumnSelection , setIsColumnSelection ] = useState ( false )
149178 const lastCheckboxRowRef = useRef < string | null > ( null )
150179 const isColumnSelectionRef = useRef ( false )
@@ -380,15 +409,10 @@ export function Table({
380409 return null
381410 } , [ dropTargetColumnName , dragColumnName , dropSide , displayColumns , columnWidths ] )
382411
383- const isAllRowsSelected = useMemo ( ( ) => {
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
389- }
390- return true
391- } , [ allRowsSelected , checkedRows , rows ] )
412+ const isAllRowsSelected = useMemo (
413+ ( ) => rowSelectionCoversAll ( rowSelection , rows ) ,
414+ [ rowSelection , rows ]
415+ )
392416
393417 const isAllRowsSelectedRef = useRef ( isAllRowsSelected )
394418 isAllRowsSelectedRef . current = isAllRowsSelected
@@ -402,11 +426,8 @@ export function Table({
402426 const anchorRowIdRef = useRef < string | null > ( null )
403427 const focusRowIdRef = useRef < string | null > ( null )
404428
405- const checkedRowsRef = useRef ( checkedRows )
406- checkedRowsRef . current = checkedRows
407-
408- const allRowsSelectedRef = useRef ( allRowsSelected )
409- allRowsSelectedRef . current = allRowsSelected
429+ const rowSelectionRef = useRef ( rowSelection )
430+ rowSelectionRef . current = rowSelection
410431
411432 columnsRef . current = displayColumns
412433 schemaColumnsRef . current = columns
@@ -495,15 +516,14 @@ export function Table({
495516 return
496517 }
497518
498- const checked = checkedRowsRef . current
499- const allChecked = allRowsSelectedRef . current
519+ const rowSel = rowSelectionRef . current
500520 const currentRows = rowsRef . current
501521 let snapshots : DeletedRowSnapshot [ ] = [ ]
502522
503- if ( allChecked ) {
523+ if ( rowSel . kind === 'all' ) {
504524 snapshots = collectRowSnapshots ( currentRows )
505- } else if ( checked . size > 0 && checked . has ( contextRow . id ) ) {
506- snapshots = collectRowSnapshots ( currentRows . filter ( ( r ) => checked . has ( r . id ) ) )
525+ } else if ( rowSel . kind === 'some' && rowSel . ids . has ( contextRow . id ) ) {
526+ snapshots = collectRowSnapshots ( currentRows . filter ( ( r ) => rowSel . ids . has ( r . id ) ) )
507527 } else {
508528 const sel = computeNormalizedSelection ( selectionAnchorRef . current , selectionFocusRef . current )
509529 const contextRowArrayIndex = currentRows . findIndex ( ( r ) => r . id === contextRow . id )
@@ -677,8 +697,7 @@ export function Table({
677697
678698 const handleCellMouseDown = useCallback (
679699 ( rowIndex : number , colIndex : number , shiftKey : boolean ) => {
680- setCheckedRows ( ( prev ) => ( prev . size === 0 ? prev : EMPTY_CHECKED_ROWS ) )
681- setAllRowsSelected ( false )
700+ setRowSelection ( ( prev ) => ( prev . kind === 'none' ? prev : ROW_SELECTION_NONE ) )
682701 setIsColumnSelection ( false )
683702 lastCheckboxRowRef . current = null
684703 if ( shiftKey && selectionAnchorRef . current ) {
@@ -714,40 +733,30 @@ export function Table({
714733 ? currentRows . findIndex ( ( r ) => r . id === lastCheckboxRowRef . current )
715734 : - 1
716735
717- const wasAllSelected = allRowsSelectedRef . current
718- if ( wasAllSelected ) {
719- allRowsSelectedRef . current = false
720- setAllRowsSelected ( false )
721- }
722-
723- if ( lastIdx !== - 1 ) {
724- const from = Math . min ( lastIdx , rowIndex )
725- const to = Math . max ( lastIdx , rowIndex )
726- setCheckedRows ( ( prev ) => {
727- const next = wasAllSelected ? new Set ( currentRows . map ( ( r ) => r . id ) ) : new Set ( prev )
736+ setRowSelection ( ( prev ) => {
737+ const next = rowSelectionMaterialize ( prev , currentRows )
738+ if ( lastIdx !== - 1 ) {
739+ const from = Math . min ( lastIdx , rowIndex )
740+ const to = Math . max ( lastIdx , rowIndex )
728741 for ( let i = from ; i <= to ; i ++ ) {
729742 const r = currentRows [ i ]
730743 if ( r ) next . add ( r . id )
731744 }
732- return next
733- } )
734- } else {
735- setCheckedRows ( ( prev ) => {
736- const next = wasAllSelected ? new Set ( currentRows . map ( ( r ) => r . id ) ) : new Set ( prev )
737- if ( next . has ( targetId ) ) next . delete ( targetId )
738- else next . add ( targetId )
739- return next
740- } )
741- }
745+ } else if ( next . has ( targetId ) ) {
746+ next . delete ( targetId )
747+ } else {
748+ next . add ( targetId )
749+ }
750+ return next . size === 0 ? ROW_SELECTION_NONE : { kind : 'some' , ids : next }
751+ } )
742752 lastCheckboxRowRef . current = targetId
743753 scrollRef . current ?. focus ( { preventScroll : true } )
744754 } , [ ] )
745755
746756 const handleClearSelection = useCallback ( ( ) => {
747757 setSelectionAnchor ( null )
748758 setSelectionFocus ( null )
749- setCheckedRows ( ( prev ) => ( prev . size === 0 ? prev : EMPTY_CHECKED_ROWS ) )
750- setAllRowsSelected ( false )
759+ setRowSelection ( ( prev ) => ( prev . kind === 'none' ? prev : ROW_SELECTION_NONE ) )
751760 setIsColumnSelection ( false )
752761 lastCheckboxRowRef . current = null
753762 } , [ ] )
@@ -757,8 +766,7 @@ export function Table({
757766 if ( lastRow < 0 ) return
758767
759768 setEditingCell ( null )
760- setCheckedRows ( ( prev ) => ( prev . size === 0 ? prev : EMPTY_CHECKED_ROWS ) )
761- setAllRowsSelected ( false )
769+ setRowSelection ( ( prev ) => ( prev . kind === 'none' ? prev : ROW_SELECTION_NONE ) )
762770 lastCheckboxRowRef . current = null
763771
764772 if ( shiftKey && isColumnSelectionRef . current && selectionAnchorRef . current ) {
@@ -777,8 +785,7 @@ export function Table({
777785 if ( lastRow < 0 ) return
778786
779787 setEditingCell ( null )
780- setCheckedRows ( ( prev ) => ( prev . size === 0 ? prev : EMPTY_CHECKED_ROWS ) )
781- setAllRowsSelected ( false )
788+ setRowSelection ( ( prev ) => ( prev . kind === 'none' ? prev : ROW_SELECTION_NONE ) )
782789 lastCheckboxRowRef . current = null
783790
784791 setSelectionAnchor ( { rowIndex : 0 , colIndex : startColIndex } )
@@ -793,8 +800,7 @@ export function Table({
793800 const currentCols = columnsRef . current
794801 if ( rws . length === 0 || currentCols . length === 0 ) return
795802 setEditingCell ( null )
796- setCheckedRows ( ( prev ) => ( prev . size === 0 ? prev : EMPTY_CHECKED_ROWS ) )
797- setAllRowsSelected ( true )
803+ setRowSelection ( ROW_SELECTION_ALL )
798804 lastCheckboxRowRef . current = null
799805 suppressFocusScrollRef . current = true
800806 setSelectionAnchor ( { rowIndex : 0 , colIndex : 0 } )
@@ -886,8 +892,7 @@ export function Table({
886892 setDragColumnName ( columnName )
887893 setSelectionAnchor ( null )
888894 setSelectionFocus ( null )
889- setCheckedRows ( ( prev ) => ( prev . size === 0 ? prev : EMPTY_CHECKED_ROWS ) )
890- setAllRowsSelected ( false )
895+ setRowSelection ( ( prev ) => ( prev . kind === 'none' ? prev : ROW_SELECTION_NONE ) )
891896 setIsColumnSelection ( false )
892897 } , [ ] )
893898
@@ -1351,8 +1356,7 @@ export function Table({
13511356 }
13521357 setSelectionAnchor ( null )
13531358 setSelectionFocus ( null )
1354- setCheckedRows ( ( prev ) => ( prev . size === 0 ? prev : EMPTY_CHECKED_ROWS ) )
1355- setAllRowsSelected ( false )
1359+ setRowSelection ( ( prev ) => ( prev . kind === 'none' ? prev : ROW_SELECTION_NONE ) )
13561360 setIsColumnSelection ( false )
13571361 lastCheckboxRowRef . current = null
13581362 return
@@ -1365,8 +1369,7 @@ export function Table({
13651369 if ( rws . length > 0 && currentCols . length > 0 ) {
13661370 suppressFocusScrollRef . current = true
13671371 setEditingCell ( null )
1368- setCheckedRows ( ( prev ) => ( prev . size === 0 ? prev : EMPTY_CHECKED_ROWS ) )
1369- setAllRowsSelected ( false )
1372+ setRowSelection ( ( prev ) => ( prev . kind === 'none' ? prev : ROW_SELECTION_NONE ) )
13701373 lastCheckboxRowRef . current = null
13711374 setSelectionAnchor ( { rowIndex : 0 , colIndex : 0 } )
13721375 setSelectionFocus ( {
@@ -1384,8 +1387,7 @@ export function Table({
13841387 const lastRow = rowsRef . current . length - 1
13851388 if ( lastRow < 0 ) return
13861389 e . preventDefault ( )
1387- setCheckedRows ( ( prev ) => ( prev . size === 0 ? prev : EMPTY_CHECKED_ROWS ) )
1388- setAllRowsSelected ( false )
1390+ setRowSelection ( ( prev ) => ( prev . kind === 'none' ? prev : ROW_SELECTION_NONE ) )
13891391 lastCheckboxRowRef . current = null
13901392 setSelectionAnchor ( { rowIndex : 0 , colIndex : a . colIndex } )
13911393 setSelectionFocus ( { rowIndex : lastRow , colIndex : a . colIndex } )
@@ -1399,8 +1401,7 @@ export function Table({
13991401 const currentCols = columnsRef . current
14001402 if ( currentCols . length === 0 ) return
14011403 e . preventDefault ( )
1402- setCheckedRows ( ( prev ) => ( prev . size === 0 ? prev : EMPTY_CHECKED_ROWS ) )
1403- setAllRowsSelected ( false )
1404+ setRowSelection ( ( prev ) => ( prev . kind === 'none' ? prev : ROW_SELECTION_NONE ) )
14041405 lastCheckboxRowRef . current = null
14051406 setIsColumnSelection ( false )
14061407 setSelectionAnchor ( { rowIndex : a . rowIndex , colIndex : 0 } )
@@ -1410,19 +1411,18 @@ export function Table({
14101411
14111412 if (
14121413 ( e . key === 'Delete' || e . key === 'Backspace' ) &&
1413- ( checkedRowsRef . current . size > 0 || allRowsSelectedRef . current )
1414+ ! rowSelectionIsEmpty ( rowSelectionRef . current )
14141415 ) {
14151416 if ( editingCellRef . current ) return
14161417 if ( ! canEditRef . current ) return
14171418 e . preventDefault ( )
1418- const checked = checkedRowsRef . current
1419- const allChecked = allRowsSelectedRef . current
1419+ const rowSel = rowSelectionRef . current
14201420 const currentRows = rowsRef . current
14211421 const currentCols = columnsRef . current
14221422 const undoCells : Array < { rowId : string ; data : Record < string , unknown > } > = [ ]
14231423 const batchUpdates : Array < { rowId : string ; data : Record < string , unknown > } > = [ ]
14241424 for ( const row of currentRows ) {
1425- if ( ! allChecked && ! checked . has ( row . id ) ) continue
1425+ if ( ! rowSelectionIncludes ( rowSel , row . id ) ) continue
14261426 const updates : Record < string , unknown > = { }
14271427 const previousData : Record < string , unknown > = { }
14281428 for ( const col of currentCols ) {
@@ -1501,8 +1501,7 @@ export function Table({
15011501
15021502 if ( e . key === 'Tab' ) {
15031503 e . preventDefault ( )
1504- setCheckedRows ( ( prev ) => ( prev . size === 0 ? prev : EMPTY_CHECKED_ROWS ) )
1505- setAllRowsSelected ( false )
1504+ setRowSelection ( ( prev ) => ( prev . kind === 'none' ? prev : ROW_SELECTION_NONE ) )
15061505 setIsColumnSelection ( false )
15071506 lastCheckboxRowRef . current = null
15081507 setSelectionAnchor ( moveCell ( anchor , cols . length , totalRows , e . shiftKey ? - 1 : 1 ) )
@@ -1512,8 +1511,7 @@ export function Table({
15121511
15131512 if ( [ 'ArrowUp' , 'ArrowDown' , 'ArrowLeft' , 'ArrowRight' ] . includes ( e . key ) ) {
15141513 e . preventDefault ( )
1515- setCheckedRows ( ( prev ) => ( prev . size === 0 ? prev : EMPTY_CHECKED_ROWS ) )
1516- setAllRowsSelected ( false )
1514+ setRowSelection ( ( prev ) => ( prev . kind === 'none' ? prev : ROW_SELECTION_NONE ) )
15171515 setIsColumnSelection ( false )
15181516 lastCheckboxRowRef . current = null
15191517 const focus = selectionFocusRef . current ?? anchor
@@ -1691,16 +1689,15 @@ export function Table({
16911689 if ( tag === 'INPUT' || tag === 'TEXTAREA' ) return
16921690 if ( editingCellRef . current ) return
16931691
1694- const checked = checkedRowsRef . current
1695- const allChecked = allRowsSelectedRef . current
1692+ const rowSel = rowSelectionRef . current
16961693 const cols = columnsRef . current
16971694 const currentRows = rowsRef . current
16981695
1699- if ( allChecked || checked . size > 0 ) {
1696+ if ( ! rowSelectionIsEmpty ( rowSel ) ) {
17001697 e . preventDefault ( )
17011698 const lines : string [ ] = [ ]
17021699 for ( const row of currentRows ) {
1703- if ( ! allChecked && ! checked . has ( row . id ) ) continue
1700+ if ( ! rowSelectionIncludes ( rowSel , row . id ) ) continue
17041701 const cells : string [ ] = cols . map ( ( col ) => {
17051702 const value : unknown = row . data [ col . name ]
17061703 if ( value === null || value === undefined ) return ''
@@ -1743,18 +1740,17 @@ export function Table({
17431740 if ( editingCellRef . current ) return
17441741 if ( ! canEditRef . current ) return
17451742
1746- const checked = checkedRowsRef . current
1747- const allChecked = allRowsSelectedRef . current
1743+ const rowSel = rowSelectionRef . current
17481744 const cols = columnsRef . current
17491745 const currentRows = rowsRef . current
17501746 const undoCells : Array < { rowId : string ; data : Record < string , unknown > } > = [ ]
17511747 const batchUpdates : Array < { rowId : string ; data : Record < string , unknown > } > = [ ]
17521748
1753- if ( allChecked || checked . size > 0 ) {
1749+ if ( ! rowSelectionIsEmpty ( rowSel ) ) {
17541750 e . preventDefault ( )
17551751 const lines : string [ ] = [ ]
17561752 for ( const row of currentRows ) {
1757- if ( ! allChecked && ! checked . has ( row . id ) ) continue
1753+ if ( ! rowSelectionIncludes ( rowSel , row . id ) ) continue
17581754 const cells : string [ ] = cols . map ( ( col ) => {
17591755 const value : unknown = row . data [ col . name ]
17601756 if ( value === null || value === undefined ) return ''
@@ -2449,12 +2445,12 @@ export function Table({
24492445 const contextRow = contextMenu . isOpen ? contextMenu . row : null
24502446 if ( ! contextRow ) return 1
24512447
2452- if ( allRowsSelected ) return Math . max ( rows . length , 1 )
2448+ if ( rowSelection . kind === 'all' ) return Math . max ( rows . length , 1 )
24532449
2454- if ( checkedRows . size > 0 && checkedRows . has ( contextRow . id ) ) {
2450+ if ( rowSelection . kind === 'some' && rowSelection . ids . has ( contextRow . id ) ) {
24552451 let count = 0
24562452 for ( const row of rows ) {
2457- if ( checkedRows . has ( row . id ) ) count ++
2453+ if ( rowSelection . ids . has ( row . id ) ) count ++
24582454 }
24592455 return Math . max ( count , 1 )
24602456 }
@@ -2468,7 +2464,7 @@ export function Table({
24682464 const start = Math . max ( 0 , sel . startRow )
24692465 const end = Math . min ( rows . length - 1 , sel . endRow )
24702466 return Math . max ( end - start + 1 , 1 )
2471- } , [ contextMenu . isOpen , contextMenu . row , allRowsSelected , checkedRows , normalizedSelection , rows ] )
2467+ } , [ contextMenu . isOpen , contextMenu . row , rowSelection , normalizedSelection , rows ] )
24722468
24732469 const pendingUpdate = updateRowMutation . isPending ? updateRowMutation . variables : null
24742470
@@ -2782,7 +2778,7 @@ export function Table({
27822778 onContextMenu = { handleRowContextMenu }
27832779 onCellMouseDown = { handleCellMouseDown }
27842780 onCellMouseEnter = { handleCellMouseEnter }
2785- isRowChecked = { allRowsSelected || checkedRows . has ( row . id ) }
2781+ isRowChecked = { rowSelectionIncludes ( rowSelection , row . id ) }
27862782 onRowToggle = { handleRowToggle }
27872783 runningCount = { runningByRowId . get ( row . id ) ?? 0 }
27882784 hasWorkflowColumns = { hasWorkflowColumns }
0 commit comments