@@ -17,7 +17,7 @@ import { isValidRepoName } from "../../utils/aliasQuery";
1717import { formatBytes , formatNumber , formatRelativeTime } from "../../utils/format" ;
1818import { clampPage } from "../../utils/pagination" ;
1919import { Pagination } from "../common/Pagination" ;
20- import { CloseIcon , ForkIcon , IssueIcon , StarIcon } from "../common/Icons" ;
20+ import { CloseIcon , ForkIcon , IssueIcon , RefreshIcon , StarIcon } from "../common/Icons" ;
2121
2222interface RepositoryDetailsModalProps {
2323 repo : GhRepo ;
@@ -31,6 +31,8 @@ interface RepositoryDetailsModalProps {
3131
3232export type DetailTab = "overview" | "actions" | "pull-requests" | "issues" | "releases" | "forks" | "traffic" | "mentions" | "dependents" ;
3333
34+ type ReleaseFilter = "all" | "stable" | "prerelease" | "draft" ;
35+
3436const MODAL_PAGE_SIZE = 10 ;
3537
3638function historyDelta ( repo : GhRepo , field : "stars" | "forks" ) {
@@ -101,6 +103,7 @@ export function RepositoryDetailsModal({ repo, issues, pullRequests, activeTab,
101103 const [ repoPrsPage , setRepoPrsPage ] = useState ( 1 ) ;
102104 const [ repoPrsPageSize , setRepoPrsPageSize ] = useState ( MODAL_PAGE_SIZE ) ;
103105 const [ releasesPage , setReleasesPage ] = useState ( 1 ) ;
106+ const [ releaseFilter , setReleaseFilter ] = useState < ReleaseFilter > ( "all" ) ;
104107 const [ openReleaseIds , setOpenReleaseIds ] = useState < number [ ] > ( [ ] ) ;
105108 const [ forks , setForks ] = useState < ForkNode [ ] > ( [ ] ) ;
106109 const [ forksTotal , setForksTotal ] = useState ( 0 ) ;
@@ -116,7 +119,7 @@ export function RepositoryDetailsModal({ repo, issues, pullRequests, activeTab,
116119 const [ aliasInput , setAliasInput ] = useState ( "" ) ;
117120 const [ aliasError , setAliasError ] = useState ( "" ) ;
118121 const [ aliasBusy , setAliasBusy ] = useState ( false ) ;
119- const [ mentionsRefreshKey , setMentionsRefreshKey ] = useState ( 0 ) ;
122+ const [ refreshKey , setRefreshKey ] = useState ( 0 ) ;
120123
121124 useEffect ( ( ) => {
122125 let cancelled = false ;
@@ -151,7 +154,7 @@ export function RepositoryDetailsModal({ repo, issues, pullRequests, activeTab,
151154 return ( ) => {
152155 cancelled = true ;
153156 } ;
154- } , [ repo . nameWithOwner , mentionsRefreshKey ] ) ;
157+ } , [ repo . nameWithOwner , refreshKey ] ) ;
155158
156159 useEffect ( ( ) => {
157160 setContributorsPage ( 1 ) ;
@@ -161,6 +164,7 @@ export function RepositoryDetailsModal({ repo, issues, pullRequests, activeTab,
161164 setRepoIssuesPage ( 1 ) ;
162165 setRepoPrsPage ( 1 ) ;
163166 setReleasesPage ( 1 ) ;
167+ setReleaseFilter ( "all" ) ;
164168 setForksPage ( 1 ) ;
165169 setOpenReleaseIds ( [ ] ) ;
166170 } , [ repo . nameWithOwner ] ) ;
@@ -186,7 +190,7 @@ export function RepositoryDetailsModal({ repo, issues, pullRequests, activeTab,
186190 return ( ) => {
187191 cancelled = true ;
188192 } ;
189- } , [ activeTab , forkDirection , forkField , repo . nameWithOwner ] ) ;
193+ } , [ activeTab , forkDirection , forkField , repo . nameWithOwner , refreshKey ] ) ;
190194
191195 function toggleRelease ( releaseId : number ) {
192196 setOpenReleaseIds ( ( current ) =>
@@ -215,7 +219,7 @@ export function RepositoryDetailsModal({ repo, issues, pullRequests, activeTab,
215219 const result = await addRepoAlias ( repo . nameWithOwner , value ) ;
216220 setAliases ( result . aliases ) ;
217221 setAliasInput ( "" ) ;
218- setMentionsRefreshKey ( ( value ) => value + 1 ) ;
222+ setRefreshKey ( ( value ) => value + 1 ) ;
219223 } catch ( err ) {
220224 setAliasError ( ( err as Error ) . message ) ;
221225 } finally {
@@ -229,7 +233,7 @@ export function RepositoryDetailsModal({ repo, issues, pullRequests, activeTab,
229233 try {
230234 const result = await removeRepoAlias ( repo . nameWithOwner , alias ) ;
231235 setAliases ( result . aliases ) ;
232- setMentionsRefreshKey ( ( value ) => value + 1 ) ;
236+ setRefreshKey ( ( value ) => value + 1 ) ;
233237 } catch ( err ) {
234238 setAliasError ( ( err as Error ) . message ) ;
235239 } finally {
@@ -252,6 +256,22 @@ export function RepositoryDetailsModal({ repo, issues, pullRequests, activeTab,
252256 const workflows = details ?. workflows ?? [ ] ;
253257 const security = details ?. security ?? null ;
254258 const totalReleaseDownloads = releases . reduce ( ( sum , release ) => sum + release . totalDownloads , 0 ) ;
259+ const stableReleases = releases . filter ( ( release ) => ! release . prerelease && ! release . draft ) ;
260+ const preReleases = releases . filter ( ( release ) => release . prerelease && ! release . draft ) ;
261+ const draftReleases = releases . filter ( ( release ) => release . draft ) ;
262+ const filteredReleases = releaseFilter === "stable"
263+ ? stableReleases
264+ : releaseFilter === "prerelease"
265+ ? preReleases
266+ : releaseFilter === "draft"
267+ ? draftReleases
268+ : releases ;
269+ const releaseFilters : { key : ReleaseFilter ; label : string ; count : number } [ ] = [
270+ { key : "all" , label : "All" , count : releases . length } ,
271+ { key : "stable" , label : "Releases" , count : stableReleases . length } ,
272+ { key : "prerelease" , label : "Pre-releases" , count : preReleases . length } ,
273+ { key : "draft" , label : "Drafts" , count : draftReleases . length } ,
274+ ] ;
255275 const repoDigest = details ?. digest ?? null ;
256276 const releasesPageSize = 10 ;
257277 const referrers = trafficDetails ?. referrers ?? [ ] ;
@@ -267,15 +287,15 @@ export function RepositoryDetailsModal({ repo, issues, pullRequests, activeTab,
267287 const safeDependentsPage = clampPage ( dependentsPage , dependents . length , dependentsPageSize ) ;
268288 const safeRepoIssuesPage = clampPage ( repoIssuesPage , repoIssues . length , repoIssuesPageSize ) ;
269289 const safeRepoPrsPage = clampPage ( repoPrsPage , repoPullRequests . length , repoPrsPageSize ) ;
270- const safeReleasesPage = clampPage ( releasesPage , releases . length , releasesPageSize ) ;
290+ const safeReleasesPage = clampPage ( releasesPage , filteredReleases . length , releasesPageSize ) ;
271291 const safeForksPage = clampPage ( forksPage , forks . length , forksPageSize ) ;
272292 const pagedContributors = contributors . slice ( ( safeContributorsPage - 1 ) * MODAL_PAGE_SIZE , safeContributorsPage * MODAL_PAGE_SIZE ) ;
273293 const pagedMentionItems = mentionItems . slice ( ( safeMentionsPage - 1 ) * mentionsPageSize , safeMentionsPage * mentionsPageSize ) ;
274294 const pagedWorkflows = workflows . slice ( ( safeActionsPage - 1 ) * actionsPageSize , safeActionsPage * actionsPageSize ) ;
275295 const pagedDependents = dependents . slice ( ( safeDependentsPage - 1 ) * dependentsPageSize , safeDependentsPage * dependentsPageSize ) ;
276296 const pagedRepoIssues = repoIssues . slice ( ( safeRepoIssuesPage - 1 ) * repoIssuesPageSize , safeRepoIssuesPage * repoIssuesPageSize ) ;
277297 const pagedRepoPrs = repoPullRequests . slice ( ( safeRepoPrsPage - 1 ) * repoPrsPageSize , safeRepoPrsPage * repoPrsPageSize ) ;
278- const pagedReleases = releases . slice ( ( safeReleasesPage - 1 ) * releasesPageSize , safeReleasesPage * releasesPageSize ) ;
298+ const pagedReleases = filteredReleases . slice ( ( safeReleasesPage - 1 ) * releasesPageSize , safeReleasesPage * releasesPageSize ) ;
279299 const pagedForks = forks . slice ( ( safeForksPage - 1 ) * forksPageSize , safeForksPage * forksPageSize ) ;
280300 const starHistory = ( repo . history || [ ] ) . map ( ( entry ) => entry . stars ) ;
281301 const forkHistory = ( repo . history || [ ] ) . map ( ( entry ) => entry . forks ) ;
@@ -305,7 +325,16 @@ export function RepositoryDetailsModal({ repo, issues, pullRequests, activeTab,
305325 < h3 > { repo . nameWithOwner } </ h3 >
306326 </ div >
307327 </ div >
308- < button className = "modal-close" aria-label = "Close" onClick = { onClose } > < CloseIcon /> </ button >
328+ < div className = "modal-head-actions" >
329+ < button
330+ className = { `modal-close modal-refresh ${ loading ? "refreshing" : "" } ` }
331+ aria-label = "Reload repository data"
332+ title = "Reload repository data"
333+ disabled = { loading }
334+ onClick = { ( ) => setRefreshKey ( ( value ) => value + 1 ) }
335+ > < RefreshIcon /> </ button >
336+ < button className = "modal-close" aria-label = "Close" onClick = { onClose } > < CloseIcon /> </ button >
337+ </ div >
309338 </ header >
310339
311340 < div className = "modal-body repo-detail-body" >
@@ -671,11 +700,29 @@ export function RepositoryDetailsModal({ repo, issues, pullRequests, activeTab,
671700 </ div >
672701 < div className = "repo-detail-traffic-card" >
673702 < span className = "repo-detail-traffic-label" > Latest release</ span >
674- < strong > { releases [ 0 ] ?. tag_name || "n/a" } </ strong >
675- < em > { releases [ 0 ] ? formatReleaseDate ( releases [ 0 ] . published_at ) : "No release published." } </ em >
703+ < strong > { ( stableReleases [ 0 ] ?? releases [ 0 ] ) ?. tag_name || "n/a" } </ strong >
704+ < em > { ( stableReleases [ 0 ] ?? releases [ 0 ] ) ? formatReleaseDate ( ( stableReleases [ 0 ] ?? releases [ 0 ] ) . published_at ) : "No release published." } </ em >
676705 </ div >
677706 </ div >
678707 { releases . length ? (
708+ < div className = "repo-detail-release-filters" role = "group" aria-label = "Filter releases by type" >
709+ { releaseFilters . filter ( ( item ) => item . key === "all" || item . count > 0 ) . map ( ( item ) => (
710+ < button
711+ type = "button"
712+ key = { item . key }
713+ className = { releaseFilter === item . key ? "active" : "" }
714+ onClick = { ( ) => {
715+ setReleaseFilter ( item . key ) ;
716+ setReleasesPage ( 1 ) ;
717+ } }
718+ >
719+ < span > { item . label } </ span >
720+ < strong > { formatNumber ( item . count ) } </ strong >
721+ </ button >
722+ ) ) }
723+ </ div >
724+ ) : null }
725+ { filteredReleases . length ? (
679726 < div className = "repo-detail-releases" >
680727 { pagedReleases . map ( ( release ) => (
681728 < div className = "repo-detail-release" key = { release . id } >
@@ -686,7 +733,10 @@ export function RepositoryDetailsModal({ repo, issues, pullRequests, activeTab,
686733 aria-expanded = { openReleaseIds . includes ( release . id ) }
687734 >
688735 < div >
689- < strong > { release . name || release . tag_name } </ strong >
736+ < span className = "repo-detail-release-title" >
737+ < strong > { release . name || release . tag_name } </ strong >
738+ { release . draft ? < span className = "rb draft" > Draft</ span > : release . prerelease ? < span className = "rb prerelease" > Pre-release</ span > : null }
739+ </ span >
690740 < span > { release . tag_name } · { formatReleaseDate ( release . published_at ) } </ span >
691741 </ div >
692742 < span > { formatNumber ( release . totalDownloads ) } downloads</ span >
@@ -720,9 +770,10 @@ export function RepositoryDetailsModal({ repo, issues, pullRequests, activeTab,
720770 </ div >
721771 ) : null }
722772 { ! loading && ! releases . length ? < div className = "modal-empty sub" > No releases available.</ div > : null }
723- { releases . length ? (
773+ { ! loading && releases . length && ! filteredReleases . length ? < div className = "modal-empty sub" > No releases in this category.</ div > : null }
774+ { filteredReleases . length ? (
724775 < Pagination
725- totalItems = { releases . length }
776+ totalItems = { filteredReleases . length }
726777 page = { safeReleasesPage }
727778 pageSize = { releasesPageSize }
728779 onPageChange = { setReleasesPage }
0 commit comments