| ... | ... | @@ -3,7 +3,7 @@ import { |
| 3 | 3 | isAtomCommandInterrupted, |
| 4 | 4 | squashAtomCommandFailure, |
| 5 | 5 | } from "@t3tools/client-runtime/state/runtime"; |
| 6 | | import type { EnvironmentId, VcsRef, ThreadId } from "@t3tools/contracts"; |
| 6 | import type { ContextMenuItem, EnvironmentId, VcsRef, ThreadId } from "@t3tools/contracts"; |
| 7 | 7 | import { LegendList, type LegendListRef } from "@legendapp/list/react"; |
| 8 | 8 | import { ChevronDownIcon, GitBranchIcon, RefreshCwIcon, SearchIcon } from "lucide-react"; |
| 9 | 9 | import { |
| ... | ... | @@ -17,9 +17,12 @@ import { |
| 17 | 17 | useRef, |
| 18 | 18 | useState, |
| 19 | 19 | useTransition, |
| 20 | type MouseEvent as ReactMouseEvent, |
| 20 | 21 | } from "react"; |
| 21 | 22 | |
| 22 | 23 | import { useComposerDraftStore, type DraftId } from "../composerDraftStore"; |
| 24 | import { writeTextToClipboard } from "../hooks/useCopyToClipboard"; |
| 25 | import { readLocalApi } from "../localApi"; |
| 23 | 26 | import { useOpenPrLink } from "../lib/openPullRequestLink"; |
| 24 | 27 | import { usePaginatedBranches } from "../state/queries"; |
| 25 | 28 | import { useProject, useThread } from "../state/entities"; |
| ... | ... | @@ -317,6 +320,45 @@ export function BranchToolbarBranchSelector({ |
| 317 | 320 | // --------------------------------------------------------------------------- |
| 318 | 321 | // Branch actions |
| 319 | 322 | // --------------------------------------------------------------------------- |
| 323 | const copyBranchName = useCallback((branchName: string) => { |
| 324 | void writeTextToClipboard(branchName, "branch name").then( |
| 325 | (didCopy) => { |
| 326 | if (!didCopy) return; |
| 327 | toastManager.add({ |
| 328 | type: "success", |
| 329 | title: "Branch name copied", |
| 330 | description: branchName, |
| 331 | }); |
| 332 | }, |
| 333 | (error: unknown) => { |
| 334 | toastManager.add( |
| 335 | stackedThreadToast({ |
| 336 | type: "error", |
| 337 | title: "Failed to copy branch name", |
| 338 | description: toBranchActionErrorMessage(error), |
| 339 | }), |
| 340 | ); |
| 341 | }, |
| 342 | ); |
| 343 | }, []); |
| 344 | |
| 345 | const handleBranchContextMenu = useCallback( |
| 346 | (event: ReactMouseEvent, branchName: string | null) => { |
| 347 | if (!branchName) return; |
| 348 | const api = readLocalApi(); |
| 349 | if (!api) return; |
| 350 | event.preventDefault(); |
| 351 | event.stopPropagation(); |
| 352 | const items: ContextMenuItem<"copy-branch-name">[] = [ |
| 353 | { id: "copy-branch-name", label: "Copy branch name", icon: "copy" }, |
| 354 | ]; |
| 355 | void api.contextMenu.show(items, { x: event.clientX, y: event.clientY }).then((action) => { |
| 356 | if (action === "copy-branch-name") copyBranchName(branchName); |
| 357 | }); |
| 358 | }, |
| 359 | [copyBranchName], |
| 360 | ); |
| 361 | |
| 320 | 362 | const runBranchAction = (action: () => Promise<void>) => { |
| 321 | 363 | startBranchActionTransition(async () => { |
| 322 | 364 | await action(); |
| ... | ... | @@ -624,6 +666,7 @@ export function BranchToolbarBranchSelector({ |
| 624 | 666 | value={itemValue} |
| 625 | 667 | className="pe-1.5" |
| 626 | 668 | onClick={() => selectBranch(refName)} |
| 669 | onContextMenu={(event) => handleBranchContextMenu(event, itemValue)} |
| 627 | 670 | > |
| 628 | 671 | <div className="flex w-full min-w-0 items-center justify-between gap-2"> |
| 629 | 672 | <span className="min-w-0 flex-1 truncate">{itemValue}</span> |
| ... | ... | @@ -674,15 +717,23 @@ export function BranchToolbarBranchSelector({ |
| 674 | 717 | <TooltipPopup side="top">{branchPrTooltip}</TooltipPopup> |
| 675 | 718 | </Tooltip> |
| 676 | 719 | ) : null} |
| 677 | | <ComboboxTrigger |
| 678 | | render={<Button variant="ghost" size="xs" />} |
| 679 | | className="min-w-0 text-muted-foreground/70 hover:text-foreground/80" |
| 680 | | disabled={isInitialBranchesLoadPending || isBranchActionPending} |
| 720 | {/* Context menu lives on the wrapper: the disabled Button has |
| 721 | pointer-events-none, so the trigger itself never sees right-clicks |
| 722 | while refs are loading or a branch action is pending. */} |
| 723 | <span |
| 724 | className="flex min-w-0" |
| 725 | onContextMenu={(event) => handleBranchContextMenu(event, resolvedActiveBranch)} |
| 681 | 726 | > |
| 682 | | <GitBranchIcon className="size-3 shrink-0 opacity-70" /> |
| 683 | | <span className="min-w-0 max-w-[240px] truncate">{triggerLabel}</span> |
| 684 | | <ChevronDownIcon className="size-3 shrink-0 opacity-50" /> |
| 685 | | </ComboboxTrigger> |
| 727 | <ComboboxTrigger |
| 728 | render={<Button variant="ghost" size="xs" />} |
| 729 | className="min-w-0 text-muted-foreground/70 hover:text-foreground/80" |
| 730 | disabled={isInitialBranchesLoadPending || isBranchActionPending} |
| 731 | > |
| 732 | <GitBranchIcon className="size-3 shrink-0 opacity-70" /> |
| 733 | <span className="min-w-0 max-w-[240px] truncate">{triggerLabel}</span> |
| 734 | <ChevronDownIcon className="size-3 shrink-0 opacity-50" /> |
| 735 | </ComboboxTrigger> |
| 736 | </span> |
| 686 | 737 | </div> |
| 687 | 738 | <ComboboxPopup align="end" side="top" className="flex w-80 flex-col"> |
| 688 | 739 | <div className="shrink-0 px-3 pt-2.5"> |