/** * "Changes from {n} commits" (`CodeReviewDetailsCommitsComponentFactory`): the bold label, * the commit dropdown link (the selected commit's hash, else "{n} commits"; disabled with * one commit), its popup ("All commits (n)", a separator, then every commit with its * author and date, filtered by the search field) and the next / previous arrows. Below the * row, the selected commit's info block with Show / Hide details * (`CodeReviewDetailsCommitInfoComponentFactory`). */ import { useMemo, useRef, useState, type KeyboardEvent } from "react"; import clsx from "clsx"; import { HighlightedText, Icon, IconButton, List, Popup, SearchField, fuzzyMatch, listNavigate, T, type PopupAnchor, } from "@/ui"; import { useTx } from "@/platform/i18n"; import type { GHPullRequestCommit } from "../api/types"; import { GHOST_USER } from "../data/model"; import { prettyDate } from "../ui/prettyDate"; import { commitsTitle, stepCommit } from "./model"; import styles from "./Details.module.css"; /** `createCommitsPopupPresenter`: headline, body, author (the ghost user when unknown) and date. */ function presentation(c: GHPullRequestCommit) { const user = c.commit.author?.user ?? GHOST_USER; return { title: c.commit.messageHeadline, description: c.commit.messageBody, author: user.name || user.login, date: prettyDate(c.commit.committedDate), }; } export function CommitPicker({ commits, selected, onSelect, }: { commits: readonly GHPullRequestCommit[]; /** Index of the selected commit; -1 for all. */ selected: number; onSelect: (index: number) => void; }) { const tx = useTx(); const link = useRef(null); const [open, setOpen] = useState(false); const commit = commits[selected]; const arrows = commits.length > 1 && selected >= 0; const longest = Math.max(0, ...commits.map((c) => c.commit.abbreviatedOid.length)); const next = stepCommit(selected, commits.length, 1); const previous = stepCommit(selected, commits.length, -1); return (
{commitsTitle(commits.length)} {arrows && ( <> next !== null && onSelect(next)} /> previous !== null && onSelect(previous)} /> )} {open && ( setOpen(false)} /> )}
); } type Row = { kind: "all" } | { kind: "separator" } | { kind: "commit"; index: number; match: number[] | null }; function CommitsPopup(props: { anchor: PopupAnchor; commits: readonly GHPullRequestCommit[]; selected: number; onChoose: (index: number) => void; onClose: () => void; }) { const { commits, selected } = props; const tx = useTx(); const [filter, setFilter] = useState(""); const [active, setActive] = useState(0); const allText = tx`All commits (${commits.length})`; const rows = useMemo((): Row[] => { const q = filter.trim(); const out: Row[] = []; if (!q || fuzzyMatch(allText, q)) out.push({ kind: "all" }, { kind: "separator" }); commits.forEach((c, index) => { const match = q ? fuzzyMatch(c.commit.messageHeadline, q) : null; if (!q || match) out.push({ kind: "commit", index, match }); }); return out; }, [commits, filter, allText]); const isSection = (r: Row) => r.kind === "separator"; const current = Math.min(active, rows.length - 1); const choose = (r: Row | undefined) => { if (!r || r.kind === "separator") return; props.onChoose(r.kind === "all" ? -1 : r.index); props.onClose(); }; const onKeyDown = (e: KeyboardEvent) => { const n = listNavigate(e.key, { items: rows, active: current, isSection, wrap: true, pageSize: 7 }); if (n !== null && e.key !== "Home" && e.key !== "End") { e.preventDefault(); setActive(n); } else if (e.key === "Enter") { e.preventDefault(); choose(rows[current]); } }; return (
{ setFilter(v); setActive(0); }} onKeyDown={onKeyDown} placeholder="Search commits" aria-label={tx("Search commits")} data-autofocus />
(r.kind === "commit" ? `c:${r.index}` : `${r.kind}:${i}`)} isSection={isSection} renderSection={() =>
} sectionHeight={9} activeIndex={current} onActiveChange={setActive} onActivate={(r) => choose(r)} focusable={false} hoverSelects rowHeight={52} className={styles.commitsList} aria-label={tx("Commits")} renderItem={(r, state) => { if (r.kind !== "commit") return (
{selected < 0 && } {allText}
); const p = presentation(commits[r.index]!); return (
{selected === r.index && } {p.author}, {p.date}
); }} /> ); } /** The selected commit: its headline (one line), Show / Hide details, then its body and "author, date". */ export function CommitInfo({ commit }: { commit: GHPullRequestCommit }) { const [detailed, setDetailed] = useState(false); const p = presentation(commit); return (
{p.title}
{detailed && ( <> {p.description &&
{p.description}
}
{p.author}, {p.date}
)}
); }