ContributionPage

tcmts.share.ContributionPage · #vqelf44o · term · runs on the server

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

used by