1- import { useMemo , useState } from 'react'
1+ import { useMemo , useState , useRef } from 'react'
22import { useDroppable } from '@dnd-kit/core'
3+ import { useVirtualizer } from '@tanstack/react-virtual'
34import { Card } from './Card'
45import { ColumnInfoPopover } from './ColumnInfoPopover'
56import type { BugzillaBug } from '@/lib/bugzilla/types'
@@ -8,6 +9,11 @@ import type { Assignee } from '@/hooks/use-board-assignees'
89import type { QeVerifyStatus } from '@/lib/bugzilla/qe-verify'
910import { COLUMN_NAMES } from '@/types'
1011
12+ // Estimated height of each card in pixels (used for virtual scrolling)
13+ const ESTIMATED_CARD_HEIGHT = 160
14+ // Gap between cards in pixels
15+ const CARD_GAP = 12
16+
1117const NOBODY_EMAIL = 'nobody@mozilla.org'
1218
1319function calculateTotalPoints ( bugs : BugzillaBug [ ] ) : number {
@@ -126,6 +132,15 @@ export function Column({
126132 return allAssignees . filter ( ( assignee ) => assignee . email !== NOBODY_EMAIL )
127133 } , [ allAssignees , column ] )
128134
135+ // Virtual scrolling for large bug lists
136+ const scrollContainerRef = useRef < HTMLDivElement > ( null )
137+ const virtualizer = useVirtualizer ( {
138+ count : bugs . length ,
139+ getScrollElement : ( ) => scrollContainerRef . current ,
140+ estimateSize : ( ) => ESTIMATED_CARD_HEIGHT ,
141+ gap : CARD_GAP ,
142+ } )
143+
129144 // Determine column styling based on state
130145 const getColumnClassName = ( ) => {
131146 const base = 'flex min-h-[500px] w-72 flex-shrink-0 flex-col rounded-lg p-4 transition-colors'
@@ -227,34 +242,57 @@ export function Column({
227242 </ div >
228243 ) }
229244
230- { /* Bug Cards */ }
245+ { /* Bug Cards with Virtual Scrolling */ }
231246 { ! isLoading && bugs . length > 0 && (
232- < div className = "flex flex-1 flex-col gap-3 overflow-y-auto" >
233- { bugs . map ( ( bug , index ) => (
234- < Card
235- key = { bug . id }
236- bug = { bug }
237- isStaged = { stagedBugIds . has ( bug . id ) }
238- isAssigneeStaged = { stagedAssigneeBugIds ?. has ( bug . id ) }
239- stagedAssignee = { stagedAssignees ?. get ( bug . id ) }
240- isPointsStaged = { stagedPointsBugIds ?. has ( bug . id ) }
241- stagedPoints = { stagedPoints ?. get ( bug . id ) }
242- isPriorityStaged = { stagedPriorityBugIds ?. has ( bug . id ) }
243- stagedPriority = { stagedPriorities ?. get ( bug . id ) }
244- isSeverityStaged = { stagedSeverityBugIds ?. has ( bug . id ) }
245- stagedSeverity = { stagedSeverities ?. get ( bug . id ) }
246- isQeVerifyStaged = { stagedQeVerifyBugIds ?. has ( bug . id ) }
247- stagedQeVerify = { stagedQeVerifies ?. get ( bug . id ) }
248- isSelected = { selectedIndex === index }
249- isGrabbed = { selectedIndex === index && isGrabbing }
250- allAssignees = { filteredAssignees }
251- onAssigneeChange = { onAssigneeChange }
252- onPointsChange = { onPointsChange }
253- onPriorityChange = { onPriorityChange }
254- onSeverityChange = { onSeverityChange }
255- onQeVerifyChange = { onQeVerifyChange }
256- />
257- ) ) }
247+ < div ref = { scrollContainerRef } className = "flex-1 overflow-y-auto" >
248+ < div
249+ style = { {
250+ height : `${ virtualizer . getTotalSize ( ) . toString ( ) } px` ,
251+ width : '100%' ,
252+ position : 'relative' ,
253+ } }
254+ >
255+ { virtualizer . getVirtualItems ( ) . map ( ( virtualItem ) => {
256+ const bug = bugs [ virtualItem . index ]
257+ if ( ! bug ) return null
258+ const index = virtualItem . index
259+ return (
260+ < div
261+ key = { bug . id }
262+ style = { {
263+ position : 'absolute' ,
264+ top : 0 ,
265+ left : 0 ,
266+ width : '100%' ,
267+ transform : `translateY(${ virtualItem . start . toString ( ) } px)` ,
268+ } }
269+ >
270+ < Card
271+ bug = { bug }
272+ isStaged = { stagedBugIds . has ( bug . id ) }
273+ isAssigneeStaged = { stagedAssigneeBugIds ?. has ( bug . id ) }
274+ stagedAssignee = { stagedAssignees ?. get ( bug . id ) }
275+ isPointsStaged = { stagedPointsBugIds ?. has ( bug . id ) }
276+ stagedPoints = { stagedPoints ?. get ( bug . id ) }
277+ isPriorityStaged = { stagedPriorityBugIds ?. has ( bug . id ) }
278+ stagedPriority = { stagedPriorities ?. get ( bug . id ) }
279+ isSeverityStaged = { stagedSeverityBugIds ?. has ( bug . id ) }
280+ stagedSeverity = { stagedSeverities ?. get ( bug . id ) }
281+ isQeVerifyStaged = { stagedQeVerifyBugIds ?. has ( bug . id ) }
282+ stagedQeVerify = { stagedQeVerifies ?. get ( bug . id ) }
283+ isSelected = { selectedIndex === index }
284+ isGrabbed = { selectedIndex === index && isGrabbing }
285+ allAssignees = { filteredAssignees }
286+ onAssigneeChange = { onAssigneeChange }
287+ onPointsChange = { onPointsChange }
288+ onPriorityChange = { onPriorityChange }
289+ onSeverityChange = { onSeverityChange }
290+ onQeVerifyChange = { onQeVerifyChange }
291+ />
292+ </ div >
293+ )
294+ } ) }
295+ </ div >
258296 </ div >
259297 ) }
260298 </ div >
0 commit comments