Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 7 additions & 1 deletion apps/web/src/components/pullRequest/PullRequestMarkdown.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,13 @@ export function PullRequestMarkdown({
[repositoryUrl],
);
return (
<div className={cn("space-y-3", className)} data-image-gallery>
<div
className={cn(
"space-y-3 [&_[data-markdown-details]]:border-0 [&_[data-markdown-details-summary]]:text-foreground/80 [&_[data-markdown-details-summary]>svg]:text-muted-foreground/60",
className,
)}
data-image-gallery
>
{segments.map((segment) => {
if (segment.kind === "markdown") {
return (
Expand Down
188 changes: 94 additions & 94 deletions apps/web/src/components/pullRequest/PullRequestSummaryTab.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -12,14 +12,13 @@ import {
ChevronRightIcon,
GitPullRequestClosedIcon,
HammerIcon,
MessageSquareIcon,
PencilIcon,
RotateCcwIcon,
SendIcon,
TagIcon,
UsersIcon,
} from "lucide-react";
import { useRef, useState, type ReactNode } from "react";
import { useId, useRef, useState, type ReactNode } from "react";

import { useAtomCommand } from "~/state/use-atom-command";
import { pullRequestEnvironment } from "~/state/pullRequests";
Expand Down Expand Up @@ -187,7 +186,7 @@ function CollapsedComment({
<ChevronDownIcon
aria-hidden
className={cn(
"size-3.5 shrink-0 text-muted-foreground transition-transform",
"size-3.5 shrink-0 text-muted-foreground/60 transition-transform",
open && "rotate-180",
)}
/>
Expand Down Expand Up @@ -229,8 +228,8 @@ function MetaRow({
children: ReactNode;
}) {
return (
<div className="grid min-h-8 grid-cols-[6rem_minmax(0,1fr)] items-center gap-2 py-1.5 text-xs">
<span className="flex min-w-0 items-center gap-1.5 text-muted-foreground">
<div className="grid min-w-0 grid-cols-[6rem_minmax(0,1fr)] items-center gap-2 text-xs">
<span className="flex items-center gap-1.5 text-muted-foreground">
{icon}
{label}
</span>
Expand All @@ -241,16 +240,13 @@ function MetaRow({

function Section({
title,
count,
defaultOpen = true,
actions,
children,
}: {
title: string;
count?: number;
defaultOpen?: boolean;
/** Controls riding on the heading row itself. A sibling of the trigger, not a child of it —
a button cannot hold a button — and only while open, since they act on what is shown. */
/** Heading controls stay separate from the collapse trigger so they remain independently usable. */
actions?: ReactNode;
children: ReactNode;
}) {
Expand Down Expand Up @@ -286,24 +282,19 @@ function Section({
where it started. Opaque, because the rows it covers scroll beneath it. */}
<div
ref={headingRef}
className="sticky top-0 z-10 flex w-full items-center border-t border-border/60 bg-background pr-4"
className="sticky top-0 z-10 flex w-full items-center bg-background pr-4"
>
{/* Title first, chevron riding to its right, count last: the row reads as a heading
with an affordance rather than a tree node. */}
<CollapsibleTrigger className="flex min-w-0 flex-1 items-center gap-1.5 px-4 py-3 text-left text-sm font-medium">
<CollapsibleTrigger className="flex min-w-0 flex-1 items-center gap-1.5 px-4 py-3 text-left text-xs font-medium text-muted-foreground hover:text-foreground">
<span>{title}</span>
<ChevronRightIcon
aria-hidden
className={cn(
"size-3.5 text-muted-foreground transition-transform",
"size-3.5 text-muted-foreground/60 transition-transform",
open && "rotate-90",
)}
/>
{count === undefined ? null : (
<span className="text-xs tabular-nums text-muted-foreground">{count}</span>
)}
</CollapsibleTrigger>
{open ? actions : null}
{actions}
</div>
<CollapsiblePanel>
<div className="px-4 pb-4">{children}</div>
Expand Down Expand Up @@ -461,6 +452,9 @@ export function PullRequestSummaryTab({
// Keyed by the pull request, so opening another one starts at the end of its conversation
// rather than wherever the last one had been read back to.
const [shown, setShown] = useState({ url: detail.url, count: COMMENT_PAGE });
const checksId = useId();
const [expandedChecksUrl, setExpandedChecksUrl] = useState<string | null>(null);
const showChecks = expandedChecksUrl === detail.url;
const shownComments = shown.url === detail.url ? shown.count : COMMENT_PAGE;
// Windowed by recency regardless of display order: expanding always reaches further back in
// time, whether the newest comment currently reads first or last.
Expand Down Expand Up @@ -596,8 +590,8 @@ export function PullRequestSummaryTab({

return (
<div className="h-full overflow-y-auto" data-pull-request-summary-scroll>
<section className="px-4 py-3">
<div>
<section className="px-4 py-2.5">
<div className="space-y-2">
<MetaRow icon={<UsersIcon className="size-3.5" />} label="Reviewers">
<span className="flex min-w-0 flex-wrap items-center gap-1.5">
{reviewerEntries.length === 0 ? (
Expand Down Expand Up @@ -701,7 +695,7 @@ export function PullRequestSummaryTab({
return (
<span
key={label.name}
className="inline-flex max-w-48 items-center gap-1.5 rounded-full border border-border/70 bg-muted/40 py-0.5 pl-1.5 pr-2 text-xs"
className="inline-flex max-w-48 items-center gap-1.5 rounded-full bg-muted/40 py-0.5 pl-1.5 pr-2 text-xs"
>
<span
aria-hidden
Expand All @@ -724,19 +718,10 @@ export function PullRequestSummaryTab({
</span>
</MetaRow>
) : null}
<MetaRow icon={<MessageSquareIcon className="size-3.5" />} label="Comments">
{activityPending
? "Loading conversation…"
: activityError
? "Conversation unavailable"
: detail.commentCount === 1
? "1 comment"
: `${detail.commentCount} comments`}
</MetaRow>
</div>
</section>

<Section title="Description">
<section aria-label="Description" className="px-4 pt-2 pb-4">
Comment thread
maria-rcks marked this conversation as resolved.
<div className="group">
{bodyScope === detail.url ? (
<PullRequestMarkdownEditor
Expand Down Expand Up @@ -783,81 +768,96 @@ export function PullRequestSummaryTab({
onRefresh={onRefresh}
/>
</div>
</Section>
</section>

<Section title="Checks" count={detail.checks.length}>
<section aria-label="Checks" className="px-4 py-3">
{detail.checks.length === 0 ? (
<p className="text-xs text-muted-foreground">No checks reported.</p>
) : (
<div className="space-y-0.5">
{detail.checks.map((check, index) => {
const finding = { kind: "check", check } as const;
const failing = check.status === "failure" || check.status === "cancelled";
return (
<div
// Position too: the host decides how many runs share a name, and a repeated
// key would be a rendering fault on top of whatever the list already says.
key={`${index}:${check.name}:${check.url ?? ""}`}
className="group flex items-center gap-1 rounded-md pr-1 hover:bg-accent/60"
>
<button
type="button"
disabled={!check.url}
onClick={() => check.url && openCheck(check.url)}
className={cn(
"flex min-w-0 flex-1 items-center gap-2 rounded-md px-2 py-1.5 text-left text-xs",
check.url ? undefined : "cursor-default",
)}
<div>
<div className="flex items-center gap-1 text-xs">
<span className="font-medium text-muted-foreground">Checks</span>
<Button
size="icon-xs"
variant="ghost-muted"
aria-label={showChecks ? "Hide checks" : "Show checks"}
aria-expanded={showChecks}
aria-controls={checksId}
onClick={() => setExpandedChecksUrl(showChecks ? null : detail.url)}
>
<ChevronRightIcon
aria-hidden
className={cn("size-3.5 text-muted-foreground/60", showChecks && "rotate-90")}
/>
</Button>
</div>
<div id={checksId} className={showChecks ? "mt-2" : "hidden"}>
{(showChecks ? detail.checks : []).map((check, index) => {
Comment thread
maria-rcks marked this conversation as resolved.
const finding = { kind: "check", check } as const;
const failing = check.status === "failure" || check.status === "cancelled";
return (
<div
// Position too: the host decides how many runs share a name, and a repeated
// key would be a rendering fault on top of whatever the list already says.
key={`${index}:${check.name}:${check.url ?? ""}`}
className="group flex items-center gap-2 rounded-md pr-1 hover:bg-accent/60"
>
<PullRequestCheckStatusIcon status={check.status} />
<span className="min-w-0 flex-1 truncate">{check.name}</span>
<span className="shrink-0 text-muted-foreground">
{pullRequestCheckStatusLabel(check)}
</span>
</button>
{/* Only where there is something to fix. A passing check has no failure to
reproduce, and the button would be an invitation to waste a thread. */}
{onFixFinding && failing ? (
<Button
size="xs"
variant="ghost"
className="shrink-0"
disabled={pendingFinding !== null && pendingFinding !== undefined}
onClick={() => onFixFinding(finding)}
<button
type="button"
disabled={!check.url}
onClick={() => check.url && openCheck(check.url)}
className={cn(
"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",
check.url ? undefined : "cursor-default",
)}
>
<HammerIcon className="size-3" />
{pendingFinding === pullRequestFindingKey(finding)
? "Preparing..."
: fixCheckLabel}
</Button>
) : null}
</div>
);
})}
<PullRequestCheckStatusIcon status={check.status} />
<span className="min-w-0 flex-1 wrap-anywhere">{check.name}</span>
<span className="shrink-0 text-muted-foreground">
{pullRequestCheckStatusLabel(check)}
</span>
</button>
{/* Only where there is something to fix. A passing check has no failure to
reproduce, and the button would be an invitation to waste a thread. */}
{onFixFinding && failing ? (
<Button
size="xs"
variant="ghost"
className="shrink-0"
disabled={pendingFinding !== null && pendingFinding !== undefined}
onClick={() => onFixFinding(finding)}
>
<HammerIcon className="size-3" />
{pendingFinding === pullRequestFindingKey(finding)
? "Preparing..."
: fixCheckLabel}
</Button>
) : null}
</div>
);
})}
</div>
</div>
)}
</Section>
</section>

<Section
title="Comments"
{...(activityPending || activityError ? {} : { count: detail.commentCount })}
actions={
!activityPending && !activityError && detail.comments.length > 0 ? (
<Button
size="xs"
variant="ghost"
className="h-7 shrink-0 px-2 text-[10px] text-muted-foreground"
aria-label={
commentOrder === "newest"
? "Show oldest comments first"
: "Show newest comments first"
}
onClick={() => setCommentOrder((value) => (value === "newest" ? "oldest" : "newest"))}
>
<ArrowDownUpIcon aria-hidden className="size-3" />
{commentOrder === "newest" ? "Newest first" : "Oldest first"}
</Button>
) : null
<Button
size="xs"
variant="ghost"
className="h-7 shrink-0 px-2 text-[10px] text-muted-foreground"
aria-label={
commentOrder === "newest"
? "Show oldest comments first"
: "Show newest comments first"
}
onClick={() => setCommentOrder((value) => (value === "newest" ? "oldest" : "newest"))}
>
<ArrowDownUpIcon aria-hidden className="size-3" />
{commentOrder === "newest" ? "Newest first" : "Oldest first"}
</Button>
}
>
{activityPending ? (
Expand Down
Loading