v0.29.0: #28 Parts 2+3 — create-RFC offers + contribute-to-pending requests
Extends the v0.26.0 (#28 Part 1) read-time scanner into three buckets in one pass — active link (Part 1), pending-RFC contribute offer (Part 3), create-RFC offer (Part 2) — precedence active > pending > candidate. The backend still emits only structured segments (never HTML), so the surface stays XSS-safe by construction. Part 2 — create-RFC offers: a multi-word tag from the #27 taxonomy with no defining RFC renders, for a create-rights viewer, as an inline "+ create RFC" affordance that opens the propose modal pre-filled (?propose=<term>; ProposeModal gained initialTitle). Conservative multi-word gate; broader heuristics + the Haiku path are deferred. Part 3 — contribute-to-pending offers: a term matching a super-draft renders, for a signed-in non-owner, an "ask to contribute" affordance with the owner's display name. It opens a 3-field request form (who/why/optional use-case); submitting lands a contribution_requests row (migration 024) and one actionable §15 notification per owner (new kind contribution_request_on_pending_rfc, personal-direct). The owner's inbox shows who/why/use-case inline with Accept/Decline. Accept fires #12's owner-invite flow with the requester as invitee and echoes a notification back; decline notifies the requester. Pre-merge idea PRs are out of scope. New endpoints: GET /api/rfcs/{slug}/contribution-target, POST /api/rfcs/{slug}/contribution-requests, .../{id}/accept, .../{id}/decline. The invite issue path was refactored into one reusable api_invitations.issue_invitation(...) chokepoint shared by the manual invite endpoint and Part 3's accept. Tests: 9 new (3 scanner-bucket unit + 6 e2e). Full suite 374 passing. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -1,21 +1,40 @@
|
||||
// LinkedText.jsx — roadmap #28 Part 1.
|
||||
// LinkedText.jsx — roadmap #28 (Parts 1–3).
|
||||
//
|
||||
// Renders a backend-provided list of text/rfc-link segments (see
|
||||
// backend/app/rfc_links.py). RFC references in PR descriptions and
|
||||
// comments arrive pre-scanned as structured segments — this component
|
||||
// maps them onto plain text runs and anchor elements. It never renders
|
||||
// Renders a backend-provided list of text/link segments (see
|
||||
// backend/app/rfc_links.py). References in PR descriptions and comments
|
||||
// arrive pre-scanned as structured segments — this component maps them
|
||||
// onto plain text runs, anchors, and inline affordances. It never renders
|
||||
// HTML from the server (no dangerouslySetInnerHTML), so the surface is
|
||||
// XSS-safe regardless of what a comment author typed.
|
||||
//
|
||||
// Segment types:
|
||||
// * `rfc` — Part 1: a link to an accepted (active) RFC.
|
||||
// * `rfc-pending` — Part 3: the term names a pending (super-draft)
|
||||
// RFC; a signed-in viewer who isn't its owner gets
|
||||
// an inline "ask to contribute" affordance routing
|
||||
// to the contribute form (App reads `?contribute=`).
|
||||
// * `rfc-candidate` — Part 2: a strong-candidate term with no RFC yet;
|
||||
// a viewer with create rights (`canCreate`) gets a
|
||||
// "create RFC" affordance routing to the propose
|
||||
// flow pre-filled (App reads `?propose=`).
|
||||
//
|
||||
// Affordances degrade to plain text when the viewer lacks the relevant
|
||||
// right, so the visible prose is identical for everyone — only the
|
||||
// offered actions differ.
|
||||
//
|
||||
// `segments` is the enriched array; `text` is the raw fallback used when
|
||||
// the field is absent (an older cached response, or a caller that didn't
|
||||
// pass segments). Either way the visible text is identical — only the
|
||||
// links differ.
|
||||
// pass segments).
|
||||
|
||||
export default function LinkedText({ segments, text }) {
|
||||
import { Link } from 'react-router-dom'
|
||||
|
||||
export default function LinkedText({ segments, text, viewer, canCreate }) {
|
||||
if (!Array.isArray(segments) || segments.length === 0) {
|
||||
return <>{text ?? ''}</>
|
||||
}
|
||||
// A signed-in, beta-granted viewer can ask to contribute; the backend
|
||||
// re-checks ownership/collaborator status and rejects self-requests.
|
||||
const canContribute = !!viewer && viewer.permission_state === 'granted'
|
||||
return (
|
||||
<>
|
||||
{segments.map((seg, i) => {
|
||||
@@ -31,6 +50,40 @@ export default function LinkedText({ segments, text }) {
|
||||
</a>
|
||||
)
|
||||
}
|
||||
if (seg.type === 'rfc-pending') {
|
||||
const who = seg.owner || 'Someone'
|
||||
return (
|
||||
<span key={i} className="rfc-pending">
|
||||
{seg.label}
|
||||
{canContribute && (
|
||||
<Link
|
||||
className="rfc-offer rfc-offer-contribute"
|
||||
to={`?contribute=${encodeURIComponent(seg.slug)}&term=${encodeURIComponent(seg.label)}`}
|
||||
title={`${who} is working on an RFC for '${seg.label}' — ask to contribute`}
|
||||
>
|
||||
ask to contribute
|
||||
</Link>
|
||||
)}
|
||||
</span>
|
||||
)
|
||||
}
|
||||
if (seg.type === 'rfc-candidate') {
|
||||
const term = seg.term || seg.label
|
||||
return (
|
||||
<span key={i} className="rfc-candidate">
|
||||
{seg.label}
|
||||
{canCreate && (
|
||||
<Link
|
||||
className="rfc-offer rfc-offer-create"
|
||||
to={`?propose=${encodeURIComponent(term)}`}
|
||||
title={`Create RFC for '${term}'`}
|
||||
>
|
||||
+ create RFC
|
||||
</Link>
|
||||
)}
|
||||
</span>
|
||||
)
|
||||
}
|
||||
return <span key={i}>{seg.text}</span>
|
||||
})}
|
||||
</>
|
||||
|
||||
Reference in New Issue
Block a user