ContributionPage
is an async component
A contribution's page: the conversation (its description, comments and events, the merge box), or the changes read along a review path (what's new before what uses it, then tests; what only re-hashed last, folded).
import { find, mergeable } from "#codebase/tcmts/cli/contributions"; import type { Mergeable } from "#codebase/tcmts/cli/contributions"; import { shortHash } from "#codebase/tcmts/core/hash"; import { ActButton, ago, Avatar, BADGES, ChangeCard, codeHref, CommentView, contributionChanges, contributionHref, DiffMode, Fold, Frame, Html, isEvent, leafOf, markdownHtml, MergeBox, NavTabs, Ripple, Say, shareNow, StateBadge, } from "#codebase/tcmts/share"; import { Link } from "react-aria-components"; export async function ContributionPage({ id, tab }: { id: number; tab: string }) { const s = shareNow(); if (!s.signedIn) return <Frame tab="contributions" title="Sign in" children={null} />; if (!s.cb.store.contribution(id)) return ( <Frame tab="contributions" title="Not found"> <div className="empty">No contribution #{id}.</div> </Frame> ); const c = find(s.cb, id); const ch = contributionChanges(s.cb, c); let m: Mergeable | null = null; if (c.status === "open") try { m = mergeable(s.cb, c); } catch {} const comments = s.cb.store.comments(id); const talk = comments.filter((k) => !isEvent(k)); const items = ch?.items ?? []; const changes = tab === "changes"; const tests = items.filter((i) => i.test && i.change !== "removed").length + (ch?.propagated.filter((p) => p.test).length ?? 0); const docs = items.filter((i) => /(^|\.)docs\./.test(i.path) || i.path.endsWith("README")); const onDefs = comments.filter((k) => k.about && items.some((i) => i.path === k.about)); const general = comments.filter((k) => !onDefs.includes(k)); return ( <Frame tab="contributions" title={`${c.title} #${c.id}`}> <div className="narrow" style={{ paddingBottom: 0 }}> <h2 className="title"> {c.title} <span className="muted" style={{ fontWeight: 400 }}>#{c.id}</span> </h2> <div className="filters" style={{ margin: "8px 0 12px" }}> <StateBadge c={c} /> <span className="muted"> <Avatar name={c.author} /> <b style={{ color: "var(--fg)" }}>{c.author}</b> {c.status === "merged" ? "proposed" : "wants to merge"} <code>{c.source}</code> into{" "} <code>{c.target}</code> {c.author.startsWith("@") ? " · signed by their token" : ""} · opened {ago(c.created)} </span> </div> <div className="subtabs"> <NavTabs label="Contribution" selected={changes ? "changes" : "conversation"} tabs={[ { id: "conversation", label: "Conversation", href: contributionHref(id), count: talk.length }, { id: "changes", label: "Changes", href: `${contributionHref(id)}?tab=changes`, count: items.length }, ]} /> </div> </div> {changes ? ( <div className="page cols-path"> <aside className="side path-side"> <h4>Review path</h4> <div className="muted" style={{ fontSize: 12, marginBottom: 8 }}> What's new first, then what uses it </div> <div className="steps"> {items.map((i, k) => ( <a className="step" key={i.path} href={`#d-${i.path}`}> <span className="n">{k + 1}</span> <span className="name" title={i.path}> {leafOf(i.path)} </span> <span className={`badge ${BADGES[i.change][0]}`}>{i.test ? "test" : i.change}</span> </a> ))} {ch && ch.propagated.length > 0 && ( <a className="step skip" href="#rehashed"> <span className="n">·</span> <span>{ch.propagated.length} re-hashed</span> <span className="badge b-prop">no code</span> </a> )} </div> <div style={{ marginTop: 18 }}> <h4>Checks</h4> <div className="sect"> <div> {m ? ( m.kind === "conflicts" ? ( <> <span className="bad">✕</span> conflicts in {m.names.join(", ")} </> ) : ( <> <span className="ok">✓</span> merges {m.kind === "fast-forward" ? "as a fast-forward" : m.kind === "up-to-date" ? "(nothing to merge)" : "cleanly"} </> ) ) : ( <> <span className="muted">—</span> {c.status} </> )} </div> <div> <span className="ok">✓</span> every definition typechecked when it was pushed </div> <div>{tests ? <><span className="warn">○</span> {tests} tests reach it</> : <><span className="muted">—</span> no tests reach it</>}</div> <div> {docs.length ? ( <> <span className="ok">✓</span> docs changed with it: {docs.map((d) => leafOf(d.path)).join(", ")} </> ) : ( <> <span className="muted">—</span> no docs changed </> )} </div> <div className="muted">— renames aren't changes, nor is formatting</div> </div> </div> </aside> <div> {!ch && <div className="notice">Its branches are gone, so there's nothing to show.</div>} {items.some((i) => i.lines.length) && ( <div className="filters" style={{ justifyContent: "flex-end" }}> <DiffMode /> </div> )} {ch && <Ripple ch={ch} />} {items.map((i) => ( <ChangeCard key={i.path} c={c} item={i} comments={comments.filter((k) => k.about === i.path)} /> ))} {ch && ch.propagated.length > 0 && ( <div className="box" id="rehashed"> <Fold head={ <> <span className="badge b-prop">re-hashed</span> <b>{ch.propagated.length} definitions</b> <span className="muted">their code is the same: they use what changed</span> </> } > {ch.propagated.slice(0, 100).map((p) => ( <div className="drow" key={p.path}> <span /> <a className="mono" href={codeHref(p.path)}> {p.path} </a> <span className="sig">via {p.because.join(", ")}</span> </div> ))} {ch.propagated.length > 100 && ( <div className="drow"> <span /> <span className="muted">and {ch.propagated.length - 100} more</span> </div> )} </Fold> </div> )} {!items.length && ch && <div className="empty">No changes.</div>} <MergeBox c={c} m={m} tests={tests} /> </div> </div> ) : ( <div className="page cols-2"> <div> <div className="comment"> <div className="ch"> <Avatar name={c.author} /> <b>{c.author}</b> opened this · {ago(c.created)} </div> {c.description ? <Html className="cb md" html={markdownHtml(s.cb, c.description)} /> : <div className="cb muted">No description.</div>} </div> {general.map((k) => ( <CommentView key={k.id} k={k} /> ))} {onDefs.length > 0 && ( <div className="event"> 💬 {onDefs.length} comment{onDefs.length > 1 ? "s" : ""} on definitions: <a href={`${contributionHref(id)}?tab=changes`}>see them with the changes</a> </div> )} <MergeBox c={c} m={m} tests={tests} /> {c.status !== "merged" && <Say c={c} about={items.map((i) => i.path)} />} </div> <aside className="side"> <div className="sect"> <h4>Proposed by</h4> <Avatar name={c.author} /> {c.author} {c.author.startsWith("@") && <span className="muted"> · signed by token</span>} </div> <div className="sect"> <h4>Branches</h4> <code>{c.source}</code> → <code>{c.target}</code> {ch && ( <div className="muted"> from <span className="hash">{shortHash(ch.from)}</span> to <span className="hash">{shortHash(ch.to)}</span> </div> )} </div> <div className="sect"> <h4>Size</h4> {items.filter((i) => i.change === "edited").length} edited · {items.filter((i) => i.change === "added").length} added · {items.filter((i) => i.change === "removed").length} removed <div>{ch?.propagated.length ?? 0} re-hashed (no code change)</div> </div> <div className="sect"> <h4>Review it locally</h4> <pre>tcm contribution view {c.id}</pre> </div> </aside> </div> )} {c.status === "open" && s.caller.role === "maintainer" && m && m.kind !== "conflicts" && ( <div className="dock"> <Link className="btn" href="#comment"> Comment </Link> <ActButton id={c.id} what="merge" label="Merge ✓" kind="primary" /> </div> )} </Frame> ); }
uses
- tcmts.cli.contributions.Mergeable
- tcmts.cli.contributions.find
- tcmts.cli.contributions.mergeable
- tcmts.core.hash.shortHash
- tcmts.share.ActButton
- tcmts.share.Avatar
- tcmts.share.BADGES
- tcmts.share.ChangeCard
- tcmts.share.CommentView
- tcmts.share.DiffMode
- tcmts.share.Fold
- tcmts.share.Frame
- tcmts.share.Html
- tcmts.share.MergeBox
- tcmts.share.NavTabs
- tcmts.share.Ripple
- tcmts.share.Say
- tcmts.share.StateBadge
- tcmts.share.ago
- tcmts.share.codeHref
- tcmts.share.contributionChanges
- tcmts.share.contributionHref
- tcmts.share.isEvent
- tcmts.share.leafOf
- tcmts.share.markdownHtml
- tcmts.share.shareNow