Skip to content
Merged
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
66 changes: 66 additions & 0 deletions apps/web/src/components/pullRequest/PullRequestCopyableCode.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
import { useCopyToClipboard } from "~/hooks/useCopyToClipboard";
import { cn } from "~/lib/utils";

import { Tooltip, TooltipPopup, TooltipTrigger } from "../ui/tooltip";

export function PullRequestCopyableCode({
value,
target,
copyLabel,
copiedLabel,
className,
tooltipSide = "top",
onError,
}: {
readonly value: string;
readonly target: string;
readonly copyLabel: string;
readonly copiedLabel: string;
readonly className?: string;
readonly tooltipSide?: "top" | "bottom";
readonly onError?: (error: Error) => void;
}) {
const { copyToClipboard, isCopied } = useCopyToClipboard({
target,
timeout: 1600,
...(onError ? { onError } : {}),
});
return (
<Tooltip>
<TooltipTrigger
render={
Comment thread
flamboh marked this conversation as resolved.
<button
type="button"
className={cn(
"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",
className,
)}
aria-label={isCopied ? copiedLabel : copyLabel}
onClick={() => copyToClipboard(value)}
/>
}
>
<code
className={cn(
"col-start-1 row-start-1 min-w-0 truncate transition-opacity duration-150 motion-reduce:transition-none",
isCopied ? "opacity-0" : "opacity-100",
)}
>
{value}
</code>
<span
aria-hidden="true"
className={cn(
"col-start-1 row-start-1 truncate text-center transition-opacity duration-150 motion-reduce:transition-none",
isCopied ? "opacity-100" : "opacity-0",
)}
>
Copied
</span>
</TooltipTrigger>
<TooltipPopup className="max-w-96 wrap-anywhere font-mono" side={tooltipSide}>
{`${isCopied ? "Copied" : copyLabel}: ${value}`}
</TooltipPopup>
</Tooltip>
);
}
113 changes: 31 additions & 82 deletions apps/web/src/components/pullRequest/PullRequestDetailPanel.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -121,6 +121,7 @@ import { Popover, PopoverPopup, PopoverTrigger } from "../ui/popover";
import { toastManager } from "../ui/toast";
import { Tooltip, TooltipPopup, TooltipProvider, TooltipTrigger } from "../ui/tooltip";
import { PullRequestDetailGhost, PullRequestTimelineGhost } from "./PullRequestGhosts";
import { PullRequestCopyableCode } from "./PullRequestCopyableCode";
import { PullRequestActivityUnavailableState } from "./PullRequestActivityUnavailableState";
import { DiffPanelLoadingState } from "../DiffPanelShell";
import { PullRequestsUnavailableState } from "./PullRequestsUnavailableState";
Expand All @@ -140,6 +141,7 @@ import {
handoffPrompt,
handoffReviewComments,
latestPullRequestReviewOutcomes,
loadingPullRequestCheckoutCommand,
isStackedPullRequestBase,
pullRequestActionMenuHasGroup,
pullRequestActionNeedsHostRefresh,
Expand Down Expand Up @@ -326,68 +328,6 @@ const openNumberContextMenu = (
});
};

function PullRequestCopyableCode({
value,
target,
copyLabel,
copiedLabel,
className,
tooltipSide = "top",
onError,
}: {
readonly value: string;
readonly target: string;
readonly copyLabel: string;
readonly copiedLabel: string;
readonly className?: string;
readonly tooltipSide?: "top" | "bottom";
readonly onError?: (error: Error) => void;
}) {
const { copyToClipboard, isCopied } = useCopyToClipboard({
target,
timeout: 1600,
...(onError ? { onError } : {}),
});
return (
<Tooltip>
<TooltipTrigger
render={
<button
type="button"
className={cn(
"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",
className,
)}
aria-label={isCopied ? copiedLabel : copyLabel}
onClick={() => copyToClipboard(value)}
/>
}
>
<code
className={cn(
"col-start-1 row-start-1 min-w-0 truncate transition-opacity duration-150 motion-reduce:transition-none",
isCopied ? "opacity-0" : "opacity-100",
)}
>
{value}
</code>
<span
aria-hidden="true"
className={cn(
"col-start-1 row-start-1 truncate text-center transition-opacity duration-150 motion-reduce:transition-none",
isCopied ? "opacity-100" : "opacity-0",
)}
>
Copied
</span>
</TooltipTrigger>
<TooltipPopup className="max-w-96 wrap-anywhere font-mono" side={tooltipSide}>
{`${isCopied ? "Copied" : copyLabel}: ${value}`}
</TooltipPopup>
</Tooltip>
);
}

/**
* The stale-branch warning, said beside the branch it is about rather than as a bar of its own.
* The banner this replaces held a row of chrome open across the top of every pull request that
Expand Down Expand Up @@ -804,15 +744,22 @@ export function PullRequestDetailPanel({
repositoryUrl !== null
? new URL(`/${encodeURIComponent(detail.author.login)}`, repositoryUrl).toString()
: null;
const checkoutCommand = detail
const checkoutCommand = handoffSummary
? pullRequestCheckoutCommand(
detail.provider,
detail.number,
detail.headBranch,
detail.headRepositoryNameWithOwner,
repositoryUrl,
handoffSummary.provider,
handoffSummary.number,
handoffSummary.headBranch,
detail?.headRepositoryNameWithOwner,
changeRequestRepositoryUrl(handoffSummary.url),
)
: null;
: loadingPullRequestCheckoutCommand(reference, repositoryIdentity);
const onCheckoutCommandError = useCallback((error: Error) => {
toastManager.add({
type: "error",
title: "Could not copy checkout command",
description: error.message,
});
}, []);
const branchRefsQuery = useEnvironmentQuery(
detail === null
? null
Expand Down Expand Up @@ -1475,8 +1422,9 @@ export function PullRequestDetailPanel({
// Out of date with the base, and still cleanly mergeable — the one pairing an update button
// exists for. Null everywhere else, including hosts that cannot compare at all.
const freshness = detail === null ? null : resolveBaseFreshness(detail);
// A host that cannot produce a patch has no Code tab to open. The tabs themselves stay hidden
// until the detail arrives, so the loading ghost is the panel's only unfinished UI.
// A host that cannot produce a patch has no Code tab to open. While detail is loading the ghost
// uses this optimistic tab set to reserve the same chrome; a host without a patch removes Code
// when its capabilities arrive.
const visibleTabs = TABS.filter(
(item) => item.value !== "code" || detail === null || detail.capabilities.diff,
);
Expand Down Expand Up @@ -1664,6 +1612,13 @@ export function PullRequestDetailPanel({
<PullRequestDetailGhost
seed={matchingListEntry}
summary={sharedSummary}
checkoutCommand={checkoutCommand}
onCheckoutError={onCheckoutCommandError}
number={reference.number}
tabs={visibleTabs}
activeTab={tab}
{...(onBack ? { onBack } : {})}
{...(onClose ? { onClose } : {})}
actions={
handoffSummary ? (
<TooltipProvider delay={150} closeDelay={150} timeout={400}>
Expand Down Expand Up @@ -2371,7 +2326,7 @@ export function PullRequestDetailPanel({
{detail ? (
<div className="col-span-2 mt-1 min-w-0 px-4 pb-4">
{titleDraft === null ? (
<div className="group flex min-w-0 items-center gap-1">
<div className="group flex min-h-7 min-w-0 items-center gap-1 sm:min-h-6">
<Tooltip>
<TooltipTrigger
render={
Expand Down Expand Up @@ -2432,7 +2387,7 @@ export function PullRequestDetailPanel({
</div>
</div>
)}
<div className="mt-2 flex min-w-0 items-center gap-2 text-xs text-muted-foreground">
<div className="mt-2 flex min-h-5 min-w-0 items-center gap-2 text-xs text-muted-foreground">
<PullRequestMetaLine className="min-w-0 whitespace-nowrap">
<PullRequestActorLabel
actor={detail.author}
Expand All @@ -2450,18 +2405,12 @@ export function PullRequestDetailPanel({
copiedLabel="Checkout command copied"
className="ml-auto font-mono"
tooltipSide="bottom"
onError={(error) =>
toastManager.add({
type: "error",
title: "Could not copy checkout command",
description: error.message,
})
}
onError={onCheckoutCommandError}
/>
) : null}
</div>

<div className="mt-4 flex min-w-0 items-center gap-2 text-xs text-muted-foreground">
<div className="mt-4 flex min-h-5 min-w-0 items-center gap-2 text-xs text-muted-foreground">
<span className="flex min-w-0 flex-1 items-center gap-1.5 font-mono text-xs text-muted-foreground/70">
<Tooltip>
<TooltipTrigger
Expand Down Expand Up @@ -2504,7 +2453,7 @@ export function PullRequestDetailPanel({
/>
</span>
<span className="ml-auto inline-flex shrink-0 items-center justify-end gap-2">
<span className="inline-flex items-center gap-1.5 tabular-nums">
<span className="inline-flex min-w-16 items-center justify-end gap-1.5 tabular-nums">
<FileDiffIcon className="size-3.5" />
{detail.changedFiles.toLocaleString()}{" "}
{detail.changedFiles === 1 ? "file" : "files"}
Expand Down
Loading
Loading