§22 S6: type-driven entry noun (§22.4a) — backend source of truth + chrome
The displayed noun for an entry is a framework concept keyed on the collection's immutable type — document→"RFC", specification→"Spec", bdd→"Feature" — not deployment content. The chrome reads it from the API instead of hardcoding "RFC". - collections.ENTRY_NOUN + entry_noun(type) (unknown type → generic "RFC"). - Surfaced as entry_noun on get_collection, list_collections items, the /api/deployment directory items, and GET /api/projects/:id. - Frontend: Catalog reads entry_noun for the "+ Propose New <noun>" control; ProposeModal fetches the active collection's noun for its title + field copy. - test_s6_entry_noun_vertical.py: map + API surfacing (collection + directory). Scope note: this is §22.4a item (2) — terminology. The deeper type-module work (item 1 per-type frontmatter schemas; item 3 specification release-planning + bdd scenario/coverage surfaces) is flagged OPEN in the design doc and is handed off to a follow-up spec+slice. Backend 528 passed; frontend 30 passed + build green. Part of v0.45.0. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -31,6 +31,9 @@ export default function Catalog({ viewer, onProposeRFC, version }) {
|
||||
// collection's caps load; we fall back to "any authenticated viewer" so the
|
||||
// common case doesn't flicker, then refine.
|
||||
const [canContribute, setCanContribute] = useState(null)
|
||||
// §22.4a: the type-driven entry noun ("RFC" | "Spec" | "Feature") for this
|
||||
// collection, read from the API. Defaults to the generic "RFC" until loaded.
|
||||
const [entryNoun, setEntryNoun] = useState('RFC')
|
||||
const [search, setSearch] = useState('')
|
||||
const [sort, setSort] = useState('recent')
|
||||
const [activeChips, setActiveChips] = useState(new Set())
|
||||
@@ -46,7 +49,10 @@ export default function Catalog({ viewer, onProposeRFC, version }) {
|
||||
listProposals(pid).then(d => setProposals(d.items)).catch(() => setProposals([]))
|
||||
setCanContribute(null)
|
||||
getCollection(pid, cid)
|
||||
.then(c => setCanContribute(!!c?.viewer?.can_contribute))
|
||||
.then(c => {
|
||||
setCanContribute(!!c?.viewer?.can_contribute)
|
||||
setEntryNoun(c?.entry_noun || 'RFC')
|
||||
})
|
||||
.catch(() => setCanContribute(false))
|
||||
}, [version, pid, cid])
|
||||
|
||||
@@ -177,7 +183,7 @@ export default function Catalog({ viewer, onProposeRFC, version }) {
|
||||
// contribute to *this* collection (the scope-role gate); a granted
|
||||
// viewer with no contribute right in this collection sees nothing.
|
||||
mayPropose && (
|
||||
<button className="btn-propose" onClick={onProposeRFC}>+ Propose New RFC</button>
|
||||
<button className="btn-propose" onClick={onProposeRFC}>+ Propose New {entryNoun}</button>
|
||||
)
|
||||
) : (
|
||||
<a className="btn-propose" href="/auth/login" title="Private beta — only invited emails can propose">
|
||||
|
||||
@@ -12,7 +12,7 @@
|
||||
// success navigates the proposer to the pending-idea view per §9.3.
|
||||
|
||||
import { useEffect, useRef, useState } from 'react'
|
||||
import { proposeRFC, suggestTags } from '../api'
|
||||
import { proposeRFC, suggestTags, getCollection } from '../api'
|
||||
import { EVENTS, track } from '../lib/analytics'
|
||||
|
||||
// How long the draft must sit unchanged before we ask for suggestions —
|
||||
@@ -33,6 +33,9 @@ export default function ProposeModal({ viewer, onClose, onSubmitted, initialTitl
|
||||
// (App passes the `?propose=<term>` value here); the slug derives from it
|
||||
// via the same effect that drives manual typing.
|
||||
const [title, setTitle] = useState(initialTitle)
|
||||
// §22.4a: the type-driven noun for the active collection ("RFC" | "Spec" |
|
||||
// "Feature"), read from the API. Defaults to "RFC" until the collection loads.
|
||||
const [entryNoun, setEntryNoun] = useState('RFC')
|
||||
const [slug, setSlug] = useState('')
|
||||
const [slugEdited, setSlugEdited] = useState(false)
|
||||
const [pitch, setPitch] = useState('')
|
||||
@@ -53,6 +56,17 @@ export default function ProposeModal({ viewer, onClose, onSubmitted, initialTitl
|
||||
if (!slugEdited) setSlug(slugify(title))
|
||||
}, [title, slugEdited])
|
||||
|
||||
// §22.4a: resolve the active collection's entry noun so the modal chrome
|
||||
// names entries per the collection type. Best-effort; falls back to "RFC".
|
||||
useEffect(() => {
|
||||
if (!projectId || !collectionId) return
|
||||
let live = true
|
||||
getCollection(projectId, collectionId)
|
||||
.then(c => { if (live) setEntryNoun(c?.entry_noun || 'RFC') })
|
||||
.catch(() => {})
|
||||
return () => { live = false }
|
||||
}, [projectId, collectionId])
|
||||
|
||||
// #27: debounced tag-suggestion fetch. Fires after the draft sits
|
||||
// unchanged for SUGGEST_DEBOUNCE_MS, only once there's something to go
|
||||
// on (a title). A stale-response guard keeps an earlier in-flight
|
||||
@@ -116,7 +130,7 @@ export default function ProposeModal({ viewer, onClose, onSubmitted, initialTitl
|
||||
<div className="modal-overlay" onClick={e => { if (e.target === e.currentTarget) onClose() }}>
|
||||
<div className="modal">
|
||||
<div className="modal-header">
|
||||
<h2>Propose a New RFC</h2>
|
||||
<h2>Propose a New {entryNoun}</h2>
|
||||
<button className="modal-close" onClick={onClose}>×</button>
|
||||
</div>
|
||||
<form onSubmit={handleSubmit}>
|
||||
@@ -130,7 +144,7 @@ export default function ProposeModal({ viewer, onClose, onSubmitted, initialTitl
|
||||
autoFocus
|
||||
required
|
||||
/>
|
||||
<p className="field-help">The word or topic this RFC will define.</p>
|
||||
<p className="field-help">The word or topic this {entryNoun} will define.</p>
|
||||
|
||||
<label htmlFor="propose-slug">Slug</label>
|
||||
<input
|
||||
@@ -142,22 +156,22 @@ export default function ProposeModal({ viewer, onClose, onSubmitted, initialTitl
|
||||
/>
|
||||
<p className="field-help">Auto-derived from the title; edit if needed.</p>
|
||||
|
||||
<label htmlFor="propose-pitch">Why is this RFC needed?</label>
|
||||
<label htmlFor="propose-pitch">Why is this {entryNoun} needed?</label>
|
||||
<textarea
|
||||
id="propose-pitch"
|
||||
value={pitch}
|
||||
onChange={e => setPitch(e.target.value)}
|
||||
placeholder="One or two paragraphs answering 'why this RFC is needed.'"
|
||||
placeholder={`One or two paragraphs answering 'why this ${entryNoun} is needed.'`}
|
||||
rows={5}
|
||||
required
|
||||
/>
|
||||
|
||||
<label htmlFor="propose-use-case">What will you be using this RFC for? (optional)</label>
|
||||
<label htmlFor="propose-use-case">What will you be using this {entryNoun} for? (optional)</label>
|
||||
<textarea
|
||||
id="propose-use-case"
|
||||
value={useCase}
|
||||
onChange={e => setUseCase(e.target.value)}
|
||||
placeholder="The concrete thing you intend to build or do with this RFC. Optional, but it helps ground the work."
|
||||
placeholder={`The concrete thing you intend to build or do with this ${entryNoun}. Optional, but it helps ground the work.`}
|
||||
rows={3}
|
||||
/>
|
||||
<p className="field-help">
|
||||
|
||||
Reference in New Issue
Block a user