From af479577d128bdbf472365f14b98ddf32dae0540 Mon Sep 17 00:00:00 2001 From: Nickolas Kyryliuk Date: Fri, 7 Aug 2026 11:21:09 +0300 Subject: [PATCH] fix(web): draw one divider where the actions menu has one group MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The group holding the draft toggle and the merge strategies opened with a divider whether or not it held anything, and the merge strategies drew a second one above themselves whether or not the draft toggle was above them. Both cases are reachable: - On a host with no draft of its own — Bitbucket declares merge and close — nothing renders between the divider that opens the group and the one the merge strategies draw, so the two land together. - On a draft whose "Ready for review" is already the header button, the toggle is withheld to avoid offering it twice and the strategies are hidden because a draft cannot be merged, so the group is empty and the dividers that open and close it land together. Both dividers are now drawn from the same answer as the group's contents. Co-Authored-By: Claude Opus 5 (1M context) --- .../pullRequest/PullRequestDetailPanel.tsx | 27 +++++++++++++------ 1 file changed, 19 insertions(+), 8 deletions(-) diff --git a/apps/web/src/components/pullRequest/PullRequestDetailPanel.tsx b/apps/web/src/components/pullRequest/PullRequestDetailPanel.tsx index 5d31b04fd65..6a5aebded91 100644 --- a/apps/web/src/components/pullRequest/PullRequestDetailPanel.tsx +++ b/apps/web/src/components/pullRequest/PullRequestDetailPanel.tsx @@ -596,6 +596,18 @@ export function PullRequestDetailPanel({ : allowedMergeMethods.length > 0 ? "merge" : null; + // What the menu's action group holds. Named once so the separators around it are drawn from + // the same answer as its contents, rather than on the assumption that it has any. + const showsDraftToggle = + detail?.state === "open" && + can(detail.isDraft ? "ready" : "draft") && + !(detail.isDraft && primaryAction === "ready"); + const showsMergeMethods = + detail?.state === "open" && + can("merge") && + !detail.isDraft && + !conflicting && + allowedMergeMethods.length > 1; // The pull request number carries this state in the overview and the right-panel tab mirrors // it. Conflicts keep their own row below: an open pull request remains green there. const statePresentation = detail @@ -669,8 +681,7 @@ export function PullRequestDetailPanel({ {/* Only where the button row could not take it: "Ready for review" on a draft is the primary header button, so offering it here as well would show the same action twice. */} - {can(detail.isDraft ? "ready" : "draft") && - !(detail.isDraft && primaryAction === "ready") ? ( + {showsDraftToggle ? ( void perform(detail.isDraft ? "ready" : "draft")} @@ -688,12 +699,12 @@ export function PullRequestDetailPanel({ Hidden while conflicting: every method would fail. */} {/* Only where merging is on offer at all: a strategy to merge with is not a choice for someone who may not merge. */} - {can("merge") && - !detail.isDraft && - !conflicting && - allowedMergeMethods.length > 1 ? ( + {showsMergeMethods ? ( <> - + {/* Only below the draft control. A host with no draft of its own, or + a draft whose control is already the header button, would leave + this against the separator that opened the group. */} + {showsDraftToggle ? : null} @@ -713,7 +724,7 @@ export function PullRequestDetailPanel({ ) : null} - + {showsDraftToggle || showsMergeMethods ? : null} ) : null} void readLocalApi()?.shell.openExternal(detail.url)}>