authorgravatar for 97427849+flamboh@users.noreply.github.comoliver <97427849+flamboh@users.noreply.github.com> 2026-09-29 17:40:55-07:00
committergravatar for noreply@github.comGitHub <noreply@github.com> 2026-09-29 21:40:55-03:00
log0fcd5f90611451cca842689faea53b5450c022da
tree25019ae99f68876b1e84f10b3c11f9f04ee9ec27
parent2a23c30ea60854cb17bec1a9d248032207c5d6b2
signature Signed by PGP key B5690EEEBB952194

fix(web): multi-PR badges open the linked pull requests panel (#13211)


4 files changed, 19 insertions(+), 17 deletions(-)

apps/web/src/components/BranchToolbarBranchSelector.tsx+1-1
...@@ -793,7 +793,7 @@ export function BranchToolbarBranchSelector({...@@ -793,7 +793,7 @@ export function BranchToolbarBranchSelector({
793 number={prNumber}793 number={prNumber}
794 url={prUrl}794 url={prUrl}
795 status={displayedPrStatus}795 status={displayedPrStatus}
796 onOpenStack={() => useRightPanelStore.getState().open(threadRef, "pull-requests")}796 onOpenList={() => useRightPanelStore.getState().open(threadRef, "pull-requests")}
797 onOpenPullRequest={(event) => {797 onOpenPullRequest={(event) => {
798 if (prUrl) openPrLink(event, prUrl);798 if (prUrl) openPrLink(event, prUrl);
799 }}799 }}
apps/web/src/components/Sidebar.tsx+4-3
...@@ -1515,11 +1515,12 @@ const SidebarThreadRow = memo(function SidebarThreadRow(props: {...@@ -1515,11 +1515,12 @@ const SidebarThreadRow = memo(function SidebarThreadRow(props: {
1515 const accessibleTitle = isRenaming ? null : <span className="sr-only">{thread.title}</span>;1515 const accessibleTitle = isRenaming ? null : <span className="sr-only">{thread.title}</span>;
15161516
1517 // Stacks show their layer count; multiple unrelated links show their total count.1517 // Stacks show their layer count; multiple unrelated links show their total count.
1518 // Plain clicks open T3; individual PR links also support opening the host in a new tab.1518 // Either opens the thread's pull requests tab; a single PR link opens that PR and still
1519 // supports opening the host in a new tab.
1519 const prBadgeShape = supportsMultiplePullRequests1520 const prBadgeShape = supportsMultiplePullRequests
1520 ? resolveThreadPullRequestBadge(thread.pullRequests)1521 ? resolveThreadPullRequestBadge(thread.pullRequests)
1521 : null;1522 : null;
1522 const handlePrStackClick = useCallback(() => {1523 const handlePrListClick = useCallback(() => {
1523 useRightPanelStore.getState().open(threadRef, "pull-requests");1524 useRightPanelStore.getState().open(threadRef, "pull-requests");
1524 if (!props.isActive) onThreadActivate(threadRef);1525 if (!props.isActive) onThreadActivate(threadRef);
1525 }, [onThreadActivate, props.isActive, threadRef]);1526 }, [onThreadActivate, props.isActive, threadRef]);
...@@ -1531,7 +1532,7 @@ const SidebarThreadRow = memo(function SidebarThreadRow(props: {...@@ -1531,7 +1532,7 @@ const SidebarThreadRow = memo(function SidebarThreadRow(props: {
1531 number={pr?.number ?? currentLinkedPr?.number}1532 number={pr?.number ?? currentLinkedPr?.number}
1532 url={pr?.url ?? currentLinkedPr?.url}1533 url={pr?.url ?? currentLinkedPr?.url}
1533 status={prStatus}1534 status={prStatus}
1534 onOpenStack={handlePrStackClick}1535 onOpenList={handlePrListClick}
1535 onOpenPullRequest={handlePrClick}1536 onOpenPullRequest={handlePrClick}
1536 />1537 />
1537 ) : null;1538 ) : null;
apps/web/src/components/ThreadStatusIndicators.tsx+13-12
...@@ -195,7 +195,8 @@ export function resolveThreadPullRequestBadgePresentation({...@@ -195,7 +195,8 @@ export function resolveThreadPullRequestBadgePresentation({
195 * The linked-PR badge shared by the sidebar and composer footer. The badge owns what it shows:195 * The linked-PR badge shared by the sidebar and composer footer. The badge owns what it shows:
196 * the state glyph and number at the meta size, in the state's color. The caller owns the control196 * the state glyph and number at the meta size, in the state's color. The caller owns the control
197 * it sits in through `render` (an inline link in a sidebar row, a toolbar control in the197 * it sits in through `render` (an inline link in a sidebar row, a toolbar control in the
198 * composer), and the badge fills in the link or stack button behavior.198 * composer), and the badge fills in the behavior: a single PR is a link to it, while a stack or
199 * several linked PRs is a button that opens the thread's pull requests tab.
199 */200 */
200export function ThreadPullRequestBadgeControl({201export function ThreadPullRequestBadgeControl({
201 render,202 render,
...@@ -203,7 +204,7 @@ export function ThreadPullRequestBadgeControl({...@@ -203,7 +204,7 @@ export function ThreadPullRequestBadgeControl({
203 number,204 number,
204 url,205 url,
205 status,206 status,
206 onOpenStack,207 onOpenList,
207 onOpenPullRequest,208 onOpenPullRequest,
208}: {209}: {
209 render: ReactElement<{ render?: useRender.RenderProp }>;210 render: ReactElement<{ render?: useRender.RenderProp }>;
...@@ -211,7 +212,7 @@ export function ThreadPullRequestBadgeControl({...@@ -211,7 +212,7 @@ export function ThreadPullRequestBadgeControl({
211 number?: number | undefined;212 number?: number | undefined;
212 url?: string | undefined;213 url?: string | undefined;
213 status: PrStatusIndicator | null;214 status: PrStatusIndicator | null;
214 onOpenStack: () => void;215 onOpenList: () => void;
215 onOpenPullRequest: (event: MouseEvent<HTMLElement>) => void;216 onOpenPullRequest: (event: MouseEvent<HTMLElement>) => void;
216}) {217}) {
217 const presentation = resolveThreadPullRequestBadgePresentation({ badge, number, url, status });218 const presentation = resolveThreadPullRequestBadgePresentation({ badge, number, url, status });
...@@ -220,9 +221,9 @@ export function ThreadPullRequestBadgeControl({...@@ -220,9 +221,9 @@ export function ThreadPullRequestBadgeControl({
220 <PullRequestBadge221 <PullRequestBadge
221 render={render}222 render={render}
222 presentation={presentation}223 presentation={presentation}
223 isStack={badge?.kind === "stack"}224 opensList={badge !== null && (badge.kind === "stack" || badge.others > 0)}
224 url={url}225 url={url}
225 onOpenStack={onOpenStack}226 onOpenList={onOpenList}
226 onOpenPullRequest={onOpenPullRequest}227 onOpenPullRequest={onOpenPullRequest}
227 />228 />
228 );229 );
...@@ -231,26 +232,26 @@ export function ThreadPullRequestBadgeControl({...@@ -231,26 +232,26 @@ export function ThreadPullRequestBadgeControl({
231function PullRequestBadge({232function PullRequestBadge({
232 render,233 render,
233 presentation,234 presentation,
234 isStack,235 opensList,
235 url,236 url,
236 onOpenStack,237 onOpenList,
237 onOpenPullRequest,238 onOpenPullRequest,
238}: {239}: {
239 render: ReactElement<{ render?: useRender.RenderProp }>;240 render: ReactElement<{ render?: useRender.RenderProp }>;
240 presentation: NonNullable<ReturnType<typeof resolveThreadPullRequestBadgePresentation>>;241 presentation: NonNullable<ReturnType<typeof resolveThreadPullRequestBadgePresentation>>;
241 isStack: boolean;242 opensList: boolean;
242 url: string | undefined;243 url: string | undefined;
243 onOpenStack: () => void;244 onOpenList: () => void;
244 onOpenPullRequest: (event: MouseEvent<HTMLElement>) => void;245 onOpenPullRequest: (event: MouseEvent<HTMLElement>) => void;
245}) {246}) {
246 const onClick = isStack247 const onClick = opensList
247 ? (event: MouseEvent<HTMLElement>) => {248 ? (event: MouseEvent<HTMLElement>) => {
248 event.preventDefault();249 event.preventDefault();
249 event.stopPropagation();250 event.stopPropagation();
250 onOpenStack();251 onOpenList();
251 }252 }
252 : onOpenPullRequest;253 : onOpenPullRequest;
253 const element = isStack ? (254 const element = opensList ? (
254 <button type="button" />255 <button type="button" />
255 ) : (256 ) : (
256 <a href={url} target="_blank" rel="noopener noreferrer" />257 <a href={url} target="_blank" rel="noopener noreferrer" />
docs/user/source-control.md+1-1
...@@ -164,7 +164,7 @@ on the Pull Requests page, **Link to thread** lets you search for an active thre...@@ -164,7 +164,7 @@ on the Pull Requests page, **Link to thread** lets you search for an active thre
164also lists the threads that link to it, including archived threads, so you can return to their context.164also lists the threads that link to it, including archived threads, so you can return to their context.
165165
166Thread badges show a stack's layer count or the current review number with a count of additional166Thread badges show a stack's layer count or the current review number with a count of additional
167links. On mobile, the Git overview lists linked reviews and their stacks; tap a review to open it.167links. Clicking a badge with more than one review opens the **Linked pull requests** panel. On mobile, the Git overview lists linked reviews and their stacks; tap a review to open it.
168Linking and unlinking are available in the web and desktop clients.168Linking and unlinking are available in the web and desktop clients.
169169
170The **Linked pull requests** panel lists every review and groups stacks. Unlink a review from its170The **Linked pull requests** panel lists every review and groups stacks. Unlink a review from its