66 Show ,
77 Index ,
88} from "solid-js" ;
9- import { fetchOrgs , fetchRepos , OrgEntry , RepoRef } from "../../services/api" ;
9+ import { fetchOrgs , fetchRepos , OrgEntry , RepoRef , RepoEntry } from "../../services/api" ;
1010import { getClient } from "../../services/github" ;
11+ import { relativeTime } from "../../lib/format" ;
1112import LoadingSpinner from "../shared/LoadingSpinner" ;
1213import FilterInput from "../shared/FilterInput" ;
1314
@@ -20,7 +21,7 @@ interface RepoSelectorProps {
2021interface OrgRepoState {
2122 org : string ;
2223 type : "org" | "user" ;
23- repos : RepoRef [ ] ;
24+ repos : RepoEntry [ ] ;
2425 loading : boolean ;
2526 error : string | null ;
2627}
@@ -146,23 +147,46 @@ export default function RepoSelector(props: RepoSelectorProps) {
146147 new Set ( props . selected . map ( ( r ) => r . fullName ) )
147148 ) ;
148149
150+ const sortedOrgStates = createMemo ( ( ) => {
151+ const states = orgStates ( ) ;
152+ // Defer sorting during initial load to prevent layout shift as orgs trickle in.
153+ // After initial load (all orgs resolved), sorting stays active during retries
154+ // because loadedCount is not reset by retryOrg.
155+ if ( loadedCount ( ) < props . selectedOrgs . length ) return states ;
156+ const maxPushedAt = new Map (
157+ states . map ( ( s ) => [
158+ s . org ,
159+ s . repos . reduce ( ( max , r ) => r . pushedAt && r . pushedAt > max ? r . pushedAt : max , "" ) ,
160+ ] )
161+ ) ;
162+ return [ ...states ] . sort ( ( a , b ) => {
163+ const aMax = maxPushedAt . get ( a . org ) ?? "" ;
164+ const bMax = maxPushedAt . get ( b . org ) ?? "" ;
165+ return aMax > bMax ? - 1 : aMax < bMax ? 1 : 0 ;
166+ } ) ;
167+ } ) ;
168+
169+ function toRepoRef ( entry : RepoEntry ) : RepoRef {
170+ return { owner : entry . owner , name : entry . name , fullName : entry . fullName } ;
171+ }
172+
149173 function isSelected ( fullName : string ) {
150174 return selectedSet ( ) . has ( fullName ) ;
151175 }
152176
153- function toggleRepo ( repo : RepoRef ) {
177+ function toggleRepo ( repo : RepoEntry ) {
154178 if ( isSelected ( repo . fullName ) ) {
155179 props . onChange ( props . selected . filter ( ( r ) => r . fullName !== repo . fullName ) ) ;
156180 } else {
157- props . onChange ( [ ...props . selected , repo ] ) ;
181+ props . onChange ( [ ...props . selected , toRepoRef ( repo ) ] ) ;
158182 }
159183 }
160184
161185 // ── Filtering ──────────────────────────────────────────────────────────────
162186
163187 const q = ( ) => filter ( ) . toLowerCase ( ) . trim ( ) ;
164188
165- function filteredReposForOrg ( state : OrgRepoState ) : RepoRef [ ] {
189+ function filteredReposForOrg ( state : OrgRepoState ) : RepoEntry [ ] {
166190 const query = q ( ) ;
167191 if ( ! query ) return state . repos ;
168192 return state . repos . filter (
@@ -177,7 +201,7 @@ export default function RepoSelector(props: RepoSelectorProps) {
177201 function selectAllInOrg ( state : OrgRepoState ) {
178202 const visible = filteredReposForOrg ( state ) ;
179203 const current = new Map ( props . selected . map ( ( r ) => [ r . fullName , r ] ) ) ;
180- for ( const repo of visible ) current . set ( repo . fullName , repo ) ;
204+ for ( const repo of visible ) current . set ( repo . fullName , toRepoRef ( repo ) ) ;
181205 props . onChange ( [ ...current . values ( ) ] ) ;
182206 }
183207
@@ -186,18 +210,13 @@ export default function RepoSelector(props: RepoSelectorProps) {
186210 props . onChange ( props . selected . filter ( ( r ) => ! visible . has ( r . fullName ) ) ) ;
187211 }
188212
189- function allVisibleInOrgSelected ( state : OrgRepoState ) : boolean {
190- const visible = filteredReposForOrg ( state ) ;
191- return visible . length > 0 && visible . every ( ( r ) => isSelected ( r . fullName ) ) ;
192- }
193-
194213 // ── Global select/deselect all ────────────────────────────────────────────
195214
196215 function selectAll ( ) {
197216 const current = new Map ( props . selected . map ( ( r ) => [ r . fullName , r ] ) ) ;
198217 for ( const state of orgStates ( ) ) {
199218 for ( const repo of filteredReposForOrg ( state ) ) {
200- current . set ( repo . fullName , repo ) ;
219+ current . set ( repo . fullName , toRepoRef ( repo ) ) ;
201220 }
202221 }
203222 props . onChange ( [ ...current . values ( ) ] ) ;
@@ -252,9 +271,9 @@ export default function RepoSelector(props: RepoSelectorProps) {
252271 </ Show >
253272
254273 { /* Per-org repo lists */ }
255- < For each = { orgStates ( ) } >
274+ < For each = { sortedOrgStates ( ) } >
256275 { ( state ) => {
257- const visible = ( ) => filteredReposForOrg ( state ) ;
276+ const visible = createMemo ( ( ) => filteredReposForOrg ( state ) ) ;
258277
259278 return (
260279 < div class = "overflow-hidden rounded-lg border border-gray-200 dark:border-gray-700" >
@@ -269,8 +288,8 @@ export default function RepoSelector(props: RepoSelectorProps) {
269288 type = "button"
270289 onClick = { ( ) => selectAllInOrg ( state ) }
271290 disabled = {
272- allVisibleInOrgSelected ( state ) ||
273- visible ( ) . length === 0
291+ visible ( ) . length === 0 ||
292+ visible ( ) . every ( ( r ) => isSelected ( r . fullName ) )
274293 }
275294 class = "text-xs text-blue-600 hover:underline disabled:cursor-not-allowed disabled:opacity-40 dark:text-blue-400"
276295 >
@@ -341,9 +360,14 @@ export default function RepoSelector(props: RepoSelectorProps) {
341360 />
342361 < div class = "min-w-0 flex-1" >
343362 < div class = "flex items-center gap-2" >
344- < span class = "truncate text-sm font-medium text-gray-900 dark:text-gray-100" >
363+ < span class = "min-w-0 truncate text-sm font-medium text-gray-900 dark:text-gray-100" >
345364 { repo ( ) . name }
346365 </ span >
366+ < Show when = { repo ( ) . pushedAt } >
367+ < span class = "ml-auto shrink-0 text-xs text-gray-500 dark:text-gray-400" >
368+ { relativeTime ( repo ( ) . pushedAt ! ) }
369+ </ span >
370+ </ Show >
347371 </ div >
348372 </ div >
349373 </ label >
0 commit comments