From 10a22d3d1ade748933e0944d68ffb22003d9f5e6 Mon Sep 17 00:00:00 2001 From: flamboh Date: Mon, 2 Nov 2026 04:18:46 +0000 Subject: [PATCH 1/4] fix(web): pull request header shows its repository and collapses on scroll --- .../pullRequest/PullRequestDetailCrumbs.tsx | 36 +++ .../pullRequest/PullRequestDetailPanel.tsx | 206 ++++++++++++++---- 2 files changed, 202 insertions(+), 40 deletions(-) diff --git a/apps/web/src/components/pullRequest/PullRequestDetailCrumbs.tsx b/apps/web/src/components/pullRequest/PullRequestDetailCrumbs.tsx index 0128f01f06a48..1d28270049480 100644 --- a/apps/web/src/components/pullRequest/PullRequestDetailCrumbs.tsx +++ b/apps/web/src/components/pullRequest/PullRequestDetailCrumbs.tsx @@ -14,6 +14,7 @@ interface CrumbsPullRequest { readonly provider: string; readonly number: number; readonly repository: string; + readonly title: string; readonly state: "open" | "merged" | "closed"; readonly isDraft: boolean; } @@ -98,12 +99,16 @@ function NumberLink({ export function PullRequestDetailCrumbs({ detail, toneClassName, + condensed, + repositoryUrl, openOnHostText, onBack, children, }: { detail: CrumbsPullRequest | null; toneClassName: string | null; + condensed: boolean; + repositoryUrl: string | null; openOnHostText: string; onBack: (() => void) | undefined; children?: ReactNode; @@ -120,6 +125,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} + + ) : null} + {condensed ? ( + + + {detail.title} + + } + /> + {detail.title} + + ) : null} {children} ) : null} diff --git a/apps/web/src/components/pullRequest/PullRequestDetailPanel.tsx b/apps/web/src/components/pullRequest/PullRequestDetailPanel.tsx index c7be3048019f5..d908cab807d2b 100644 --- a/apps/web/src/components/pullRequest/PullRequestDetailPanel.tsx +++ b/apps/web/src/components/pullRequest/PullRequestDetailPanel.tsx @@ -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"; @@ -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> + >({}); + 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 +1532,83 @@ export function PullRequestDetailPanel({ ) : null} - {detail ? ( -
- - setTitleScope(text === null ? null : { pullRequestKey, text }) - } - onSaved={refreshDetail} - /> -
- - - - - - void perform("update-branch", undefined, method)} - iconClassName="size-3" +
+
+ {detail ? ( +
+ - - + + + {headerBranches} +
+ ) : null} +
+
+ +
+
+ {detail ? ( +
+ + setTitleScope(text === null ? null : { pullRequestKey, text }) + } + onSaved={refreshDetail} /> - -
+
+ + + + + {headerBranches} +
+
+ ) : null}
- ) : null} +
{detail ? (