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