Skip to content

Commit f732dee

Browse files
committed
feat(releases): add release type filters and repository reload button
- Filter releases by category (all, stable, pre-releases, drafts) with counts - Show pre-release and draft badges on release rows - Latest release card now points to the latest stable release - Add reload button in the repository modal header to refresh all repo data
1 parent 8748d51 commit f732dee

3 files changed

Lines changed: 83 additions & 14 deletions

File tree

‎src/components/modals/RepositoryDetailsModal.tsx‎

Lines changed: 65 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@ import { isValidRepoName } from "../../utils/aliasQuery";
1717
import { formatBytes, formatNumber, formatRelativeTime } from "../../utils/format";
1818
import { clampPage } from "../../utils/pagination";
1919
import { Pagination } from "../common/Pagination";
20-
import { CloseIcon, ForkIcon, IssueIcon, StarIcon } from "../common/Icons";
20+
import { CloseIcon, ForkIcon, IssueIcon, RefreshIcon, StarIcon } from "../common/Icons";
2121

2222
interface RepositoryDetailsModalProps {
2323
repo: GhRepo;
@@ -31,6 +31,8 @@ interface RepositoryDetailsModalProps {
3131

3232
export type DetailTab = "overview" | "actions" | "pull-requests" | "issues" | "releases" | "forks" | "traffic" | "mentions" | "dependents";
3333

34+
type ReleaseFilter = "all" | "stable" | "prerelease" | "draft";
35+
3436
const MODAL_PAGE_SIZE = 10;
3537

3638
function 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}

‎src/styles/modals.css‎

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,10 @@
4949
display: inline-flex; align-items: center; justify-content: center;
5050
}
5151
.modal-close:hover { color: var(--text); background: var(--panel-3); }
52+
.modal-head-actions { display: inline-flex; align-items: center; gap: 2px; }
53+
.modal-refresh:disabled { cursor: default; }
54+
.modal-refresh.refreshing svg { animation: modalRefreshSpin 1s linear infinite; }
55+
@keyframes modalRefreshSpin { to { transform: rotate(360deg); } }
5256

5357
.modal-toolbar {
5458
display: flex; align-items: center; gap: 10px; flex-wrap: wrap;
@@ -491,6 +495,18 @@
491495
background: var(--panel-2); border: 1px solid var(--border-soft);
492496
border-radius: 999px; padding: 4px 9px; font-size: 11.5px; color: var(--text);
493497
}
498+
.repo-detail-release-filters { display: flex; align-items: center; gap: 6px; flex-wrap: wrap; margin-bottom: 12px; }
499+
.repo-detail-release-filters button {
500+
display: inline-flex; align-items: center; gap: 6px;
501+
background: var(--panel-2); border: 1px solid var(--border-soft); color: var(--muted);
502+
padding: 5px 11px; border-radius: 999px; font-size: 12px; cursor: pointer;
503+
}
504+
.repo-detail-release-filters button:hover { background: var(--panel-3); color: var(--text); }
505+
.repo-detail-release-filters button.active {
506+
background: var(--accent-soft); border-color: var(--accent-border); color: var(--text);
507+
}
508+
.repo-detail-release-filters button strong { font-size: 11px; font-weight: 700; font-variant-numeric: tabular-nums; }
509+
.repo-detail-release-title { display: flex; align-items: center; gap: 8px; min-width: 0; }
494510
.repo-detail-releases { padding: 0 0 8px; }
495511
.repo-detail-release { border-top: 1px solid var(--border-soft); }
496512
.repo-detail-release:first-child { border-top: none; }

‎src/styles/views.css‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -442,6 +442,8 @@
442442
.rb.private { color: #ffd79a; background: rgba(245,158,11,0.08); border-color: rgba(245,158,11,0.25); }
443443
.rb.archived { color: #fca5a5; background: rgba(239,68,68,0.08); border-color: rgba(239,68,68,0.25); }
444444
.rb.fork { color: #a5b4fc; background: rgba(129,140,248,0.08); border-color: rgba(129,140,248,0.25); }
445+
.rb.prerelease { color: #fdba74; background: rgba(249,115,22,0.08); border-color: rgba(249,115,22,0.25); }
446+
.rb.draft { color: #9ca3af; background: rgba(156,163,175,0.08); border-color: rgba(156,163,175,0.3); }
445447

446448
.repo-desc { font-size: 13px; color: var(--muted); line-height: 1.4; display: -webkit-box; -webkit-line-clamp: 2; -webkit-box-orient: vertical; overflow: hidden; }
447449
.repo-health-row {

0 commit comments

Comments
 (0)