diff --git a/apps/web/src/components/pullRequest/PullRequestDetailCrumbs.tsx b/apps/web/src/components/pullRequest/PullRequestDetailCrumbs.tsx index 0128f01f06a48..ce9e97578b22c 100644 --- a/apps/web/src/components/pullRequest/PullRequestDetailCrumbs.tsx +++ b/apps/web/src/components/pullRequest/PullRequestDetailCrumbs.tsx @@ -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; @@ -120,6 +122,25 @@ export function PullRequestDetailCrumbs({ plural={sourceControlClients.get(detail.provider).changeRequest.plural} /> ) : null} + + void readLocalApi()?.shell.openExternal(repositoryUrl)} + > + {detail.repository} + + ) : ( + {detail.repository} + ) + } + /> + + {repositoryUrl ? `Open ${detail.repository} repository` : detail.repository} + + > + >({}); + const condensed = condensedByTab[tab] ?? false; + const scrollerByTab = useRef>>({}); + 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(null); + const condensedRowRef = useRef(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; @@ -1306,6 +1350,29 @@ export function PullRequestDetailPanel({ ); + const headerBranches = detail ? ( + + void perform("update-branch", undefined, method)} + iconClassName="size-3" + /> + + + + ) : null; + if (detailQuery.isPending && !detail) { return ( @@ -1463,47 +1531,93 @@ export function PullRequestDetailPanel({ ) : null} - {detail ? ( -
- - setTitleScope(text === null ? null : { pullRequestKey, text }) - } - onSaved={refreshDetail} - /> -
- - - - - - void perform("update-branch", undefined, method)} - iconClassName="size-3" +
+
+ {detail ? ( +
+ - - + + {detail.title} + + } + /> + {detail.title} + + + + + {headerBranches} +
+ ) : null} +
+
+ +
+
+ {detail ? ( +
+ + setTitleScope(text === null ? null : { pullRequestKey, text }) + } + onSaved={refreshDetail} /> - -
+
+ + + + + {headerBranches} +
+
+ ) : null}
- ) : null} +
{detail ? (