| ... | ... | @@ -15,7 +15,7 @@ import { |
| 15 | 15 | TagIcon, |
| 16 | 16 | UsersIcon, |
| 17 | 17 | } from "lucide-react"; |
| 18 | | import { useId, useRef, useState, type ReactNode } from "react"; |
| 18 | import { useRef, useState, type ReactNode } from "react"; |
| 19 | 19 | |
| 20 | 20 | import { useAtomCommand } from "~/state/use-atom-command"; |
| 21 | 21 | import { pullRequestEnvironment } from "~/state/pullRequests"; |
| ... | ... | @@ -237,11 +237,13 @@ function MetaRow({ |
| 237 | 237 | function Section({ |
| 238 | 238 | title, |
| 239 | 239 | defaultOpen = true, |
| 240 | keepMounted = false, |
| 240 | 241 | actions, |
| 241 | 242 | children, |
| 242 | 243 | }: { |
| 243 | 244 | title: string; |
| 244 | 245 | defaultOpen?: boolean; |
| 246 | keepMounted?: boolean; |
| 245 | 247 | /** Heading controls stay separate from the collapse trigger so they remain independently usable. */ |
| 246 | 248 | actions?: ReactNode; |
| 247 | 249 | children: ReactNode; |
| ... | ... | @@ -271,6 +273,7 @@ function Section({ |
| 271 | 273 | <Collapsible |
| 272 | 274 | open={open} |
| 273 | 275 | onOpenChange={setOpenWithScrollAnchor} |
| 276 | render={<section aria-label={title} />} |
| 274 | 277 | data-pull-request-summary-section |
| 275 | 278 | > |
| 276 | 279 | {/* The heading rides the top of the scroll box the way a diff's file header does, so a |
| ... | ... | @@ -292,7 +295,7 @@ function Section({ |
| 292 | 295 | </CollapsibleTrigger> |
| 293 | 296 | {actions} |
| 294 | 297 | </div> |
| 295 | | <CollapsiblePanel> |
| 298 | <CollapsiblePanel keepMounted={keepMounted}> |
| 296 | 299 | <div className="px-4 pb-4">{children}</div> |
| 297 | 300 | </CollapsiblePanel> |
| 298 | 301 | </Collapsible> |
| ... | ... | @@ -334,9 +337,6 @@ export function PullRequestSummaryTab({ |
| 334 | 337 | // Keyed by the pull request, so opening another one starts at the end of its conversation |
| 335 | 338 | // rather than wherever the last one had been read back to. |
| 336 | 339 | const [shown, setShown] = useState({ url: detail.url, count: COMMENT_PAGE }); |
| 337 | | const checksId = useId(); |
| 338 | | const [expandedChecksUrl, setExpandedChecksUrl] = useState<string | null>(null); |
| 339 | | const showChecks = expandedChecksUrl === detail.url; |
| 340 | 340 | const shownComments = shown.url === detail.url ? shown.count : COMMENT_PAGE; |
| 341 | 341 | // Windowed by recency regardless of display order: expanding always reaches further back in |
| 342 | 342 | // time, whether the newest comment currently reads first or last. |
| ... | ... | @@ -472,7 +472,7 @@ export function PullRequestSummaryTab({ |
| 472 | 472 | |
| 473 | 473 | return ( |
| 474 | 474 | <div className="h-full overflow-y-auto" data-pull-request-summary-scroll> |
| 475 | | <section className="px-4 py-2.5"> |
| 475 | <section className="px-4 pt-2.5 pb-1"> |
| 476 | 476 | <div className="space-y-2"> |
| 477 | 477 | <MetaRow icon={<UsersIcon className="size-3.5" />} label="Reviewers"> |
| 478 | 478 | <span className="flex min-w-0 flex-wrap items-center gap-1.5"> |
| ... | ... | @@ -601,7 +601,7 @@ export function PullRequestSummaryTab({ |
| 601 | 601 | </div> |
| 602 | 602 | </section> |
| 603 | 603 | |
| 604 | | <section aria-label="Description" className="px-4 pt-2 pb-1"> |
| 604 | <Section key={`description:${detail.url}`} title="Description" keepMounted> |
| 605 | 605 | <div className="group"> |
| 606 | 606 | {bodyScope === detail.url ? ( |
| 607 | 607 | <PullRequestMarkdownEditor |
| ... | ... | @@ -640,78 +640,58 @@ export function PullRequestSummaryTab({ |
| 640 | 640 | </div> |
| 641 | 641 | )} |
| 642 | 642 | </div> |
| 643 | | </section> |
| 643 | </Section> |
| 644 | 644 | |
| 645 | | <section aria-label="Checks" className="px-4 py-3"> |
| 645 | <Section key={`checks:${detail.url}`} title="Checks" defaultOpen={false}> |
| 646 | 646 | {detail.checks.length === 0 ? ( |
| 647 | 647 | <p className="text-xs text-muted-foreground">No checks reported.</p> |
| 648 | 648 | ) : ( |
| 649 | | <div> |
| 650 | | <div className="flex items-center gap-1 text-xs"> |
| 651 | | <span className="font-medium text-muted-foreground">Checks</span> |
| 652 | | <Button |
| 653 | | size="icon-xs" |
| 654 | | variant="ghost-muted" |
| 655 | | aria-label={showChecks ? "Hide checks" : "Show checks"} |
| 656 | | aria-expanded={showChecks} |
| 657 | | aria-controls={checksId} |
| 658 | | onClick={() => setExpandedChecksUrl(showChecks ? null : detail.url)} |
| 649 | detail.checks.map((check, index) => { |
| 650 | const finding = { kind: "check", check } as const; |
| 651 | const failing = check.status === "failure" || check.status === "cancelled"; |
| 652 | return ( |
| 653 | <div |
| 654 | // Position too: the host decides how many runs share a name, and a repeated |
| 655 | // key would be a rendering fault on top of whatever the list already says. |
| 656 | key={`${index}:${check.name}:${check.url ?? ""}`} |
| 657 | className="group flex items-center gap-2 rounded-md pr-1 hover:bg-accent/60" |
| 659 | 658 | > |
| 660 | | <ChevronRightIcon |
| 661 | | aria-hidden |
| 662 | | className={cn("size-3.5 text-muted-foreground/60", showChecks && "rotate-90")} |
| 663 | | /> |
| 664 | | </Button> |
| 665 | | </div> |
| 666 | | <div id={checksId} className={showChecks ? "mt-2" : "hidden"}> |
| 667 | | {(showChecks ? detail.checks : []).map((check, index) => { |
| 668 | | const finding = { kind: "check", check } as const; |
| 669 | | const failing = check.status === "failure" || check.status === "cancelled"; |
| 670 | | return ( |
| 671 | | <div |
| 672 | | // Position too: the host decides how many runs share a name, and a repeated |
| 673 | | // key would be a rendering fault on top of whatever the list already says. |
| 674 | | key={`${index}:${check.name}:${check.url ?? ""}`} |
| 675 | | className="group flex items-center gap-2 rounded-md pr-1 hover:bg-accent/60" |
| 676 | | > |
| 677 | | <button |
| 678 | | type="button" |
| 679 | | disabled={!check.url} |
| 680 | | onClick={() => check.url && openCheck(check.url)} |
| 681 | | className={cn( |
| 682 | | "flex min-w-0 flex-1 items-start gap-2 rounded-md px-2 py-2 text-left text-xs leading-5 [&>svg]:mt-0.5", |
| 683 | | check.url ? "cursor-pointer" : "cursor-default", |
| 684 | | )} |
| 685 | | > |
| 686 | | <PullRequestCheckStatusIcon status={check.status} /> |
| 687 | | <span className="min-w-0 flex-1 wrap-anywhere">{check.name}</span> |
| 688 | | <span className="shrink-0 text-muted-foreground"> |
| 689 | | {pullRequestCheckStatusLabel(check)} |
| 690 | | </span> |
| 691 | | </button> |
| 692 | | {/* Only where there is something to fix. A passing check has no failure to |
| 659 | <button |
| 660 | type="button" |
| 661 | disabled={!check.url} |
| 662 | onClick={() => check.url && openCheck(check.url)} |
| 663 | className={cn( |
| 664 | "flex min-w-0 flex-1 items-start gap-2 rounded-md px-2 py-2 text-left text-xs leading-5 [&>svg]:mt-0.5", |
| 665 | check.url ? "cursor-pointer" : "cursor-default", |
| 666 | )} |
| 667 | > |
| 668 | <PullRequestCheckStatusIcon status={check.status} /> |
| 669 | <span className="min-w-0 flex-1 wrap-anywhere">{check.name}</span> |
| 670 | <span className="shrink-0 text-muted-foreground"> |
| 671 | {pullRequestCheckStatusLabel(check)} |
| 672 | </span> |
| 673 | </button> |
| 674 | {/* Only where there is something to fix. A passing check has no failure to |
| 693 | 675 | reproduce, and the button would be an invitation to waste a thread. */} |
| 694 | | {onFixFinding && failing ? ( |
| 695 | | <Button |
| 696 | | size="xs" |
| 697 | | variant="ghost" |
| 698 | | className="shrink-0" |
| 699 | | disabled={pendingFinding !== null && pendingFinding !== undefined} |
| 700 | | onClick={() => onFixFinding(finding)} |
| 701 | | > |
| 702 | | <HammerIcon className="size-3" /> |
| 703 | | {pendingFinding === pullRequestFindingKey(finding) |
| 704 | | ? "Preparing..." |
| 705 | | : fixCheckLabel} |
| 706 | | </Button> |
| 707 | | ) : null} |
| 708 | | </div> |
| 709 | | ); |
| 710 | | })} |
| 711 | | </div> |
| 712 | | </div> |
| 676 | {onFixFinding && failing ? ( |
| 677 | <Button |
| 678 | size="xs" |
| 679 | variant="ghost" |
| 680 | className="shrink-0" |
| 681 | disabled={pendingFinding !== null && pendingFinding !== undefined} |
| 682 | onClick={() => onFixFinding(finding)} |
| 683 | > |
| 684 | <HammerIcon className="size-3" /> |
| 685 | {pendingFinding === pullRequestFindingKey(finding) |
| 686 | ? "Preparing..." |
| 687 | : fixCheckLabel} |
| 688 | </Button> |
| 689 | ) : null} |
| 690 | </div> |
| 691 | ); |
| 692 | }) |
| 713 | 693 | )} |
| 714 | | </section> |
| 694 | </Section> |
| 715 | 695 | |
| 716 | 696 | <Section |
| 717 | 697 | title="Comments" |