authorgravatar for maria@kuuro.netmaria <maria@kuuro.net> 2026-09-02 13:29:27-04:00
committergravatar for noreply@github.comGitHub <noreply@github.com> 2026-09-02 13:29:27-04:00
log57a66608b918d673eeec7e6c94ea5906b756fcd0
tree9abc44fcb420d6384a18dc4e4883bbffa87f2d44
parent6e3bac3722d19b2e2cb736c2e789568eaf151032
signature Signed by PGP key B5690EEEBB952194

fix(pull-requests): align checkout control with author (#9196)

Co-authored-by: maria-rcks <254055478+maria-rcks@users.noreply.github.com>

1 files changed, 93 insertions(+), 75 deletions(-)

apps/web/src/components/pullRequest/PullRequestDetailPanel.tsx+93-75
......@@ -16,7 +16,6 @@ import {
1616 BookOpenIcon,
1717 CircleDotIcon,
1818 ChevronDownIcon,
19 CopyIcon,
2019 ExternalLinkIcon,
2120 FileDiffIcon,
2221 FolderGit2Icon,
......@@ -284,6 +283,68 @@ const openNumberContextMenu = (
284283 });
285284};
286285
286function PullRequestCopyableCode({
287 value,
288 target,
289 copyLabel,
290 copiedLabel,
291 className,
292 tooltipSide = "top",
293 onError,
294}: {
295 readonly value: string;
296 readonly target: string;
297 readonly copyLabel: string;
298 readonly copiedLabel: string;
299 readonly className?: string;
300 readonly tooltipSide?: "top" | "bottom";
301 readonly onError?: (error: Error) => void;
302}) {
303 const { copyToClipboard, isCopied } = useCopyToClipboard({
304 target,
305 timeout: 1600,
306 ...(onError ? { onError } : {}),
307 });
308 return (
309 <Tooltip>
310 <TooltipTrigger
311 render={
312 <button
313 type="button"
314 className={cn(
315 "relative grid w-fit min-w-0 max-w-full shrink cursor-pointer rounded px-1 py-0.5 text-left outline-none transition-colors pointer-coarse:after:absolute pointer-coarse:after:size-full pointer-coarse:after:min-h-11 pointer-coarse:after:min-w-11 hover:bg-accent/45 hover:text-foreground focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-1 focus-visible:ring-offset-background",
316 className,
317 )}
318 aria-label={isCopied ? copiedLabel : copyLabel}
319 onClick={() => copyToClipboard(value)}
320 />
321 }
322 >
323 <code
324 className={cn(
325 "col-start-1 row-start-1 min-w-0 truncate transition-opacity duration-150 motion-reduce:transition-none",
326 isCopied ? "opacity-0" : "opacity-100",
327 )}
328 >
329 {value}
330 </code>
331 <span
332 aria-hidden="true"
333 className={cn(
334 "col-start-1 row-start-1 truncate text-center transition-opacity duration-150 motion-reduce:transition-none",
335 isCopied ? "opacity-100" : "opacity-0",
336 )}
337 >
338 Copied
339 </span>
340 </TooltipTrigger>
341 <TooltipPopup className="max-w-96 wrap-anywhere font-mono" side={tooltipSide}>
342 {`${isCopied ? "Copied" : copyLabel}: ${value}`}
343 </TooltipPopup>
344 </Tooltip>
345 );
346}
347
287348/**
288349 * The stale-branch warning, said beside the branch it is about rather than as a bar of its own.
289350 * The banner this replaces held a row of chrome open across the top of every pull request that
......@@ -466,20 +527,6 @@ export function PullRequestDetailPanel({
466527 // Which handoff is preparing, keyed so a per-finding button can say "Preparing..." on itself
467528 // alone. One at a time whatever the key: they all check the same pull request out.
468529 const [handoff, setHandoff] = useState<string | null>(null);
469 const { copyToClipboard: copyBranchToClipboard, isCopied: isBranchCopied } = useCopyToClipboard({
470 target: "branch name",
471 timeout: 1600,
472 });
473 const { copyToClipboard: copyCheckoutCommandToClipboard } = useCopyToClipboard({
474 target: "pull request checkout command",
475 onCopy: () => toastManager.add({ type: "success", title: "Checkout command copied" }),
476 onError: (error) =>
477 toastManager.add({
478 type: "error",
479 title: "Could not copy checkout command",
480 description: error.message,
481 }),
482 });
483530 // The chunk is fetched as soon as the panel exists rather than waiting for the Code tab to be
484531 // clicked, so a reader who does click it lands on a chunk already in the module cache.
485532 useEffect(() => {
......@@ -1697,10 +1744,30 @@ export function PullRequestDetailPanel({
16971744 </div>
16981745 </div>
16991746 )}
1700 <PullRequestMetaLine className="mt-2 text-xs text-muted-foreground">
1701 <PullRequestActorLabel actor={detail.author} className="font-medium" />
1702 <span>updated {formatRelativeTimeLabel(detail.updatedAt)}</span>
1703 </PullRequestMetaLine>
1747 <div className="mt-2 flex min-w-0 items-center gap-2 text-xs text-muted-foreground">
1748 <PullRequestMetaLine className="min-w-0 whitespace-nowrap">
1749 <PullRequestActorLabel actor={detail.author} className="font-medium" />
1750 <span>updated {formatRelativeTimeLabel(detail.updatedAt)}</span>
1751 </PullRequestMetaLine>
1752 {checkoutCommand ? (
1753 <PullRequestCopyableCode
1754 key={checkoutCommand}
1755 value={checkoutCommand}
1756 target="pull request checkout command"
1757 copyLabel="Copy checkout command"
1758 copiedLabel="Checkout command copied"
1759 className="ml-auto font-mono"
1760 tooltipSide="bottom"
1761 onError={(error) =>
1762 toastManager.add({
1763 type: "error",
1764 title: "Could not copy checkout command",
1765 description: error.message,
1766 })
1767 }
1768 />
1769 ) : null}
1770 </div>
17041771
17051772 <div className="mt-4 flex min-w-0 items-center gap-2 text-xs text-muted-foreground">
17061773 <span className="flex min-w-0 flex-1 items-center gap-1.5 font-mono text-xs text-muted-foreground/70">
......@@ -1736,62 +1803,13 @@ export function PullRequestDetailPanel({
17361803 aria-label="receives changes from"
17371804 className="size-3.5 shrink-0 opacity-60"
17381805 />
1739 <Tooltip>
1740 <TooltipTrigger
1741 render={
1742 <button
1743 type="button"
1744 className="grid w-fit min-w-0 max-w-full shrink cursor-pointer rounded px-1 py-0.5 text-left outline-none transition-colors hover:bg-accent/45 hover:text-foreground focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-1 focus-visible:ring-offset-background"
1745 aria-label={
1746 isBranchCopied ? "Branch name copied" : "Copy pull request branch"
1747 }
1748 onClick={() => copyBranchToClipboard(detail.headBranch)}
1749 />
1750 }
1751 >
1752 <code
1753 className={cn(
1754 "col-start-1 row-start-1 min-w-0 truncate transition-opacity duration-150 motion-reduce:transition-none",
1755 isBranchCopied ? "opacity-0" : "opacity-100",
1756 )}
1757 >
1758 {detail.headBranch}
1759 </code>
1760 <span
1761 aria-hidden="true"
1762 className={cn(
1763 "col-start-1 row-start-1 truncate text-center transition-opacity duration-150 motion-reduce:transition-none",
1764 isBranchCopied ? "opacity-100" : "opacity-0",
1765 )}
1766 >
1767 Copied
1768 </span>
1769 </TooltipTrigger>
1770 <TooltipPopup side="top">
1771 {`${isBranchCopied ? "Copied" : "Copy pull request branch"}: ${detail.headBranch}`}
1772 </TooltipPopup>
1773 </Tooltip>
1774 {checkoutCommand ? (
1775 <Tooltip>
1776 <TooltipTrigger
1777 render={
1778 <Button
1779 size="micro"
1780 variant="outline"
1781 className="ml-1 min-w-0 basis-40 shrink-[0.75] gap-1 font-normal text-muted-foreground"
1782 aria-label={`Copy checkout command: ${checkoutCommand}`}
1783 onClick={() => copyCheckoutCommandToClipboard(checkoutCommand)}
1784 />
1785 }
1786 >
1787 <code className="min-w-0 truncate">{checkoutCommand}</code>
1788 <CopyIcon aria-hidden />
1789 </TooltipTrigger>
1790 <TooltipPopup className="max-w-96 wrap-anywhere font-mono" side="bottom">
1791 {checkoutCommand}
1792 </TooltipPopup>
1793 </Tooltip>
1794 ) : null}
1806 <PullRequestCopyableCode
1807 key={detail.headBranch}
1808 value={detail.headBranch}
1809 target="branch name"
1810 copyLabel="Copy pull request branch"
1811 copiedLabel="Branch name copied"
1812 />
17951813 </span>
17961814 <span className="ml-auto inline-flex shrink-0 items-center justify-end gap-2">
17971815 <span className="inline-flex items-center gap-1.5 tabular-nums">