| ... | ... | @@ -88,15 +88,6 @@ import { vcsEnvironment } from "~/state/vcs"; |
| 88 | 88 | import { formatRelativeTimeLabel } from "~/timestampFormat"; |
| 89 | 89 | import { useUiStateStore } from "~/uiStateStore"; |
| 90 | 90 | |
| 91 | | import { |
| 92 | | AlertDialog, |
| 93 | | AlertDialogClose, |
| 94 | | AlertDialogDescription, |
| 95 | | AlertDialogFooter, |
| 96 | | AlertDialogHeader, |
| 97 | | AlertDialogPopup, |
| 98 | | AlertDialogTitle, |
| 99 | | } from "../ui/alert-dialog"; |
| 100 | 91 | import { EnvironmentMachineIcon } from "../EnvironmentMachineIcon"; |
| 101 | 92 | import { Badge } from "../ui/badge"; |
| 102 | 93 | import { Button } from "../ui/button"; |
| ... | ... | @@ -183,25 +174,10 @@ import { |
| 183 | 174 | summarizePullRequestChecks, |
| 184 | 175 | } from "./pullRequestPresentation"; |
| 185 | 176 | import { PullRequestGlyph } from "./pullRequestIcons"; |
| 177 | import { useInlineActionConfirm } from "./useInlineActionConfirm"; |
| 186 | 178 | |
| 187 | 179 | type DetailTab = "summary" | "timeline" | "code"; |
| 188 | 180 | |
| 189 | | const ACTION_SUCCESS_LABELS: Record<PullRequestAction, string> = { |
| 190 | | merge: "Pull request merged", |
| 191 | | ready: "Marked ready for review", |
| 192 | | draft: "Converted to draft", |
| 193 | | close: "Pull request closed", |
| 194 | | reopen: "Pull request reopened", |
| 195 | | "update-branch": "Branch updated with the base branch", |
| 196 | | // True whichever it did: a pull request that was already mergeable merges the moment this is |
| 197 | | // armed, and the client has no way to tell that apart from one still waiting on something. |
| 198 | | "enable-auto-merge": |
| 199 | | "Auto-merge turned on — merges as soon as this is ready, sooner if it already is", |
| 200 | | "disable-auto-merge": "Auto-merge turned off", |
| 201 | | revert: "Revert pull request opened", |
| 202 | | "approve-workflows": "Workflows approved", |
| 203 | | }; |
| 204 | | |
| 205 | 181 | /** Said as the thing that did not happen, rather than as the operation that returned an error. */ |
| 206 | 182 | const ACTION_FAILURE_LABELS: Record<PullRequestAction, string> = { |
| 207 | 183 | merge: "Could not merge this pull request", |
| ... | ... | @@ -578,11 +554,7 @@ export function PullRequestDetailPanel({ |
| 578 | 554 | const setMergeMethod = (method: PullRequestMergeMethod) => { |
| 579 | 555 | setMergeMethodSelection({ pullRequestKey, method }); |
| 580 | 556 | }; |
| 581 | | const [confirmation, setConfirmation] = useState<{ |
| 582 | | readonly open: boolean; |
| 583 | | readonly action: "merge" | "close" | "enable-auto-merge" | "revert" | "approve-workflows"; |
| 584 | | }>({ open: false, action: "merge" }); |
| 585 | | const confirmAction = confirmation.action; |
| 557 | const inlineConfirm = useInlineActionConfirm(pullRequestKey); |
| 586 | 558 | // Which handoff is preparing, keyed so a per-finding button can say "Preparing..." on itself |
| 587 | 559 | // alone. One at a time whatever the key: they all check the same pull request out. |
| 588 | 560 | const [handoff, setHandoff] = useState<string | null>(null); |
| ... | ... | @@ -986,7 +958,10 @@ export function PullRequestDetailPanel({ |
| 986 | 958 | onActed?.(action, "failed"); |
| 987 | 959 | return false; |
| 988 | 960 | } |
| 989 | | toastManager.add({ type: "success", title: ACTION_SUCCESS_LABELS[action] }); |
| 961 | // Reverting opens another PR; the current panel has no new state to show. |
| 962 | if (action === "revert") { |
| 963 | toastManager.add({ type: "success", title: "Revert pull request opened" }); |
| 964 | } |
| 990 | 965 | // A branch update moves the head commit, which leaves the diff atom pointed at a comparison |
| 991 | 966 | // that no longer exists — the same staleness the manual refresh button fixes, so it goes |
| 992 | 967 | // through that path rather than a second one. Every other action here only changes metadata; |
| ... | ... | @@ -1918,27 +1893,44 @@ export function PullRequestDetailPanel({ |
| 1918 | 1893 | size="xs" |
| 1919 | 1894 | variant="default" |
| 1920 | 1895 | disabled={actionPending} |
| 1921 | | onClick={() => |
| 1922 | | setConfirmation({ open: true, action: "enable-auto-merge" }) |
| 1923 | | } |
| 1896 | {...inlineConfirm.props( |
| 1897 | "enable-auto-merge", |
| 1898 | () => void perform("enable-auto-merge", selectedMergeMethod), |
| 1899 | )} |
| 1924 | 1900 | aria-label={ |
| 1925 | 1901 | pendingAction === "enable-auto-merge" |
| 1926 | 1902 | ? "Enabling..." |
| 1927 | | : pendingAutoMergeLabel |
| 1903 | : inlineConfirm.isArmed("enable-auto-merge") |
| 1904 | ? "Confirm auto-merge?" |
| 1905 | : pendingAutoMergeLabel |
| 1928 | 1906 | } |
| 1929 | 1907 | > |
| 1930 | 1908 | <PullRequestGlyph.merged aria-hidden className="size-3.5" /> |
| 1931 | | <span className="@max-[30rem]/pr-header:hidden"> |
| 1932 | | {pendingAction === "enable-auto-merge" |
| 1933 | | ? "Enabling..." |
| 1934 | | : pendingAutoMergeLabel} |
| 1909 | <span className="inline-grid @max-[30rem]/pr-header:hidden"> |
| 1910 | <span aria-hidden className="invisible col-start-1 row-start-1"> |
| 1911 | {pendingAutoMergeLabel} |
| 1912 | </span> |
| 1913 | <span aria-hidden className="invisible col-start-1 row-start-1"> |
| 1914 | Confirm auto-merge? |
| 1915 | </span> |
| 1916 | <span className="col-start-1 row-start-1"> |
| 1917 | {pendingAction === "enable-auto-merge" |
| 1918 | ? "Enabling..." |
| 1919 | : inlineConfirm.isArmed("enable-auto-merge") |
| 1920 | ? "Confirm auto-merge?" |
| 1921 | : pendingAutoMergeLabel} |
| 1922 | </span> |
| 1935 | 1923 | </span> |
| 1936 | 1924 | </Button> |
| 1937 | 1925 | </span> |
| 1938 | 1926 | } |
| 1939 | 1927 | /> |
| 1940 | 1928 | <TooltipPopup side="top"> |
| 1941 | | {pendingAction === "enable-auto-merge" ? "Enabling..." : pendingAutoMergeLabel} |
| 1929 | {pendingAction === "enable-auto-merge" |
| 1930 | ? "Enabling..." |
| 1931 | : inlineConfirm.isArmed("enable-auto-merge") |
| 1932 | ? "Confirm auto-merge?" |
| 1933 | : pendingAutoMergeLabel} |
| 1942 | 1934 | </TooltipPopup> |
| 1943 | 1935 | </Tooltip> |
| 1944 | 1936 | ) : primaryAction === "auto-merge-armed" ? ( |
| ... | ... | @@ -1970,21 +1962,44 @@ export function PullRequestDetailPanel({ |
| 1970 | 1962 | size="xs" |
| 1971 | 1963 | variant="default" |
| 1972 | 1964 | disabled={actionPending} |
| 1973 | | onClick={() => setConfirmation({ open: true, action: "merge" })} |
| 1965 | {...inlineConfirm.props( |
| 1966 | "merge", |
| 1967 | () => void perform("merge", selectedMergeMethod), |
| 1968 | )} |
| 1974 | 1969 | aria-label={ |
| 1975 | | pendingAction === "merge" ? "Merging..." : selectedMergeMethodLabel |
| 1970 | pendingAction === "merge" |
| 1971 | ? "Merging..." |
| 1972 | : inlineConfirm.isArmed("merge") |
| 1973 | ? "Confirm merge?" |
| 1974 | : selectedMergeMethodLabel |
| 1976 | 1975 | } |
| 1977 | 1976 | > |
| 1978 | 1977 | <PullRequestGlyph.merged aria-hidden className="size-3.5" /> |
| 1979 | | <span className="@max-[30rem]/pr-header:hidden"> |
| 1980 | | {pendingAction === "merge" ? "Merging..." : selectedMergeMethodLabel} |
| 1978 | <span className="inline-grid @max-[30rem]/pr-header:hidden"> |
| 1979 | <span aria-hidden className="invisible col-start-1 row-start-1"> |
| 1980 | {selectedMergeMethodLabel} |
| 1981 | </span> |
| 1982 | <span aria-hidden className="invisible col-start-1 row-start-1"> |
| 1983 | Confirm merge? |
| 1984 | </span> |
| 1985 | <span className="col-start-1 row-start-1"> |
| 1986 | {pendingAction === "merge" |
| 1987 | ? "Merging..." |
| 1988 | : inlineConfirm.isArmed("merge") |
| 1989 | ? "Confirm merge?" |
| 1990 | : selectedMergeMethodLabel} |
| 1991 | </span> |
| 1981 | 1992 | </span> |
| 1982 | 1993 | </Button> |
| 1983 | 1994 | </span> |
| 1984 | 1995 | } |
| 1985 | 1996 | /> |
| 1986 | 1997 | <TooltipPopup side="top"> |
| 1987 | | {pendingAction === "merge" ? "Merging..." : selectedMergeMethodLabel} |
| 1998 | {pendingAction === "merge" |
| 1999 | ? "Merging..." |
| 2000 | : inlineConfirm.isArmed("merge") |
| 2001 | ? "Confirm merge?" |
| 2002 | : selectedMergeMethodLabel} |
| 1988 | 2003 | </TooltipPopup> |
| 1989 | 2004 | </Tooltip> |
| 1990 | 2005 | ) : (primaryAction === "merged" || primaryAction === "closed") && |
| ... | ... | @@ -2101,11 +2116,18 @@ export function PullRequestDetailPanel({ |
| 2101 | 2116 | ) : null} |
| 2102 | 2117 | {showsMergeNow ? ( |
| 2103 | 2118 | <MenuItem |
| 2119 | closeOnClick={inlineConfirm.isArmed("merge")} |
| 2120 | aria-label={ |
| 2121 | inlineConfirm.isArmed("merge") ? "Confirm merge?" : "Merge now" |
| 2122 | } |
| 2104 | 2123 | disabled={actionPending} |
| 2105 | | onClick={() => setConfirmation({ open: true, action: "merge" })} |
| 2124 | {...inlineConfirm.props( |
| 2125 | "merge", |
| 2126 | () => void perform("merge", selectedMergeMethod), |
| 2127 | )} |
| 2106 | 2128 | > |
| 2107 | 2129 | <PullRequestGlyph.merged className="size-3.5" /> |
| 2108 | | Merge now |
| 2130 | {inlineConfirm.isArmed("merge") ? "Confirm merge?" : "Merge now"} |
| 2109 | 2131 | </MenuItem> |
| 2110 | 2132 | ) : null} |
| 2111 | 2133 | {/* The same merge, left with the host to carry out once its requirements |
| ... | ... | @@ -2121,13 +2143,22 @@ export function PullRequestDetailPanel({ |
| 2121 | 2143 | </MenuItem> |
| 2122 | 2144 | ) : showsAutoMerge ? ( |
| 2123 | 2145 | <MenuItem |
| 2124 | | disabled={actionPending} |
| 2125 | | onClick={() => |
| 2126 | | setConfirmation({ open: true, action: "enable-auto-merge" }) |
| 2146 | closeOnClick={inlineConfirm.isArmed("enable-auto-merge")} |
| 2147 | aria-label={ |
| 2148 | inlineConfirm.isArmed("enable-auto-merge") |
| 2149 | ? "Confirm auto-merge?" |
| 2150 | : "Enable auto-merge" |
| 2127 | 2151 | } |
| 2152 | disabled={actionPending} |
| 2153 | {...inlineConfirm.props( |
| 2154 | "enable-auto-merge", |
| 2155 | () => void perform("enable-auto-merge", selectedMergeMethod), |
| 2156 | )} |
| 2128 | 2157 | > |
| 2129 | 2158 | <PullRequestGlyph.merged className="size-3.5" /> |
| 2130 | | Enable auto-merge |
| 2159 | {inlineConfirm.isArmed("enable-auto-merge") |
| 2160 | ? "Confirm auto-merge?" |
| 2161 | : "Enable auto-merge"} |
| 2131 | 2162 | </MenuItem> |
| 2132 | 2163 | ) : null} |
| 2133 | 2164 | {/* A preference for the merge action rather than a second action, so it |
| ... | ... | @@ -2200,12 +2231,16 @@ export function PullRequestDetailPanel({ |
| 2200 | 2231 | <> |
| 2201 | 2232 | <MenuSeparator /> |
| 2202 | 2233 | <MenuItem |
| 2234 | closeOnClick={inlineConfirm.isArmed("close")} |
| 2235 | aria-label={ |
| 2236 | inlineConfirm.isArmed("close") ? "Confirm close?" : "Close pull request" |
| 2237 | } |
| 2203 | 2238 | variant="destructive" |
| 2204 | 2239 | disabled={actionPending} |
| 2205 | | onClick={() => setConfirmation({ open: true, action: "close" })} |
| 2240 | {...inlineConfirm.props("close", () => void perform("close"))} |
| 2206 | 2241 | > |
| 2207 | 2242 | <PullRequestGlyph.closed className="size-3.5" /> |
| 2208 | | Close pull request |
| 2243 | {inlineConfirm.isArmed("close") ? "Confirm close?" : "Close pull request"} |
| 2209 | 2244 | </MenuItem> |
| 2210 | 2245 | </> |
| 2211 | 2246 | ) : detail.state === "closed" && can("reopen") ? ( |
| ... | ... | @@ -2220,11 +2255,16 @@ export function PullRequestDetailPanel({ |
| 2220 | 2255 | <> |
| 2221 | 2256 | <MenuSeparator /> |
| 2222 | 2257 | <MenuItem |
| 2258 | closeOnClick={inlineConfirm.isArmed("revert")} |
| 2259 | aria-label={ |
| 2260 | inlineConfirm.isArmed("revert") ? "Confirm revert?" : "Revert changes" |
| 2261 | } |
| 2262 | variant={inlineConfirm.isArmed("revert") ? "destructive" : "default"} |
| 2223 | 2263 | disabled={actionPending} |
| 2224 | | onClick={() => setConfirmation({ open: true, action: "revert" })} |
| 2264 | {...inlineConfirm.props("revert", () => void perform("revert"))} |
| 2225 | 2265 | > |
| 2226 | 2266 | <RotateCcwIcon className="size-3.5" /> |
| 2227 | | Revert changes |
| 2267 | {inlineConfirm.isArmed("revert") ? "Confirm revert?" : "Revert changes"} |
| 2228 | 2268 | </MenuItem> |
| 2229 | 2269 | </> |
| 2230 | 2270 | ) : null} |
| ... | ... | @@ -2553,20 +2593,33 @@ export function PullRequestDetailPanel({ |
| 2553 | 2593 | size="xs" |
| 2554 | 2594 | variant="warning-outline" |
| 2555 | 2595 | disabled={actionPending} |
| 2556 | | onClick={() => |
| 2557 | | setConfirmation({ open: true, action: "approve-workflows" }) |
| 2558 | | } |
| 2596 | {...inlineConfirm.props( |
| 2597 | "approve-workflows", |
| 2598 | () => void perform("approve-workflows"), |
| 2599 | )} |
| 2559 | 2600 | aria-label={ |
| 2560 | 2601 | pendingAction === "approve-workflows" |
| 2561 | 2602 | ? "Approving..." |
| 2562 | | : "Approve workflows to run" |
| 2603 | : inlineConfirm.isArmed("approve-workflows") |
| 2604 | ? "Confirm approve?" |
| 2605 | : "Approve workflows to run" |
| 2563 | 2606 | } |
| 2564 | 2607 | > |
| 2565 | 2608 | <PlayIcon aria-hidden className="size-3.5" /> |
| 2566 | | <span> |
| 2567 | | {pendingAction === "approve-workflows" |
| 2568 | | ? "Approving..." |
| 2569 | | : "Approve workflows to run"} |
| 2609 | <span className="inline-grid"> |
| 2610 | <span aria-hidden className="invisible col-start-1 row-start-1"> |
| 2611 | Approve workflows to run |
| 2612 | </span> |
| 2613 | <span aria-hidden className="invisible col-start-1 row-start-1"> |
| 2614 | Confirm approve? |
| 2615 | </span> |
| 2616 | <span className="col-start-1 row-start-1"> |
| 2617 | {pendingAction === "approve-workflows" |
| 2618 | ? "Approving..." |
| 2619 | : inlineConfirm.isArmed("approve-workflows") |
| 2620 | ? "Confirm approve?" |
| 2621 | : "Approve workflows to run"} |
| 2622 | </span> |
| 2570 | 2623 | </span> |
| 2571 | 2624 | </Button> |
| 2572 | 2625 | </span> |
| ... | ... | @@ -2575,7 +2628,9 @@ export function PullRequestDetailPanel({ |
| 2575 | 2628 | <TooltipPopup side="top"> |
| 2576 | 2629 | {pendingAction === "approve-workflows" |
| 2577 | 2630 | ? "Approving..." |
| 2578 | | : "Approve workflows to run"} |
| 2631 | : inlineConfirm.isArmed("approve-workflows") |
| 2632 | ? "Confirm approve?" |
| 2633 | : "Approve workflows to run"} |
| 2579 | 2634 | </TooltipPopup> |
| 2580 | 2635 | </Tooltip> |
| 2581 | 2636 | ) : ( |
| ... | ... | @@ -2802,74 +2857,6 @@ export function PullRequestDetailPanel({ |
| 2802 | 2857 | /> |
| 2803 | 2858 | </div> |
| 2804 | 2859 | ) : null} |
| 2805 | | |
| 2806 | | <AlertDialog |
| 2807 | | open={confirmation.open} |
| 2808 | | onOpenChange={(open) => setConfirmation((current) => ({ ...current, open }))} |
| 2809 | | onOpenChangeComplete={(open) => { |
| 2810 | | if (!open) setConfirmation({ open: false, action: "merge" }); |
| 2811 | | }} |
| 2812 | | > |
| 2813 | | <AlertDialogPopup> |
| 2814 | | <AlertDialogHeader> |
| 2815 | | <AlertDialogTitle> |
| 2816 | | {confirmAction === "merge" |
| 2817 | | ? "Merge pull request?" |
| 2818 | | : confirmAction === "enable-auto-merge" |
| 2819 | | ? "Enable auto-merge?" |
| 2820 | | : confirmAction === "revert" |
| 2821 | | ? "Revert these changes?" |
| 2822 | | : confirmAction === "approve-workflows" |
| 2823 | | ? "Approve workflows to run?" |
| 2824 | | : "Close pull request?"} |
| 2825 | | </AlertDialogTitle> |
| 2826 | | <AlertDialogDescription> |
| 2827 | | {confirmAction === "merge" |
| 2828 | | ? `This merges #${reference.number} using ${selectedMergeMethod}.` |
| 2829 | | : confirmAction === "enable-auto-merge" |
| 2830 | | ? // The host merges this as soon as it considers the pull request ready, which |
| 2831 | | // may be immediately — there is no telling from here whether anything is |
| 2832 | | // still outstanding. |
| 2833 | | `This merges #${reference.number} using ${selectedMergeMethod} as soon as the host considers it ready, which may be immediately.` |
| 2834 | | : confirmAction === "revert" |
| 2835 | | ? `This opens a new pull request that reverses the changes merged by #${reference.number}.` |
| 2836 | | : confirmAction === "approve-workflows" |
| 2837 | | ? `This allows ${workflowApprovalsRequired} ${workflowApprovalsRequired === 1 ? "workflow" : "workflows"} from #${reference.number} to run. Review the code and workflow changes first.` |
| 2838 | | : `This closes #${reference.number} without merging it.`} |
| 2839 | | </AlertDialogDescription> |
| 2840 | | </AlertDialogHeader> |
| 2841 | | <AlertDialogFooter> |
| 2842 | | <AlertDialogClose render={<Button variant="outline" size="sm" />}> |
| 2843 | | Cancel |
| 2844 | | </AlertDialogClose> |
| 2845 | | <Button |
| 2846 | | size="sm" |
| 2847 | | variant={confirmAction === "close" ? "destructive" : "default"} |
| 2848 | | disabled={actionPending} |
| 2849 | | onClick={() => { |
| 2850 | | const action = confirmAction; |
| 2851 | | setConfirmation((current) => ({ ...current, open: false })); |
| 2852 | | if (action === "merge") void perform("merge", selectedMergeMethod); |
| 2853 | | if (action === "enable-auto-merge") |
| 2854 | | void perform("enable-auto-merge", selectedMergeMethod); |
| 2855 | | if (action === "revert") void perform("revert"); |
| 2856 | | if (action === "approve-workflows") void perform("approve-workflows"); |
| 2857 | | if (action === "close") void perform("close"); |
| 2858 | | }} |
| 2859 | | > |
| 2860 | | {confirmAction === "merge" |
| 2861 | | ? selectedMergeMethodLabel |
| 2862 | | : confirmAction === "enable-auto-merge" |
| 2863 | | ? "Enable auto-merge" |
| 2864 | | : confirmAction === "revert" |
| 2865 | | ? "Create revert PR" |
| 2866 | | : confirmAction === "approve-workflows" |
| 2867 | | ? "Approve and run" |
| 2868 | | : "Close"} |
| 2869 | | </Button> |
| 2870 | | </AlertDialogFooter> |
| 2871 | | </AlertDialogPopup> |
| 2872 | | </AlertDialog> |
| 2873 | 2860 | </div> |
| 2874 | 2861 | ); |
| 2875 | 2862 | } |