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
21 changes: 21 additions & 0 deletions apps/web/src/components/pullRequest/PullRequestDetailCrumbs.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -98,12 +98,14 @@ function NumberLink({
export function PullRequestDetailCrumbs({
detail,
toneClassName,
repositoryUrl,
openOnHostText,
onBack,
children,
}: {
detail: CrumbsPullRequest | null;
toneClassName: string | null;
repositoryUrl: string | null;
openOnHostText: string;
onBack: (() => void) | undefined;
children?: ReactNode;
Expand All @@ -120,6 +122,25 @@ export function PullRequestDetailCrumbs({
plural={sourceControlClients.get(detail.provider).changeRequest.plural}
/>
) : null}
<Tooltip>
<TooltipTrigger
render={
repositoryUrl ? (
<InlineButton
tone="muted"
onClick={() => void readLocalApi()?.shell.openExternal(repositoryUrl)}
>
{detail.repository}
</InlineButton>
) : (
<span className="shrink-0 font-medium">{detail.repository}</span>
)
}
/>
<TooltipPopup side="top">
{repositoryUrl ? `Open ${detail.repository} repository` : detail.repository}
</TooltipPopup>
</Tooltip>
<NumberLink
detail={detail}
toneClassName={toneClassName}
Expand Down
217 changes: 176 additions & 41 deletions apps/web/src/components/pullRequest/PullRequestDetailPanel.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,15 @@ import {
GitCommitHorizontalIcon,
PanelRightIcon,
} from "lucide-react";
import { lazy, Suspense, useEffect, useEffectEvent, useRef, useState } from "react";
import {
lazy,
Suspense,
useEffect,
useEffectEvent,
useLayoutEffect,
useRef,
useState,
} from "react";
import { type DraftId, useComposerDraftStore } from "~/composerDraftStore";
import { type PullRequestDetailTab, useRightPanelStore } from "~/rightPanelStore";
import { useEnvironmentSettings } from "~/hooks/useSettings";
Expand Down Expand Up @@ -78,7 +86,7 @@ import { useUiStateStore } from "~/uiStateStore";
import { Button } from "../ui/button";
import { Toggle, ToggleGroup } from "../ui/toggle-group";
import { toastManager } from "../ui/toast";
import { TooltipProvider } from "../ui/tooltip";
import { Tooltip, TooltipPopup, TooltipProvider, TooltipTrigger } from "../ui/tooltip";
import { PullRequestDetailGhost, PullRequestTimelineGhost } from "./PullRequestGhosts";
import { PullRequestCopyableCode } from "./PullRequestCopyableCode";
import { PullRequestActivityUnavailableState } from "./PullRequestActivityUnavailableState";
Expand Down Expand Up @@ -390,6 +398,42 @@ export function PullRequestDetailPanel({
} else if (!tabMountState.tabs.has(tab)) {
setTabMountState({ key: tabScopeKey, tabs: new Set(tabMountState.tabs).add(tab) });
}
const [condensedByTab, setCondensedByTab] = useState<
Partial<Record<PullRequestDetailTab, boolean>>
>({});
const condensed = condensedByTab[tab] ?? false;
const scrollerByTab = useRef<Partial<Record<PullRequestDetailTab, HTMLElement | null>>>({});
const [scrollerRefs] = useState(() => ({
summary: (node: HTMLElement | null) => {
scrollerByTab.current.summary = node;
},
timeline: (node: HTMLElement | null) => {
scrollerByTab.current.timeline = node;
},
code: (node: HTMLElement | null) => {
scrollerByTab.current.code = node;
},
}));
const foldRef = useRef<HTMLDivElement | null>(null);
const condensedRowRef = useRef<HTMLDivElement | null>(null);
const compensationRef = useRef<{
tab: PullRequestDetailTab;
scroller: HTMLElement;
target: number;
} | null>(null);
// oxlint-disable-next-line t3code/no-unreasoned-state-sync -- restores the scroller's scrollTop before paint after the header condenses
useLayoutEffect(() => {
const compensation = compensationRef.current;
if (compensation === null) return;
compensationRef.current = null;
if (
!condensed ||
compensation.tab !== tab ||
scrollerByTab.current[tab] !== compensation.scroller
)
return;
compensation.scroller.scrollTop = Math.max(0, compensation.target);
}, [condensed, tab]);
const [titleScope, setTitleScope] = useState<{
readonly pullRequestKey: string;
readonly text: string;
Expand Down Expand Up @@ -1306,6 +1350,29 @@ export function PullRequestDetailPanel({
</TooltipProvider>
);

const headerBranches = detail ? (
<span className="ml-auto flex min-w-0 flex-1 items-center justify-end gap-1.5 font-mono text-caption text-dim-foreground">
<PullRequestBaseBranch
compact
baseBranch={detail.baseBranch}
freshness={freshness}
stacked={isStackedPullRequest}
pending={actionPending}
onUpdate={(method) => void perform("update-branch", undefined, method)}
iconClassName="size-3"
/>
<ArrowLeftIcon aria-label="receives changes from" className="size-3 shrink-0" />
<PullRequestCopyableCode
key={detail.headBranch}
className="min-w-12 rounded-kbd bg-secondary px-1.5 py-px text-muted-foreground"
value={detail.headBranch}
target="branch name"
copyLabel={`Copy ${terminology.singular} branch`}
copiedLabel="Branch name copied"
/>
</span>
) : null;

if (detailQuery.isPending && !detail) {
return (
<PullRequestDetailGhost
Expand Down Expand Up @@ -1346,6 +1413,7 @@ export function PullRequestDetailPanel({
<PullRequestDetailCrumbs
detail={detail}
toneClassName={statePresentation?.toneClassName ?? null}
repositoryUrl={repositoryUrl}
openOnHostText={openOnHostText}
onBack={onBack}
>
Expand Down Expand Up @@ -1463,47 +1531,93 @@ export function PullRequestDetailPanel({
) : null}
</div>

{detail ? (
<div className="col-span-2 flex min-w-0 flex-col gap-1.5 px-4 pt-1.5 pb-2.5">
<PullRequestTitleEditor
environmentId={environmentId}
reference={reference}
detail={detail}
canWriteSourceControl={canWriteSourceControl}
value={titleDraft}
onValueChange={(text) =>
setTitleScope(text === null ? null : { pullRequestKey, text })
}
onSaved={refreshDetail}
/>
<div className="flex min-h-5 min-w-0 flex-wrap items-center gap-x-2 gap-y-1 text-meta text-muted-foreground">
<PullRequestActorLabel actor={detail.author} profileUrl={authorProfileUrl} />
<span className="shrink-0">
<RelativeTime timestamp={detail.updatedAt} />
</span>
<span className="ml-auto flex min-w-0 flex-1 items-center justify-end gap-1.5 font-mono text-caption text-dim-foreground">
<PullRequestBaseBranch
compact
baseBranch={detail.baseBranch}
freshness={freshness}
stacked={isStackedPullRequest}
pending={actionPending}
onUpdate={(method) => void perform("update-branch", undefined, method)}
iconClassName="size-3"
<div
className={cn(
"col-span-2 grid",
condensed
? "grid-rows-[1fr]"
: "grid-rows-[0fr] transition-[grid-template-rows] duration-200 ease-out motion-reduce:transition-none",
)}
>
<div
ref={condensedRowRef}
className={cn(
"min-h-0 overflow-hidden transition-[opacity,transform] duration-150 ease-out motion-reduce:transform-none motion-reduce:transition-none",
condensed
? "translate-y-0 opacity-100 delay-50"
: "translate-y-1 opacity-0 duration-100",
)}
inert={!condensed}
>
{detail ? (
<div className="flex min-w-0 items-center gap-2 px-4 pt-1 pb-1 text-meta text-muted-foreground">
<PullRequestActorLabel
actor={detail.author}
profileUrl={authorProfileUrl}
variant="avatar"
className="shrink-0"
/>
<ArrowLeftIcon aria-label="receives changes from" className="size-3 shrink-0" />
<PullRequestCopyableCode
key={detail.headBranch}
className="min-w-12 rounded-kbd bg-secondary px-1.5 py-px text-muted-foreground"
value={detail.headBranch}
target="branch name"
copyLabel={`Copy ${terminology.singular} branch`}
copiedLabel="Branch name copied"
<Tooltip>
<TooltipTrigger
render={
<span className="min-w-0 flex-1 truncate font-medium text-foreground">
{detail.title}
</span>
}
/>
<TooltipPopup side="top">{detail.title}</TooltipPopup>
</Tooltip>
<span className="shrink-0">
<RelativeTime timestamp={detail.updatedAt} />
</span>
<span className="hidden min-w-0 flex-1 @2xl/pr-header:flex">{headerBranches}</span>
</div>
) : null}
</div>
</div>

<div
className={cn(
"col-span-2 grid",
condensed
? "grid-rows-[0fr]"
: "grid-rows-[1fr] transition-[grid-template-rows] duration-200 ease-out motion-reduce:transition-none",
)}
>
<div
ref={foldRef}
className={cn(
"min-h-0 overflow-hidden transition-[opacity,transform] duration-150 ease-out motion-reduce:transform-none motion-reduce:transition-none",
condensed
? "-translate-y-1 opacity-0 duration-100"
: "translate-y-0 opacity-100 delay-50",
)}
inert={condensed}
>
{detail ? (
<div className="col-span-2 flex min-w-0 flex-col gap-1.5 px-4 pt-1.5 pb-2.5">
<PullRequestTitleEditor
environmentId={environmentId}
reference={reference}
detail={detail}
canWriteSourceControl={canWriteSourceControl}
value={titleDraft}
onValueChange={(text) =>
setTitleScope(text === null ? null : { pullRequestKey, text })
}
onSaved={refreshDetail}
/>
</span>
</div>
<div className="flex min-h-5 min-w-0 flex-wrap items-center gap-x-2 gap-y-1 text-meta text-muted-foreground">
<PullRequestActorLabel actor={detail.author} profileUrl={authorProfileUrl} />
<span className="shrink-0">
<RelativeTime timestamp={detail.updatedAt} />
</span>
{headerBranches}
</div>
</div>
) : null}
</div>
) : null}
</div>

{detail ? (
<nav
Expand Down Expand Up @@ -1604,7 +1718,25 @@ export function PullRequestDetailPanel({
repository={reference.repository}
className="border-b border-(--detail-separator)"
/>
<div className="relative flex min-h-0 flex-1 flex-col overflow-hidden">
<div
className="relative flex min-h-0 flex-1 flex-col overflow-hidden"
onScrollCapture={(event) => {
const scroller = scrollerByTab.current[tab];
if (event.target !== scroller || scroller === null || scroller === undefined) return;
const top = scroller.scrollTop;
const previous = condensedByTab[tab] ?? false;
const foldHeight = foldRef.current?.scrollHeight ?? 0;
let next = previous;
if (previous) {
if (top < 4 && foldHeight > 0) next = false;
} else if (foldHeight > 0 && top > foldHeight + 32) {
const chromeDelta = foldHeight - (condensedRowRef.current?.scrollHeight ?? 0);
compensationRef.current = { tab, scroller, target: top - chromeDelta };
next = true;
}
if (next !== previous) setCondensedByTab({ ...condensedByTab, [tab]: next });
}}
>
{detailQuery.error && !detail ? (
<PullRequestsUnavailableState
{...(isPullRequestNotFound(detailQuery.failure)
Expand All @@ -1631,6 +1763,7 @@ export function PullRequestDetailPanel({
{mountedTabs.has("summary") ? (
<div className={cn("absolute inset-0", tab !== "summary" && "invisible")}>
<PullRequestSummaryTab
scrollerRef={scrollerRefs.summary}
readiness={readinessCard}
environmentId={environmentId}
threadRef={threadRef}
Expand Down Expand Up @@ -1689,6 +1822,7 @@ export function PullRequestDetailPanel({
/>
) : (
<PullRequestTimelineTab
scrollerRef={scrollerRefs.timeline}
detail={detail}
environmentId={environmentId}
threadRef={threadRef}
Expand All @@ -1707,6 +1841,7 @@ export function PullRequestDetailPanel({
}
>
<PullRequestCodeTab
scrollerRef={scrollerRefs.code}
toolbarContainer={codeToolbarContainer}
onAddToAgentSelection={addSelectionToAgent}
environmentId={environmentId}
Expand Down
Loading