| ... | @@ -102,7 +102,6 @@ import { Badge } from "../ui/badge"; | ... | @@ -102,7 +102,6 @@ import { Badge } from "../ui/badge"; |
| 102 | import { Button } from "../ui/button"; | 102 | import { Button } from "../ui/button"; |
| 103 | import { PullRequestEditButton } from "./PullRequestEditButton"; | 103 | import { PullRequestEditButton } from "./PullRequestEditButton"; |
| 104 | import { Input } from "../ui/input"; | 104 | import { Input } from "../ui/input"; |
| 105 | import { Toggle, ToggleGroup } from "../ui/toggle-group"; | | |
| 106 | import { | 105 | import { |
| 107 | Menu, | 106 | Menu, |
| 108 | MenuItem, | 107 | MenuItem, |
| ... | @@ -117,6 +116,12 @@ import { Popover, PopoverPopup, PopoverTrigger } from "../ui/popover"; | ... | @@ -117,6 +116,12 @@ import { Popover, PopoverPopup, PopoverTrigger } from "../ui/popover"; |
| 117 | import { toastManager } from "../ui/toast"; | 116 | import { toastManager } from "../ui/toast"; |
| 118 | import { Tooltip, TooltipPopup, TooltipProvider, TooltipTrigger } from "../ui/tooltip"; | 117 | import { Tooltip, TooltipPopup, TooltipProvider, TooltipTrigger } from "../ui/tooltip"; |
| 119 | import { MiddleTruncate } from "../ui/middle-truncate"; | 118 | import { MiddleTruncate } from "../ui/middle-truncate"; |
| | 119 | import { |
| | 120 | PullRequestChecksStatusLine, |
| | 121 | PullRequestDetailHeaderBody, |
| | 122 | PullRequestDetailTabBar, |
| | 123 | PullRequestDetailTitleRow, |
| | 124 | } from "./PullRequestDetailLayout"; |
| 120 | import { PullRequestDetailGhost, PullRequestTimelineGhost } from "./PullRequestGhosts"; | 125 | import { PullRequestDetailGhost, PullRequestTimelineGhost } from "./PullRequestGhosts"; |
| 121 | import { PullRequestCopyableCode } from "./PullRequestCopyableCode"; | 126 | import { PullRequestCopyableCode } from "./PullRequestCopyableCode"; |
| 122 | import { PullRequestActivityUnavailableState } from "./PullRequestActivityUnavailableState"; | 127 | import { PullRequestActivityUnavailableState } from "./PullRequestActivityUnavailableState"; |
| ... | @@ -2373,77 +2378,79 @@ export function PullRequestDetailPanel({ | ... | @@ -2373,77 +2378,79 @@ export function PullRequestDetailPanel({ |
| 2373 | inert={condensed} | 2378 | inert={condensed} |
| 2374 | > | 2379 | > |
| 2375 | {detail ? ( | 2380 | {detail ? ( |
| 2376 | <div className="col-span-2 mt-1 min-w-0 px-4 pb-4"> | 2381 | <PullRequestDetailHeaderBody |
| 2377 | {titleDraft === null ? ( | 2382 | title={ |
| 2378 | <div className="group flex min-h-7 min-w-0 items-center gap-1 sm:min-h-6"> | 2383 | titleDraft === null ? ( |
| 2379 | <Tooltip> | 2384 | <PullRequestDetailTitleRow className="group gap-1"> |
| 2380 | <TooltipTrigger | 2385 | <Tooltip> |
| 2381 | render={ | 2386 | <TooltipTrigger |
| 2382 | <h1 className="min-w-0 flex-1 truncate text-base font-semibold leading-snug"> | 2387 | render={ |
| 2383 | {detail.title} | 2388 | <h1 className="min-w-0 flex-1 truncate text-base font-semibold leading-snug"> |
| 2384 | </h1> | 2389 | {detail.title} |
| 2385 | } | 2390 | </h1> |
| 2386 | /> | 2391 | } |
| 2387 | <TooltipPopup side="top">{detail.title}</TooltipPopup> | 2392 | /> |
| 2388 | </Tooltip> | 2393 | <TooltipPopup side="top">{detail.title}</TooltipPopup> |
| 2389 | {canEditPullRequestChangeRequest(detail) ? ( | 2394 | </Tooltip> |
| 2390 | <PullRequestEditButton | 2395 | {canEditPullRequestChangeRequest(detail) ? ( |
| 2391 | aria-label="Edit title" | 2396 | <PullRequestEditButton |
| 2392 | onClick={() => setTitleScope({ pullRequestKey, text: detail.title })} | 2397 | aria-label="Edit title" |
| 2393 | /> | 2398 | onClick={() => setTitleScope({ pullRequestKey, text: detail.title })} |
| 2394 | ) : null} | 2399 | /> |
| 2395 | </div> | 2400 | ) : null} |
| 2396 | ) : ( | 2401 | </PullRequestDetailTitleRow> |
| 2397 | // A title is one line of text, not markdown, so it takes an input rather than | 2402 | ) : ( |
| 2398 | // the editor the description and the remarks share. | 2403 | // A title is one line of text, not markdown, so it takes an input rather than |
| 2399 | <div className="space-y-2"> | 2404 | // the editor the description and the remarks share. |
| 2400 | <Input | 2405 | <div className="space-y-2"> |
| 2401 | autoFocus | 2406 | <Input |
| 2402 | size="sm" | 2407 | autoFocus |
| 2403 | disabled={titleSaving} | 2408 | size="sm" |
| 2404 | value={titleDraft} | | |
| 2405 | aria-label="Pull request title" | | |
| 2406 | onChange={(event) => | | |
| 2407 | setTitleScope({ pullRequestKey, text: event.target.value }) | | |
| 2408 | } | | |
| 2409 | onKeyDown={(event) => { | | |
| 2410 | if (event.key === "Enter") { | | |
| 2411 | event.preventDefault(); | | |
| 2412 | void saveTitle(titleDraft); | | |
| 2413 | } else if (event.key === "Escape") { | | |
| 2414 | event.preventDefault(); | | |
| 2415 | setTitleScope(null); | | |
| 2416 | } | | |
| 2417 | }} | | |
| 2418 | /> | | |
| 2419 | <div className="flex justify-end gap-2"> | | |
| 2420 | <Button | | |
| 2421 | size="xs" | | |
| 2422 | variant="ghost" | | |
| 2423 | disabled={titleSaving} | 2409 | disabled={titleSaving} |
| 2424 | onClick={() => setTitleScope(null)} | 2410 | value={titleDraft} |
| 2425 | > | 2411 | aria-label="Pull request title" |
| 2426 | Cancel | 2412 | onChange={(event) => |
| 2427 | </Button> | 2413 | setTitleScope({ pullRequestKey, text: event.target.value }) |
| 2428 | <Button | | |
| 2429 | size="xs" | | |
| 2430 | variant="outline" | | |
| 2431 | disabled={ | | |
| 2432 | !canWriteSourceControl || titleSaving || titleDraft.trim().length === 0 | | |
| 2433 | } | 2414 | } |
| 2434 | onClick={() => void saveTitle(titleDraft)} | 2415 | onKeyDown={(event) => { |
| 2435 | > | 2416 | if (event.key === "Enter") { |
| 2436 | {titleSaving ? "Saving..." : "Save"} | 2417 | event.preventDefault(); |
| 2437 | </Button> | 2418 | void saveTitle(titleDraft); |
| | 2419 | } else if (event.key === "Escape") { |
| | 2420 | event.preventDefault(); |
| | 2421 | setTitleScope(null); |
| | 2422 | } |
| | 2423 | }} |
| | 2424 | /> |
| | 2425 | <div className="flex justify-end gap-2"> |
| | 2426 | <Button |
| | 2427 | size="xs" |
| | 2428 | variant="ghost" |
| | 2429 | disabled={titleSaving} |
| | 2430 | onClick={() => setTitleScope(null)} |
| | 2431 | > |
| | 2432 | Cancel |
| | 2433 | </Button> |
| | 2434 | <Button |
| | 2435 | size="xs" |
| | 2436 | variant="outline" |
| | 2437 | disabled={ |
| | 2438 | !canWriteSourceControl || titleSaving || titleDraft.trim().length === 0 |
| | 2439 | } |
| | 2440 | onClick={() => void saveTitle(titleDraft)} |
| | 2441 | > |
| | 2442 | {titleSaving ? "Saving..." : "Save"} |
| | 2443 | </Button> |
| | 2444 | </div> |
| 2438 | </div> | 2445 | </div> |
| 2439 | </div> | 2446 | ) |
| 2440 | )} | 2447 | } |
| 2441 | <div className="mt-2 flex min-h-5 min-w-0 items-center gap-2 text-xs text-muted-foreground"> | 2448 | author={ |
| 2442 | <PullRequestMetaLine className="min-w-0 whitespace-nowrap"> | 2449 | <PullRequestActorLabel actor={detail.author} profileUrl={authorProfileUrl} /> |
| 2443 | <PullRequestActorLabel actor={detail.author} profileUrl={authorProfileUrl} /> | 2450 | } |
| 2444 | <span>updated {formatRelativeTimeLabel(detail.updatedAt)}</span> | 2451 | updated={<span>updated {formatRelativeTimeLabel(detail.updatedAt)}</span>} |
| 2445 | </PullRequestMetaLine> | 2452 | checkout={ |
| 2446 | {checkoutCommand ? ( | 2453 | checkoutCommand ? ( |
| 2447 | <PullRequestCopyableCode | 2454 | <PullRequestCopyableCode |
| 2448 | key={checkoutCommand} | 2455 | key={checkoutCommand} |
| 2449 | value={checkoutCommand} | 2456 | value={checkoutCommand} |
| ... | @@ -2454,159 +2461,127 @@ export function PullRequestDetailPanel({ | ... | @@ -2454,159 +2461,127 @@ export function PullRequestDetailPanel({ |
| 2454 | tooltipSide="bottom" | 2461 | tooltipSide="bottom" |
| 2455 | onError={onCheckoutCommandError} | 2462 | onError={onCheckoutCommandError} |
| 2456 | /> | 2463 | /> |
| 2457 | ) : null} | 2464 | ) : null |
| 2458 | </div> | 2465 | } |
| 2459 | | 2466 | base={ |
| 2460 | <div className="mt-4 flex min-h-5 min-w-0 items-center gap-2 text-xs text-muted-foreground"> | 2467 | /* An out-of-date base wears the warning on the branch name itself, so the |
| 2461 | <span className="flex min-w-0 flex-1 items-center gap-1.5 font-mono text-xs text-muted-foreground/70"> | | |
| 2462 | {/* An out-of-date base wears the warning on the branch name itself, so the | | |
| 2463 | name is amber and pointing at either the name or the mark opens the way | 2468 | name is amber and pointing at either the name or the mark opens the way |
| 2464 | out. Up to date, the name keeps its plain tooltip. */} | 2469 | out. Up to date, the name keeps its plain tooltip. */ |
| 2465 | {freshness ? ( | 2470 | freshness ? ( |
| 2466 | <PullRequestBaseFreshnessWarning | 2471 | <PullRequestBaseFreshnessWarning |
| 2467 | baseBranch={detail.baseBranch} | 2472 | baseBranch={detail.baseBranch} |
| 2468 | freshness={freshness} | 2473 | freshness={freshness} |
| 2469 | pending={actionPending} | 2474 | pending={actionPending} |
| 2470 | onUpdate={(method) => void perform("update-branch", undefined, method)} | 2475 | onUpdate={(method) => void perform("update-branch", undefined, method)} |
| 2471 | className="max-w-[40%]" | 2476 | className="max-w-[40%]" |
| 2472 | > | 2477 | > |
| 2473 | {isStackedPullRequest ? ( | 2478 | {isStackedPullRequest ? ( |
| 2474 | <PullRequestGlyph.stack | 2479 | <PullRequestGlyph.stack |
| 2475 | aria-label="Stacked pull request" | 2480 | aria-label="Stacked pull request" |
| 2476 | className="size-3 shrink-0" | 2481 | className="size-3 shrink-0" |
| 2477 | /> | | |
| 2478 | ) : null} | | |
| 2479 | <code className="flex min-w-0"> | | |
| 2480 | <MiddleTruncate value={detail.baseBranch} showTitle={false} /> | | |
| 2481 | </code> | | |
| 2482 | </PullRequestBaseFreshnessWarning> | | |
| 2483 | ) : ( | | |
| 2484 | <Tooltip> | | |
| 2485 | <TooltipTrigger | | |
| 2486 | render={ | | |
| 2487 | <span className="inline-flex min-w-0 max-w-[40%] shrink-0 items-center gap-1"> | | |
| 2488 | {isStackedPullRequest ? ( | | |
| 2489 | <PullRequestGlyph.stack | | |
| 2490 | aria-label="Stacked pull request" | | |
| 2491 | className="size-3 shrink-0" | | |
| 2492 | /> | | |
| 2493 | ) : null} | | |
| 2494 | <code className="flex min-w-0"> | | |
| 2495 | <MiddleTruncate value={detail.baseBranch} showTitle={false} /> | | |
| 2496 | </code> | | |
| 2497 | </span> | | |
| 2498 | } | | |
| 2499 | /> | 2482 | /> |
| 2500 | <TooltipPopup side="top"> | 2483 | ) : null} |
| 2501 | {isStackedPullRequest | 2484 | <code className="flex min-w-0"> |
| 2502 | ? `Stacked on ${detail.baseBranch}` | 2485 | <MiddleTruncate value={detail.baseBranch} showTitle={false} /> |
| 2503 | : detail.baseBranch} | 2486 | </code> |
| 2504 | </TooltipPopup> | 2487 | </PullRequestBaseFreshnessWarning> |
| 2505 | </Tooltip> | 2488 | ) : ( |
| 2506 | )} | 2489 | <Tooltip> |
| 2507 | <ArrowLeftIcon | 2490 | <TooltipTrigger |
| 2508 | aria-label="receives changes from" | 2491 | render={ |
| 2509 | className="size-3.5 shrink-0 opacity-60" | 2492 | <span className="inline-flex min-w-0 max-w-[40%] shrink-0 items-center gap-1"> |
| 2510 | /> | 2493 | {isStackedPullRequest ? ( |
| 2511 | <PullRequestCopyableCode | 2494 | <PullRequestGlyph.stack |
| 2512 | key={detail.headBranch} | 2495 | aria-label="Stacked pull request" |
| 2513 | value={detail.headBranch} | 2496 | className="size-3 shrink-0" |
| 2514 | target="branch name" | 2497 | /> |
| 2515 | copyLabel="Copy pull request branch" | 2498 | ) : null} |
| 2516 | copiedLabel="Branch name copied" | 2499 | <code className="flex min-w-0"> |
| 2517 | /> | 2500 | <MiddleTruncate value={detail.baseBranch} showTitle={false} /> |
| 2518 | </span> | 2501 | </code> |
| 2519 | <span className="ml-auto inline-flex shrink-0 items-center justify-end gap-2"> | 2502 | </span> |
| 2520 | <span className="inline-flex min-w-16 items-center justify-end gap-1.5 tabular-nums"> | 2503 | } |
| 2521 | <FileDiffIcon className="size-3.5" /> | 2504 | /> |
| 2522 | {detail.changedFiles.toLocaleString()}{" "} | 2505 | <TooltipPopup side="top"> |
| 2523 | {detail.changedFiles === 1 ? "file" : "files"} | 2506 | {isStackedPullRequest |
| 2524 | </span> | 2507 | ? `Stacked on ${detail.baseBranch}` |
| 2525 | <PullRequestDiffStat | 2508 | : detail.baseBranch} |
| 2526 | additions={detail.additions} | 2509 | </TooltipPopup> |
| 2527 | deletions={detail.deletions} | 2510 | </Tooltip> |
| 2528 | className="shrink-0 font-mono text-xs" | 2511 | ) |
| 2529 | /> | 2512 | } |
| 2530 | </span> | 2513 | head={ |
| 2531 | </div> | 2514 | <PullRequestCopyableCode |
| 2532 | </div> | 2515 | key={detail.headBranch} |
| | 2516 | value={detail.headBranch} |
| | 2517 | target="branch name" |
| | 2518 | copyLabel="Copy pull request branch" |
| | 2519 | copiedLabel="Branch name copied" |
| | 2520 | /> |
| | 2521 | } |
| | 2522 | files={`${detail.changedFiles.toLocaleString()} ${detail.changedFiles === 1 ? "file" : "files"}`} |
| | 2523 | diffStat={ |
| | 2524 | <PullRequestDiffStat |
| | 2525 | additions={detail.additions} |
| | 2526 | deletions={detail.deletions} |
| | 2527 | className="shrink-0 font-mono text-xs" |
| | 2528 | /> |
| | 2529 | } |
| | 2530 | /> |
| 2533 | ) : null} | 2531 | ) : null} |
| 2534 | </div> | 2532 | </div> |
| 2535 | </div> | 2533 | </div> |
| 2536 | | 2534 | |
| 2537 | {detail ? ( | 2535 | {detail ? ( |
| 2538 | <nav | 2536 | <PullRequestDetailTabBar |
| 2539 | className="col-span-2 flex min-w-0 flex-wrap items-center gap-2 border-t border-border/60 px-4 py-2" | 2537 | tabs={visibleTabs} |
| 2540 | aria-label="Pull request tabs" | 2538 | value={tab} |
| | 2539 | onValueChange={setTab} |
| | 2540 | onTabIntent={(item) => { |
| | 2541 | if (item === "code") void loadCodeTab(); |
| | 2542 | }} |
| 2541 | > | 2543 | > |
| 2542 | <ToggleGroup | | |
| 2543 | className="shrink-0" | | |
| 2544 | size="segmented" | | |
| 2545 | variant="segmented" | | |
| 2546 | value={[tab]} | | |
| 2547 | onValueChange={(next) => { | | |
| 2548 | const nextTab = visibleTabs.find((item) => item.value === next[0])?.value; | | |
| 2549 | if (nextTab) setTab(nextTab); | | |
| 2550 | }} | | |
| 2551 | > | | |
| 2552 | {visibleTabs.map((item) => ( | | |
| 2553 | <Toggle | | |
| 2554 | key={item.value} | | |
| 2555 | value={item.value} | | |
| 2556 | onPointerEnter={item.value === "code" ? () => void loadCodeTab() : undefined} | | |
| 2557 | onFocus={item.value === "code" ? () => void loadCodeTab() : undefined} | | |
| 2558 | > | | |
| 2559 | {item.label} | | |
| 2560 | </Toggle> | | |
| 2561 | ))} | | |
| 2562 | </ToggleGroup> | | |
| 2563 | {tab === "summary" ? ( | 2544 | {tab === "summary" ? ( |
| 2564 | <span | 2545 | showsApproveWorkflows ? ( |
| 2565 | className={cn( | 2546 | <Tooltip> |
| 2566 | "ml-auto flex items-center justify-end", | 2547 | <TooltipTrigger |
| 2567 | showsApproveWorkflows ? "shrink-0" : "min-w-0 flex-1", | 2548 | render={ |
| 2568 | )} | 2549 | <span className="ml-auto inline-flex shrink-0"> |
| 2569 | > | 2550 | <Button |
| 2570 | {showsApproveWorkflows ? ( | 2551 | size="xs" |
| 2571 | <Tooltip> | 2552 | variant="warning-outline" |
| 2572 | <TooltipTrigger | 2553 | disabled={actionPending} |
| 2573 | render={ | 2554 | onClick={() => |
| 2574 | <span className="inline-flex shrink-0"> | 2555 | setConfirmation({ open: true, action: "approve-workflows" }) |
| 2575 | <Button | 2556 | } |
| 2576 | size="xs" | 2557 | aria-label={ |
| 2577 | variant="warning-outline" | 2558 | pendingAction === "approve-workflows" |
| 2578 | disabled={actionPending} | 2559 | ? "Approving..." |
| 2579 | onClick={() => | 2560 | : "Approve workflows to run" |
| 2580 | setConfirmation({ open: true, action: "approve-workflows" }) | 2561 | } |
| 2581 | } | 2562 | > |
| 2582 | aria-label={ | 2563 | <PlayIcon aria-hidden className="size-3.5" /> |
| 2583 | pendingAction === "approve-workflows" | 2564 | <span> |
| 2584 | ? "Approving..." | 2565 | {pendingAction === "approve-workflows" |
| 2585 | : "Approve workflows to run" | 2566 | ? "Approving..." |
| 2586 | } | 2567 | : "Approve workflows to run"} |
| 2587 | > | 2568 | </span> |
| 2588 | <PlayIcon aria-hidden className="size-3.5" /> | 2569 | </Button> |
| 2589 | <span> | 2570 | </span> |
| 2590 | {pendingAction === "approve-workflows" | 2571 | } |
| 2591 | ? "Approving..." | 2572 | /> |
| 2592 | : "Approve workflows to run"} | 2573 | <TooltipPopup side="top"> |
| 2593 | </span> | 2574 | {pendingAction === "approve-workflows" |
| 2594 | </Button> | 2575 | ? "Approving..." |
| 2595 | </span> | 2576 | : "Approve workflows to run"} |
| 2596 | } | 2577 | </TooltipPopup> |
| 2597 | /> | 2578 | </Tooltip> |
| 2598 | <TooltipPopup side="top"> | 2579 | ) : ( |
| 2599 | {pendingAction === "approve-workflows" | 2580 | <PullRequestChecksStatusLine |
| 2600 | ? "Approving..." | 2581 | className="text-muted-foreground" |
| 2601 | : "Approve workflows to run"} | 2582 | aria-label={checksSummary ? `Checks: ${checksSummary}` : "Checks"} |
| 2602 | </TooltipPopup> | 2583 | icon={ |
| 2603 | </Tooltip> | 2584 | checksState !== null ? ( |
| 2604 | ) : ( | | |
| 2605 | <span | | |
| 2606 | className="flex h-4 min-w-0 flex-wrap content-start items-center justify-end gap-x-1.5 overflow-hidden text-xs text-muted-foreground" | | |
| 2607 | aria-label={checksSummary ? `Checks: ${checksSummary}` : "Checks"} | | |
| 2608 | > | | |
| 2609 | {checksState !== null ? ( | | |
| 2610 | <PullRequestChecksPopover | 2585 | <PullRequestChecksPopover |
| 2611 | checks={detail.checks} | 2586 | checks={detail.checks} |
| 2612 | stale={checksStale} | 2587 | stale={checksStale} |
| ... | @@ -2615,11 +2590,11 @@ export function PullRequestDetailPanel({ | ... | @@ -2615,11 +2590,11 @@ export function PullRequestDetailPanel({ |
| 2615 | /> | 2590 | /> |
| 2616 | ) : ( | 2591 | ) : ( |
| 2617 | <CircleDotIcon aria-hidden className="size-3.5" /> | 2592 | <CircleDotIcon aria-hidden className="size-3.5" /> |
| 2618 | )} | 2593 | ) |
| 2619 | <span className="whitespace-nowrap">{checksSummary}</span> | 2594 | } |
| 2620 | </span> | 2595 | label={checksSummary} |
| 2621 | )} | 2596 | /> |
| 2622 | </span> | 2597 | ) |
| 2623 | ) : tab === "timeline" ? ( | 2598 | ) : tab === "timeline" ? ( |
| 2624 | <div className="ml-auto flex shrink-0 items-center gap-2 text-xs text-muted-foreground"> | 2599 | <div className="ml-auto flex shrink-0 items-center gap-2 text-xs text-muted-foreground"> |
| 2625 | <PullRequestMetaLine | 2600 | <PullRequestMetaLine |
| ... | @@ -2694,7 +2669,7 @@ export function PullRequestDetailPanel({ | ... | @@ -2694,7 +2669,7 @@ export function PullRequestDetailPanel({ |
| 2694 | </Button> | 2669 | </Button> |
| 2695 | </div> | 2670 | </div> |
| 2696 | ) : null} | 2671 | ) : null} |
| 2697 | </nav> | 2672 | </PullRequestDetailTabBar> |
| 2698 | ) : null} | 2673 | ) : null} |
| 2699 | </div> | 2674 | </div> |
| 2700 | | 2675 | |