Files
rfc-app/frontend/src/components/PRModal.jsx
T
Ben Stull 7c6c906db2 #26: optional proposed-use-case field on propose-RFC + propose-PR
Adds an optional "What will you be using this for?" capture as a sibling
to the required justification on both propose surfaces, per roadmap #26.

- propose-RFC modal (ProposeModal): optional textarea below the required
  "Why is this RFC needed?" pitch, labeled "What will you be using this
  RFC for? (optional)".
- propose-PR modal (PRModal): optional textarea below the required
  description, labeled "What will you be using this change for?".
- Backend: ProposeBody / OpenPRBody gain an optional `proposed_use_case`
  (NULL/omitted accepted, no min, 8000-char cap matching the existing
  free-text bound). Persisted to a new canonical side table
  `proposed_use_cases` keyed by PR number, mirrored onto the cache
  columns added by migration 021. Returned on the proposal list/detail,
  RFC detail (by slug), and PR detail endpoints.
- Display: ProposalView, RFCView (main only), and PRView render the
  captured use case with a muted "left blank" treatment when NULL.
- migration 021: nullable `proposed_use_case` on cached_rfcs/cached_prs
  plus the reconcile-proof `proposed_use_cases` truth table.
- New vertical test_proposed_use_case_vertical: persists+returns when
  supplied, accepted as NULL/omitted, for both surfaces.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-05-28 12:28:52 -07:00

139 lines
5.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'
import { EVENTS, track } from '../lib/analytics'
export default function PRModal({ slug, branch, branchIsPrivate, onClose, onOpened }) {
const [title, setTitle] = useState('')
const [description, setDescription] = useState('')
// #26: optional ground-truth use case for this change.
const [useCase, setUseCase] = 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(),
proposedUseCase: useCase.trim() || null,
})
// v0.15.0 — analytics: fire on §10.2 PR-open success. slug
// and pr_number are the join keys; title/description stay out.
track(EVENTS.PR_OPENED, { rfc_slug: slug, pr_number })
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>
<label className="modal-label">What will you be using this change for? (optional)</label>
<textarea
className="modal-textarea"
value={useCase}
onChange={e => setUseCase(e.target.value)}
placeholder="The concrete thing this change unlocks for you. Optional."
disabled={drafting || submitting}
rows={3}
maxLength={8000}
/>
<p className="field-help">
#26: the concrete ground-truth use case distinct from "why
it's needed" above. Leave blank if you'd rather not say.
</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>
)
}