/** * The review flow buttons (`GHPRDetailsActionsComponentFactory`): nothing once merged, * "Reopen Pull Request" when closed, "Post Pull Request" for a draft; an open pull request * gets its role's buttons — Request / Re-request Review…, Submit Review…, Set Myself as * Reviewer, the Merge split button — and the More (⋯) menu. Every action is disabled while * another one runs; managing needs triage access or authorship, merging the rules of * `pullRequestPermissions`. */ import { useRef, useState, type RefObject } from "react"; import { Button, Icon, IconButton, Spinner, showContextMenu, type MenuItem } from "@/ui"; import { useTx } from "@/platform/i18n"; import type { GHPRMergeabilityState, GHPullRequest, GHPullRequestRequestedReviewer } from "../api/types"; import * as ops from "../data/operations"; import { isTeam, pullRequestPermissions, reviewerKey, type ReviewerWithState } from "../data/model"; import { useGhRepository, usePotentialReviewers } from "../data/repository"; import { usePullRequestData } from "../data/pullRequest"; import { ChooserPopup } from "../ui/ChooserPopup"; import { adjustReviewers, merge, reRequestReview, runFlow, useFlowBusy } from "./flow"; import { dedupe, MERGE_METHOD_LABEL, mergeButtonMethods, openedActions, reviewRequestState, reviewRole, type MergeMethod, type ReviewState, } from "./model"; import { showSubmitReview } from "./SubmitReviewPopup"; import styles from "./Details.module.css"; export function ReviewActions(props: { pr: GHPullRequest; mergeability: GHPRMergeabilityState | undefined; reviewers: readonly ReviewerWithState[]; reviewState: ReviewState; /** The Submit Review… button, for the `submitReview` handler to anchor its popup. */ submitRef: RefObject; }) { const { pr, mergeability, reviewers, reviewState, submitRef } = props; const tx = useTx(); const n = pr.number; const connection = useGhRepository((s) => s.connection); const progress = useFlowBusy((s) => s.busy[n]); const busy = progress !== undefined; const pending = usePullRequestData(n, "pendingReview").value; const more = useRef(null); const [picker, setPicker] = useState(null); if (!connection) return null; const me = connection.currentUser; const repo = connection.repository; const perms = pullRequestPermissions(pr, repo, mergeability ?? null); const canManage = perms.canManage && !busy; const enabled: Record = { merge: perms.canMerge && !busy, squash: perms.canSquashMerge && !busy, rebase: perms.canRebaseMerge && !busy, }; const requested = pr.reviewRequests.nodes.flatMap((r) => (r.requestedReviewer ? [r.requestedReviewer] : [])); const requestState = reviewRequestState(pr); const progressText = progress ? ( {progress} ) : null; if (requestState === "MERGED") return null; if (requestState === "CLOSED") return (
{progressText}
); if (requestState === "DRAFT") return (
{progressText}
); const role = reviewRole(pr, me); const myState = reviewers.find((r) => !isTeam(r.reviewer) && r.reviewer.id === me.id)?.state ?? null; const shown = openedActions(role, reviewState, requested.length, myState); const comments = pending?.commentsCount ?? 0; const methods = mergeButtonMethods(repo); const mergeItems = (): MenuItem[] => (["merge", "squash", "rebase"] as const).map((m) => ({ label: MERGE_METHOD_LABEL[m], disabled: !enabled[m], run: () => void merge(n, m), })); const showMore = () => { const r = more.current?.getBoundingClientRect(); if (!r) return; const items: MenuItem[] = shown.more.map((item): MenuItem => { if (item === "requestReview") return { label: "Request Review…", disabled: !canManage, run: () => setPicker(more.current) }; if (item === "mergeGroup") return { label: "Merge Review…", submenu: mergeItems() }; return { label: "Close Pull Request", disabled: !canManage, run: () => void runFlow(n, () => ops.closePullRequest(n)), }; }); showContextMenu({ x: r.left, y: r.bottom + 8 }, items); }; return (
{shown.requestReview && ( )} {shown.reRequestReview && ( )} {shown.submitReview && ( )} {shown.setMyselfAsReviewer && ( )} {shown.merge && ( )} {progressText} {picker && ( r.reviewer)])} onClose={(chosen) => { setPicker(null); void adjustReviewers(n, dedupe([...requested, ...reviewers.map((r) => r.reviewer)]), chosen); }} /> )}
); } /** `showMultipleChooserPopupWithIncrementalLoading` of the potential reviewers (the author excluded), above the button. */ function ReviewerPicker(props: { anchor: Element; pr: GHPullRequest; current: GHPullRequestRequestedReviewer[]; onClose: (chosen: GHPullRequestRequestedReviewer[]) => void; }) { const tx = useTx(); const candidates = usePotentialReviewers(); const authorId = props.pr.author?.id; const items = candidates.items.filter((r) => isTeam(r) || r.id !== authorId); return ( isTeam(r) ? { text: r.combinedSlug, detail: r.teamName, avatarUrl: r.teamAvatarUrl } : { text: r.login, detail: r.name, avatarUrl: r.avatarUrl } } onClose={props.onClose} aria-label={tx("Reviewers")} /> ); }