import { useQueryClient } from '@tanstack/react-query'; import { CheckIcon, MessageSquareIcon, XIcon } from 'lucide-react'; import { useState } from 'react'; import { Button } from '#/components/atoms/button.jsx'; import { Card } from '#/components/atoms/card.jsx'; import { MarkdownField } from '#/components/molecules/markdown-field.jsx'; import { publishReview } from '#/lib/publish.js'; import { useSession } from '#/lib/session.jsx'; /** * The three verdicts a review record carries, in the order offered, each * with the colour it means: a positive one reads success, a blocking one * reads danger, and a plain one reads in the accent a neutral mark takes. * The same three hues the verdict wears everywhere else it is shown. */ const VERDICTS = [ { verdict: 'approve', label: 'Approve', Icon: CheckIcon, active: 'border-success/40 bg-success/10 text-success', }, { verdict: 'changesRequested', label: 'Request changes', Icon: XIcon, active: 'border-destructive/40 bg-destructive/10 text-destructive', }, { verdict: 'comment', label: 'Comment', Icon: MessageSquareIcon, active: 'border-key/40 bg-key/10 text-key', }, ]; /** * Publish a review of the pull request in view, as the signed-in visitor. * * Signing in lives in the header; this only writes. The record goes into the * reviewer's own repo on their own PDS; this repository is never written to. * Whether the review then shows on the pull request is the repository's call, * made by the reviewers list in its config, which is why publishing carries * no promise of being displayed. * * Two shapes. The review shape, on the files, carries a verdict: approve, * request changes, or comment. The comment shape, on the conversation, * carries none, so a word about the whole pull request is not a judgement on * it. A forge keeps the verdict off the conversation and on the diff, where a * reader has read the code the verdict speaks to. * @param {Object} props * @param {'review'|'comment'} [props.variant] * @param {boolean} [props.bare] - without the card, for a surface that frames * it already, such as a popover * @param {(statement: Record) => void} [props.onPublished] - * given the published record once the write lands. A caller that takes it * adds it to the timeline itself and shows no success card here. */ export function ReviewComposer({ repo, sha, author, refName, variant = 'review', bare = false, onPublished, }) { const isReview = variant === 'review'; const { session, reviewer } = useSession(); const queryClient = useQueryClient(); const [verdict, setVerdict] = useState(isReview ? 'approve' : 'comment'); const [note, setNote] = useState(''); const [busy, setBusy] = useState(false); const [error, setError] = useState(''); const [published, setPublished] = useState(''); // A visitor who is not signed in is offered nothing here. The bar at the // top of the window is where signing in lives, on every screen. if (!session) return null; const post = async (event) => { event.preventDefault(); setBusy(true); setError(''); try { const written = await publishReview(session, { repo, sha, author, ref: refName, verdict, note: note.trim(), }); setNote(''); // The next read of the list re-reads from the network. Until then the // caller places the record on the timeline, the way an inline comment // joins from local state, so the writer sees their own words at once. queryClient.invalidateQueries({ queryKey: ['pulls', repo] }); if (onPublished) { onPublished({ uri: written.uri, cid: written.cid, sha, author, ref: refName, verdict, note: note.trim(), reviewedAt: new Date().toISOString(), by: { did: session.did, handle: session.handle }, }); } else { setPublished(written.uri); } } catch (err) { setError(err instanceof Error ? err.message : String(err)); } finally { setBusy(false); } }; // A plain function, not a component: a component defined here would take a // new identity each render and remount the textarea, so a keypress would // lose focus. The wrapped element stays a Card or a div, which React keeps. /** @param {string} className @param {import('react').ReactNode} children */ const frame = (className, children) => bare ? (
{children}
) : ( {children} ); if (published) { return frame( bare ? '' : 'mt-4 px-5 py-3',

{isReview ? 'Review published to your repo. It shows on the pull request if this repository names you a reviewer.' : 'Comment published to your repo. It shows on the pull request if this repository names you a reviewer.'}

, ); } return frame( bare ? '' : 'mt-4 px-5 py-4',
{isReview && (
Review this pull request
{VERDICTS.map((option) => ( ))}
)}
{error && ( {error} )}
, ); }