/**
* The review form floated over the Code tab: how many comments the review is holding, its
* summary, and the verdict that sends the lot. Hidden entirely on a host that cannot take a
* review. The glass card frame belongs to the caller (PullRequestCodeTab), which is why this
* only contributes its own padding.
*/
import type { EnvironmentId, PullRequestRef, PullRequestReviewVerdict } from "@t3tools/contracts";
import { CheckIcon, MessageSquareIcon, XCircleIcon } from "lucide-react";
import { useState, type ReactNode } from "react";
import { pullRequestEnvironment } from "~/state/pullRequests";
import { useAtomCommand } from "~/state/use-atom-command";
import { Button } from "../ui/button";
import { Textarea } from "../ui/textarea";
import { toastManager } from "../ui/toast";
import {
pullRequestReviewKey,
usePendingReviewComments,
usePullRequestReviewStore,
} from "./pullRequestReviewStore";
const VERDICTS: ReadonlyArray<{
readonly value: PullRequestReviewVerdict;
readonly label: string;
readonly sent: string;
readonly icon: ReactNode;
}> = [
{
value: "comment",
label: "Comment",
sent: "Review submitted",
icon: ,
},
{
value: "approve",
label: "Approve",
sent: "Pull request approved",
icon: ,
},
{
value: "request-changes",
label: "Request changes",
sent: "Changes requested",
icon: ,
},
];
export function PullRequestReviewBar({
environmentId,
reference,
verdicts,
onSubmitted,
}: {
environmentId: EnvironmentId;
reference: PullRequestRef;
verdicts: ReadonlyArray;
onSubmitted: () => void;
}) {
const [pending, setPending] = useState(false);
const comments = usePendingReviewComments(reference);
const reviewKey = pullRequestReviewKey(reference);
// The panel stays mounted while the selected pull request changes. Keeping summaries beside
// the keyed line-comment drafts makes the selected pull request's body correct on the first
// render, before an effect could reset state left behind by the previous one.
const body = usePullRequestReviewStore((store) => store.summaries[reviewKey] ?? "");
const clear = usePullRequestReviewStore((store) => store.clear);
const removeComments = usePullRequestReviewStore((store) => store.removeComments);
const setSummary = usePullRequestReviewStore((store) => store.setSummary);
const clearSummary = usePullRequestReviewStore((store) => store.clearSummary);
const submitReview = useAtomCommand(pullRequestEnvironment.submitReview, {
reportFailure: false,
});
const offered = VERDICTS.filter((verdict) => verdicts.includes(verdict.value));
if (offered.length === 0) return null;
const submit = async (verdict: (typeof VERDICTS)[number]) => {
if (pending) return;
const submittedBody = body;
const submittedComments = comments;
setPending(true);
const result = await submitReview({
environmentId,
input: {
...reference,
verdict: verdict.value,
body: submittedBody,
comments: submittedComments,
},
});
setPending(false);
if (result._tag === "Failure") {
// The draft is kept: whatever went wrong, retyping the review is not the answer.
toastManager.add({ type: "error", title: "The review could not be submitted" });
return;
}
// More remarks may have been added while the host was accepting this snapshot. Leave those,
// and any summary revised in the meantime, ready for the next review.
removeComments(
reviewKey,
submittedComments.map((comment) => comment.id),
);
clearSummary(reviewKey, submittedBody);
toastManager.add({ type: "success", title: verdict.sent });
onSubmitted();
};
// An approval needs no words; anything else does, unless it carries line comments instead.
const canSubmit = (verdict: PullRequestReviewVerdict) =>
verdict === "approve" || body.trim().length > 0 || comments.length > 0;
return (