§22 S2: Catalog + propose scoped to the active collection

Catalog reads the /c/:collectionId/ segment via useCollectionId and fetches the
collection-scoped catalog, building entry/proposal links with the active
collection; the propose modal threads the active collection so a propose from a
named collection targets it.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
Ben Stull
2026-06-05 13:12:45 -07:00
parent 98eea3e2d6
commit 2b32e124ab
3 changed files with 16 additions and 7 deletions
+6 -1
View File
@@ -66,6 +66,10 @@ export default function App() {
// right project. Falls back to the deployment default off a project route. // right project. Falls back to the deployment default off a project route.
const _projMatch = location.pathname.match(/^\/p\/([^/]+)/) const _projMatch = location.pathname.match(/^\/p\/([^/]+)/)
const currentProjectId = (_projMatch && _projMatch[1]) || deployment.defaultProjectId const currentProjectId = (_projMatch && _projMatch[1]) || deployment.defaultProjectId
// §22 S2 — the collection the viewer is currently in (from the /c/<cid>/ URL
// segment), so a propose targets that collection. Falls back to the default.
const _colMatch = location.pathname.match(/^\/p\/[^/]+\/c\/([^/]+)/)
const currentCollectionId = (_colMatch && _colMatch[1]) || DEFAULT_COLLECTION
// #28 Parts 23: the LinkedText create/contribute affordances route via // #28 Parts 23: the LinkedText create/contribute affordances route via
// query params so they need no prop-threading from deep in a comment // query params so they need no prop-threading from deep in a comment
// list. `?propose=<term>` opens the propose modal pre-filled; // list. `?propose=<term>` opens the propose modal pre-filled;
@@ -389,12 +393,13 @@ export default function App() {
viewer={viewer} viewer={viewer}
initialTitle={proposeParam || ''} initialTitle={proposeParam || ''}
projectId={currentProjectId} projectId={currentProjectId}
collectionId={currentCollectionId}
onClose={() => { setProposeOpen(false); clearParams('propose') }} onClose={() => { setProposeOpen(false); clearParams('propose') }}
onSubmitted={({ pr_number }) => { onSubmitted={({ pr_number }) => {
setProposeOpen(false) setProposeOpen(false)
clearParams('propose') clearParams('propose')
setCatalogVersion(v => v + 1) setCatalogVersion(v => v + 1)
navigate(proposalPath(currentProjectId, pr_number)) navigate(proposalPath(currentProjectId, pr_number, currentCollectionId))
}} }}
/> />
)} )}
+8 -5
View File
@@ -9,7 +9,7 @@
import { useEffect, useMemo, useState } from 'react' import { useEffect, useMemo, useState } from 'react'
import { useParams, Link } from 'react-router-dom' import { useParams, Link } from 'react-router-dom'
import { listRFCs, listProposals } from '../api' import { listRFCs, listProposals } from '../api'
import { entryPath, proposalPath, useProjectId } from '../lib/entryPaths' import { entryPath, proposalPath, useProjectId, useCollectionId } from '../lib/entryPaths'
const STATE_CHIPS = [ const STATE_CHIPS = [
{ id: 'super-draft', label: 'Super-draft' }, { id: 'super-draft', label: 'Super-draft' },
@@ -32,11 +32,14 @@ export default function Catalog({ viewer, onProposeRFC, version }) {
const [pendingOpen, setPendingOpen] = useState(true) const [pendingOpen, setPendingOpen] = useState(true)
const { slug, prNumber } = useParams() const { slug, prNumber } = useParams()
const pid = useProjectId() const pid = useProjectId()
// §22 S2: the catalog is scoped to the active collection (the `/c/:cid/`
// route segment, else the project's default collection).
const cid = useCollectionId()
useEffect(() => { useEffect(() => {
listRFCs(pid).then(d => setRfcs(d.items)).catch(() => setRfcs([])) listRFCs(pid, cid).then(d => setRfcs(d.items)).catch(() => setRfcs([]))
listProposals(pid).then(d => setProposals(d.items)).catch(() => setProposals([])) listProposals(pid).then(d => setProposals(d.items)).catch(() => setProposals([]))
}, [version, pid]) }, [version, pid, cid])
const filtered = useMemo(() => { const filtered = useMemo(() => {
const needle = search.trim().toLowerCase() const needle = search.trim().toLowerCase()
@@ -105,7 +108,7 @@ export default function Catalog({ viewer, onProposeRFC, version }) {
return ( return (
<Link <Link
key={r.slug} key={r.slug}
to={entryPath(pid, r.slug)} to={entryPath(pid, r.slug, cid)}
className={`catalog-row ${isActive ? 'active' : ''} ${isSuper ? 'is-super' : ''}`} className={`catalog-row ${isActive ? 'active' : ''} ${isSuper ? 'is-super' : ''}`}
> >
<div className="row-top"> <div className="row-top">
@@ -133,7 +136,7 @@ export default function Catalog({ viewer, onProposeRFC, version }) {
{proposals.map(p => ( {proposals.map(p => (
<Link <Link
key={p.pr_number} key={p.pr_number}
to={proposalPath(pid, p.pr_number)} to={proposalPath(pid, p.pr_number, cid)}
className={`pending-row ${String(prNumber) === String(p.pr_number) ? 'active' : ''}`} className={`pending-row ${String(prNumber) === String(p.pr_number) ? 'active' : ''}`}
> >
<div>{p.title.replace(/^Propose:\s*/, '')}</div> <div>{p.title.replace(/^Propose:\s*/, '')}</div>
+2 -1
View File
@@ -28,7 +28,7 @@ function slugify(title) {
.replace(/^-+|-+$/g, '') .replace(/^-+|-+$/g, '')
} }
export default function ProposeModal({ viewer, onClose, onSubmitted, initialTitle = '', projectId }) { export default function ProposeModal({ viewer, onClose, onSubmitted, initialTitle = '', projectId, collectionId }) {
// #28 Part 2: a "create RFC for '<term>'" affordance pre-fills the title // #28 Part 2: a "create RFC for '<term>'" affordance pre-fills the title
// (App passes the `?propose=<term>` value here); the slug derives from it // (App passes the `?propose=<term>` value here); the slug derives from it
// via the same effect that drives manual typing. // via the same effect that drives manual typing.
@@ -98,6 +98,7 @@ export default function ProposeModal({ viewer, onClose, onSubmitted, initialTitl
pitch: pitch.trim(), pitch: pitch.trim(),
tags, tags,
proposedUseCase: useCase.trim() || null, proposedUseCase: useCase.trim() || null,
collectionId,
}) })
// v0.15.0 analytics: fire on the §9.1 propose-RFC submit. // v0.15.0 analytics: fire on the §9.1 propose-RFC submit.
// Slug is a stable, low-cardinality identifier (kebab-case // Slug is a stable, low-cardinality identifier (kebab-case