Files
rfc-app/frontend/src/components/PRModal.jsx
T
Ben Stull a2bf89e90b Slice 3: the PR flow
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-24 12:37:54 -07:00

115 lines
4.3 KiB
React
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// PRModal.jsx — the §10.2 PR creation modal.
//
// Opens from the §10.1 "Open PR" affordance on a branch view. The
// modal fetches an AI-drafted title and description on open via the
// /pr-draft endpoint, prefills both fields, lets the contributor edit
// before submit. When the source branch is private, surfaces the
// §11.3 universal-public confirmation inline above the form rather
// than as a separate dialog — the confirmation is part of this
// surface per §10.1.
import { useEffect, useState } from 'react'
import { draftPRText, openPR } from '../api'
export default function PRModal({ slug, branch, branchIsPrivate, onClose, onOpened }) {
const [title, setTitle] = useState('')
const [description, setDescription] = useState('')
const [drafting, setDrafting] = useState(true)
const [submitting, setSubmitting] = useState(false)
const [confirmed, setConfirmed] = useState(!branchIsPrivate)
const [error, setError] = useState(null)
useEffect(() => {
let cancelled = false
draftPRText(slug, branch)
.then(d => {
if (cancelled) return
setTitle(d.title || '')
setDescription(d.description || '')
})
.catch(err => { if (!cancelled) setError(err.message) })
.finally(() => { if (!cancelled) setDrafting(false) })
return () => { cancelled = true }
}, [slug, branch])
const canSubmit = !!title.trim() && !drafting && !submitting && confirmed
const submit = async () => {
setSubmitting(true)
setError(null)
try {
const { pr_number } = await openPR(slug, branch, { title: title.trim(), description: description.trim() })
onOpened?.(pr_number)
} catch (e) {
setError(e.message)
setSubmitting(false)
}
}
return (
<div className="modal-overlay" onClick={e => { if (e.target === e.currentTarget) onClose() }}>
<div className="modal pr-modal">
<div className="modal-header">
<h2>Open PR {branch}</h2>
<button className="modal-close" onClick={onClose}>×</button>
</div>
<div className="modal-body">
{branchIsPrivate && (
<div className="pr-modal-warning">
<p>
<strong>This branch is currently private.</strong> Per §11.3,
opening a PR makes the branch and its history publicly readable.
There is no notion of a private PR.
</p>
<label>
<input
type="checkbox"
checked={confirmed}
onChange={e => setConfirmed(e.target.checked)}
/>
{' '}I understand the branch will become public.
</label>
</div>
)}
<label className="modal-label">Title</label>
<input
type="text"
className="modal-input"
value={title}
onChange={e => setTitle(e.target.value)}
placeholder={drafting ? 'Drafting from the diff and chat…' : 'A one-line structural description'}
disabled={drafting || submitting}
maxLength={240}
/>
<p className="field-help">
§10.2: a one-line structural description in spec voice. What was
edited, in what way. AI-drafted; edit before submit.
</p>
<label className="modal-label">Description</label>
<textarea
className="modal-textarea"
value={description}
onChange={e => setDescription(e.target.value)}
placeholder={drafting ? 'Drafting…' : 'Two to four sentences pulling from the chat'}
disabled={drafting || submitting}
rows={7}
maxLength={8000}
/>
<p className="field-help">
§10.2: two to four sentences pulling from the branch chat
what was argued, what shifted, what the arbiters are asked
to consider.
</p>
{error && <p className="field-error">{error}</p>}
</div>
<div className="modal-actions">
<button className="btn-secondary" onClick={onClose} disabled={submitting}>Cancel</button>
<button className="btn-primary" onClick={submit} disabled={!canSubmit}>
{submitting ? 'Opening…' : 'Open PR'}
</button>
</div>
</div>
</div>
)
}