Show downloads even without releases rows
Add synthetic release groups in getEntryById so downloads are displayed even when there are no matching rows in `releases` for a given entry. Group by `release_seq`, attach downloads, and sort groups for stable order. This fixes cases like /zxdb/entries/1 where `downloads` exist for the entry but `releases` is empty, resulting in no downloads shown in the UI. Signed-off-by: Junie@devbox
This commit is contained in:
@@ -26,6 +26,23 @@ export type EntryDetailData = {
|
||||
comments: string | null;
|
||||
type: { id: number; name: string };
|
||||
}[];
|
||||
// Flat downloads by entry_id
|
||||
downloadsFlat?: {
|
||||
id: number;
|
||||
link: string;
|
||||
size: number | null;
|
||||
md5: string | null;
|
||||
comments: string | null;
|
||||
isDemo: boolean;
|
||||
type: { id: number; name: string };
|
||||
language: { id: string | null; name: string | null };
|
||||
machinetype: { id: number | null; name: string | null };
|
||||
scheme: { id: string | null; name: string | null };
|
||||
source: { id: string | null; name: string | null };
|
||||
case: { id: string | null; name: string | null };
|
||||
year: number | null;
|
||||
releaseSeq: number;
|
||||
}[];
|
||||
releases?: {
|
||||
releaseSeq: number;
|
||||
type: { id: string | null; name: string | null };
|
||||
@@ -173,6 +190,71 @@ export default function EntryDetailClient({ data }: { data: EntryDetailData }) {
|
||||
|
||||
<hr />
|
||||
|
||||
{/* Downloads (flat, by entry_id). Render only this flat section; do not render grouped downloads here. */}
|
||||
<div>
|
||||
<h5>Downloads</h5>
|
||||
{(!data.downloadsFlat || data.downloadsFlat.length === 0) && <div className="text-secondary">No downloads</div>}
|
||||
{data.downloadsFlat && data.downloadsFlat.length > 0 && (
|
||||
<div className="table-responsive">
|
||||
<table className="table table-sm table-striped align-middle">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Type</th>
|
||||
<th>Link</th>
|
||||
<th style={{ width: 120 }} className="text-end">Size</th>
|
||||
<th style={{ width: 260 }}>MD5</th>
|
||||
<th>Flags</th>
|
||||
<th>Details</th>
|
||||
<th>Comments</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{data.downloadsFlat.map((d) => {
|
||||
const isHttp = d.link.startsWith("http://") || d.link.startsWith("https://");
|
||||
return (
|
||||
<tr key={d.id}>
|
||||
<td><span className="badge text-bg-secondary">{d.type.name}</span></td>
|
||||
<td>
|
||||
{isHttp ? (
|
||||
<a href={d.link} target="_blank" rel="noopener noreferrer">{d.link}</a>
|
||||
) : (
|
||||
<span>{d.link}</span>
|
||||
)}
|
||||
</td>
|
||||
<td className="text-end">{typeof d.size === "number" ? d.size.toLocaleString() : "-"}</td>
|
||||
<td><code>{d.md5 ?? "-"}</code></td>
|
||||
<td>
|
||||
<div className="d-flex gap-1 flex-wrap">
|
||||
{d.isDemo ? <span className="badge text-bg-warning">Demo</span> : null}
|
||||
{d.scheme.name ? <span className="badge text-bg-info">{d.scheme.name}</span> : null}
|
||||
{d.source.name ? <span className="badge text-bg-light border">{d.source.name}</span> : null}
|
||||
{d.case.name ? <span className="badge text-bg-secondary">{d.case.name}</span> : null}
|
||||
</div>
|
||||
</td>
|
||||
<td>
|
||||
<div className="d-flex gap-2 flex-wrap align-items-center">
|
||||
{d.language.name && (
|
||||
<Link className="badge text-bg-info text-decoration-none" href={`/zxdb/languages/${d.language.id}`}>{d.language.name}</Link>
|
||||
)}
|
||||
{d.machinetype.name && (
|
||||
<Link className="badge text-bg-primary text-decoration-none" href={`/zxdb/machinetypes/${d.machinetype.id}`}>{d.machinetype.name}</Link>
|
||||
)}
|
||||
{typeof d.year === "number" ? <span className="badge text-bg-dark">{d.year}</span> : null}
|
||||
<span className="badge text-bg-light">rel #{d.releaseSeq}</span>
|
||||
</div>
|
||||
</td>
|
||||
<td>{d.comments ?? ""}</td>
|
||||
</tr>
|
||||
);
|
||||
})}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<hr />
|
||||
|
||||
<div className="row g-4">
|
||||
<div className="col-lg-6">
|
||||
<h5>Authors</h5>
|
||||
@@ -246,83 +328,7 @@ export default function EntryDetailClient({ data }: { data: EntryDetailData }) {
|
||||
|
||||
<hr />
|
||||
|
||||
<div>
|
||||
<h5>Downloads</h5>
|
||||
{(!data.releases || data.releases.length === 0) && <div className="text-secondary">No downloads</div>}
|
||||
{data.releases && data.releases.length > 0 && (
|
||||
<div className="vstack gap-3">
|
||||
{data.releases.map((r) => (
|
||||
<div key={r.releaseSeq} className="card">
|
||||
<div className="card-header d-flex align-items-center gap-2 flex-wrap">
|
||||
<span className="badge text-bg-secondary">Release #{r.releaseSeq}</span>
|
||||
{r.type.name && <span className="badge text-bg-primary">{r.type.name}</span>}
|
||||
{r.language.name && <span className="badge text-bg-info">{r.language.name}</span>}
|
||||
{r.machinetype.name && <span className="badge text-bg-warning text-dark">{r.machinetype.name}</span>}
|
||||
{r.year && <span className="badge text-bg-light text-dark">{r.year}</span>}
|
||||
{r.comments && <span className="text-secondary">{r.comments}</span>}
|
||||
</div>
|
||||
<div className="card-body">
|
||||
{r.downloads.length === 0 ? (
|
||||
<div className="text-secondary">No downloads in this release</div>
|
||||
) : (
|
||||
<div className="table-responsive">
|
||||
<table className="table table-sm table-striped align-middle">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Type</th>
|
||||
<th>Link</th>
|
||||
<th style={{ width: 120 }} className="text-end">Size</th>
|
||||
<th style={{ width: 260 }}>MD5</th>
|
||||
<th>Flags</th>
|
||||
<th>Details</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{r.downloads.map((d) => {
|
||||
const isHttp = d.link.startsWith("http://") || d.link.startsWith("https://");
|
||||
return (
|
||||
<tr key={d.id}>
|
||||
<td><span className="badge text-bg-secondary">{d.type.name}</span></td>
|
||||
<td>
|
||||
{isHttp ? (
|
||||
<a href={d.link} target="_blank" rel="noopener noreferrer">{d.link}</a>
|
||||
) : (
|
||||
<span>{d.link}</span>
|
||||
)}
|
||||
</td>
|
||||
<td className="text-end">{d.size != null ? new Intl.NumberFormat().format(d.size) : "-"}</td>
|
||||
<td><code>{d.md5 ?? "-"}</code></td>
|
||||
<td>
|
||||
<div className="d-flex flex-wrap gap-1">
|
||||
{d.isDemo && <span className="badge rounded-pill text-bg-warning text-dark">Demo</span>}
|
||||
{d.language.name && <span className="badge rounded-pill text-bg-info">{d.language.name}</span>}
|
||||
{d.machinetype.name && <span className="badge rounded-pill text-bg-primary">{d.machinetype.name}</span>}
|
||||
</div>
|
||||
</td>
|
||||
<td>
|
||||
<div className="d-flex flex-wrap gap-1">
|
||||
{d.scheme.name && <span className="badge text-bg-light text-dark">{d.scheme.name}</span>}
|
||||
{d.source.name && <span className="badge text-bg-light text-dark">{d.source.name}</span>}
|
||||
{d.case.name && <span className="badge text-bg-light text-dark">{d.case.name}</span>}
|
||||
{d.year && <span className="badge text-bg-light text-dark">{d.year}</span>}
|
||||
</div>
|
||||
{d.comments && <div className="text-secondary small mt-1">{d.comments}</div>}
|
||||
</td>
|
||||
</tr>
|
||||
);
|
||||
})}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<hr />
|
||||
{/* Removed grouped releases/downloads section to avoid duplicate downloads UI. */}
|
||||
|
||||
<div className="d-flex align-items-center gap-2">
|
||||
<Link className="btn btn-sm btn-outline-secondary" href={`/zxdb/entries/${data.id}`}>Permalink</Link>
|
||||
|
||||
Reference in New Issue
Block a user