@@ -17,6 +17,7 @@ import SizeBadge from "../shared/SizeBadge";
1717import RoleBadge from "../shared/RoleBadge" ;
1818import SkeletonRows from "../shared/SkeletonRows" ;
1919import ChevronIcon from "../shared/ChevronIcon" ;
20+ import { groupByRepo , computePageLayout , slicePageGroups } from "../../lib/grouping" ;
2021
2122export interface PullRequestsTabProps {
2223 pullRequests : PullRequest [ ] ;
@@ -103,57 +104,6 @@ const prFilterGroups: FilterChipGroupDef[] = [
103104 } ,
104105] ;
105106
106- interface PrRepoGroup {
107- repoFullName : string ;
108- items : PullRequest [ ] ;
109- }
110-
111- function groupByRepo ( items : PullRequest [ ] ) : PrRepoGroup [ ] {
112- const groups : PrRepoGroup [ ] = [ ] ;
113- const map = new Map < string , PrRepoGroup > ( ) ;
114- for ( const item of items ) {
115- let group = map . get ( item . repoFullName ) ;
116- if ( ! group ) {
117- group = { repoFullName : item . repoFullName , items : [ ] } ;
118- map . set ( item . repoFullName , group ) ;
119- groups . push ( group ) ;
120- }
121- group . items . push ( item ) ;
122- }
123- return groups ;
124- }
125-
126- function computePageLayout (
127- groups : PrRepoGroup [ ] ,
128- approxPageSize : number ,
129- ) : { boundaries : number [ ] ; pageCount : number } {
130- if ( groups . length === 0 ) return { boundaries : [ 0 ] , pageCount : 1 } ;
131-
132- const boundaries : number [ ] = [ 0 ] ;
133- let currentPageItems = 0 ;
134- for ( let i = 0 ; i < groups . length ; i ++ ) {
135- if ( currentPageItems > 0 && currentPageItems + groups [ i ] . items . length > approxPageSize ) {
136- boundaries . push ( i ) ;
137- currentPageItems = 0 ;
138- }
139- currentPageItems += groups [ i ] . items . length ;
140- }
141-
142- return { boundaries, pageCount : Math . max ( 1 , boundaries . length ) } ;
143- }
144-
145- function slicePageGroups (
146- groups : PrRepoGroup [ ] ,
147- boundaries : number [ ] ,
148- pageCount : number ,
149- page : number ,
150- ) : PrRepoGroup [ ] {
151- const clampedPage = Math . max ( 0 , Math . min ( page , pageCount - 1 ) ) ;
152- const start = boundaries [ clampedPage ] ;
153- const end = clampedPage + 1 < boundaries . length ? boundaries [ clampedPage + 1 ] : groups . length ;
154- return groups . slice ( start , end ) ;
155- }
156-
157107export default function PullRequestsTab ( props : PullRequestsTabProps ) {
158108 const [ page , setPage ] = createSignal ( 0 ) ;
159109 const [ collapsedRepos , setCollapsedRepos ] = createStore < Record < string , boolean > > ( { } ) ;
@@ -251,13 +201,11 @@ export default function PullRequestsTab(props: PullRequestsTabProps) {
251201 const filteredSorted = createMemo ( ( ) => filteredSortedWithMeta ( ) . items ) ;
252202 const prMeta = createMemo ( ( ) => filteredSortedWithMeta ( ) . meta ) ;
253203
254- const pageSize = createMemo ( ( ) => config . itemsPerPage ) ;
255-
256204 const repoGroups = createMemo ( ( ) => groupByRepo ( filteredSorted ( ) ) ) ;
257- const pageLayout = createMemo ( ( ) => computePageLayout ( repoGroups ( ) , pageSize ( ) ) ) ;
258- const pageCount = ( ) => pageLayout ( ) . pageCount ;
205+ const pageLayout = createMemo ( ( ) => computePageLayout ( repoGroups ( ) , config . itemsPerPage ) ) ;
206+ const pageCount = createMemo ( ( ) => pageLayout ( ) . pageCount ) ;
259207 const pageGroups = createMemo ( ( ) =>
260- slicePageGroups ( repoGroups ( ) , pageLayout ( ) . boundaries , pageLayout ( ) . pageCount , page ( ) )
208+ slicePageGroups ( repoGroups ( ) , pageLayout ( ) . boundaries , pageCount ( ) , page ( ) )
261209 ) ;
262210
263211 createEffect ( ( ) => {
@@ -330,9 +278,18 @@ export default function PullRequestsTab(props: PullRequestsTabProps) {
330278 < FilterChips
331279 groups = { prFilterGroups }
332280 values = { viewState . tabFilters . pullRequests }
333- onChange = { ( field , value ) => { setTabFilter ( "pullRequests" , field as PullRequestFilterField , value ) ; setPage ( 0 ) ; } }
334- onReset = { ( field ) => { resetTabFilter ( "pullRequests" , field as PullRequestFilterField ) ; setPage ( 0 ) ; } }
335- onResetAll = { ( ) => { resetAllTabFilters ( "pullRequests" ) ; setPage ( 0 ) ; } }
281+ onChange = { ( field , value ) => {
282+ setTabFilter ( "pullRequests" , field as PullRequestFilterField , value ) ;
283+ setPage ( 0 ) ;
284+ } }
285+ onReset = { ( field ) => {
286+ resetTabFilter ( "pullRequests" , field as PullRequestFilterField ) ;
287+ setPage ( 0 ) ;
288+ } }
289+ onResetAll = { ( ) => {
290+ resetAllTabFilters ( "pullRequests" ) ;
291+ setPage ( 0 ) ;
292+ } }
336293 />
337294 </ div >
338295
@@ -370,60 +327,63 @@ export default function PullRequestsTab(props: PullRequestsTabProps) {
370327 >
371328 < div class = "divide-y divide-gray-100 dark:divide-gray-800" >
372329 < For each = { pageGroups ( ) } >
373- { ( repoGroup ) => (
374- < div class = "bg-white dark:bg-gray-900" >
375- < button
376- onClick = { ( ) => toggleRepo ( repoGroup . repoFullName ) }
377- aria-expanded = { ! collapsedRepos [ repoGroup . repoFullName ] }
378- class = "w-full flex items-center gap-2 px-4 py-2 text-left text-sm font-semibold text-gray-700 dark:text-gray-200 bg-gray-50 dark:bg-gray-800 hover:bg-gray-100 dark:hover:bg-gray-700 transition-colors"
379- >
380- < ChevronIcon size = "md" rotated = { collapsedRepos [ repoGroup . repoFullName ] } />
381- { repoGroup . repoFullName }
382- </ button >
383- < Show when = { ! collapsedRepos [ repoGroup . repoFullName ] } >
384- < div role = "list" class = "divide-y divide-gray-200 dark:divide-gray-700" >
385- < For each = { repoGroup . items } >
386- { ( pr ) => (
387- < div role = "listitem" >
388- < ItemRow
389- hideRepo = { true }
390- repo = { pr . repoFullName }
391- number = { pr . number }
392- title = { pr . title }
393- author = { pr . userLogin }
394- createdAt = { pr . createdAt }
395- url = { pr . htmlUrl }
396- labels = { pr . labels }
397- commentCount = { pr . comments + pr . reviewComments }
398- onIgnore = { ( ) => handleIgnore ( pr ) }
399- density = { config . viewDensity }
400- >
401- < div class = "flex items-center gap-2 flex-wrap" >
402- < RoleBadge roles = { prMeta ( ) . get ( pr . id ) ?. roles ?? [ ] } />
403- < ReviewBadge decision = { pr . reviewDecision } />
404- < SizeBadge additions = { pr . additions } deletions = { pr . deletions } changedFiles = { pr . changedFiles } category = { prMeta ( ) . get ( pr . id ) ?. sizeCategory } />
405- < StatusDot status = { pr . checkStatus } />
406- < Show when = { pr . draft } >
407- < span class = "inline-flex items-center rounded-full bg-gray-100 dark:bg-gray-700 text-gray-600 dark:text-gray-300 text-xs px-2 py-0.5 font-medium" >
408- Draft
409- </ span >
410- </ Show >
411- < Show when = { pr . reviewerLogins . length > 0 } >
412- < span class = "text-xs text-gray-500 dark:text-gray-400" title = { pr . reviewerLogins . join ( ", " ) } >
413- Reviewers: { pr . reviewerLogins . slice ( 0 , 5 ) . join ( ", " ) }
414- { pr . reviewerLogins . length > 5 && ` +${ pr . reviewerLogins . length - 5 } more` }
415- { pr . totalReviewCount > pr . reviewerLogins . length && ` (${ pr . totalReviewCount } total)` }
416- </ span >
417- </ Show >
418- </ div >
419- </ ItemRow >
420- </ div >
421- ) }
422- </ For >
423- </ div >
424- </ Show >
425- </ div >
426- ) }
330+ { ( repoGroup ) => {
331+ const isRepoCollapsed = ( ) => collapsedRepos [ repoGroup . repoFullName ] ;
332+ return (
333+ < div class = "bg-white dark:bg-gray-900" >
334+ < button
335+ onClick = { ( ) => toggleRepo ( repoGroup . repoFullName ) }
336+ aria-expanded = { ! isRepoCollapsed ( ) }
337+ class = "w-full flex items-center gap-2 px-4 py-2 text-left text-sm font-semibold text-gray-700 dark:text-gray-200 bg-gray-50 dark:bg-gray-800 hover:bg-gray-100 dark:hover:bg-gray-700 transition-colors"
338+ >
339+ < ChevronIcon size = "md" rotated = { isRepoCollapsed ( ) } />
340+ { repoGroup . repoFullName }
341+ </ button >
342+ < Show when = { ! isRepoCollapsed ( ) } >
343+ < div role = "list" class = "divide-y divide-gray-200 dark:divide-gray-700" >
344+ < For each = { repoGroup . items } >
345+ { ( pr ) => (
346+ < div role = "listitem" >
347+ < ItemRow
348+ hideRepo = { true }
349+ repo = { pr . repoFullName }
350+ number = { pr . number }
351+ title = { pr . title }
352+ author = { pr . userLogin }
353+ createdAt = { pr . createdAt }
354+ url = { pr . htmlUrl }
355+ labels = { pr . labels }
356+ commentCount = { pr . comments + pr . reviewComments }
357+ onIgnore = { ( ) => handleIgnore ( pr ) }
358+ density = { config . viewDensity }
359+ >
360+ < div class = "flex items-center gap-2 flex-wrap" >
361+ < RoleBadge roles = { prMeta ( ) . get ( pr . id ) ?. roles ?? [ ] } />
362+ < ReviewBadge decision = { pr . reviewDecision } />
363+ < SizeBadge additions = { pr . additions } deletions = { pr . deletions } changedFiles = { pr . changedFiles } category = { prMeta ( ) . get ( pr . id ) ?. sizeCategory } />
364+ < StatusDot status = { pr . checkStatus } />
365+ < Show when = { pr . draft } >
366+ < span class = "inline-flex items-center rounded-full bg-gray-100 dark:bg-gray-700 text-gray-600 dark:text-gray-300 text-xs px-2 py-0.5 font-medium" >
367+ Draft
368+ </ span >
369+ </ Show >
370+ < Show when = { pr . reviewerLogins . length > 0 } >
371+ < span class = "text-xs text-gray-500 dark:text-gray-400" title = { pr . reviewerLogins . join ( ", " ) } >
372+ Reviewers: { pr . reviewerLogins . slice ( 0 , 5 ) . join ( ", " ) }
373+ { pr . reviewerLogins . length > 5 && ` +${ pr . reviewerLogins . length - 5 } more` }
374+ { pr . totalReviewCount > pr . reviewerLogins . length && ` (${ pr . totalReviewCount } total)` }
375+ </ span >
376+ </ Show >
377+ </ div >
378+ </ ItemRow >
379+ </ div >
380+ ) }
381+ </ For >
382+ </ div >
383+ </ Show >
384+ </ div >
385+ ) ;
386+ } }
427387 </ For >
428388 </ div >
429389 </ Show >
0 commit comments