From 2b32e124abe099b3535c6eddfb6488a8f8c44da3 Mon Sep 17 00:00:00 2001 From: Ben Stull Date: Fri, 5 Jun 2026 13:12:45 -0700 Subject: [PATCH] =?UTF-8?q?=C2=A722=20S2:=20Catalog=20+=20propose=20scoped?= =?UTF-8?q?=20to=20the=20active=20collection?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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) --- frontend/src/App.jsx | 7 ++++++- frontend/src/components/Catalog.jsx | 13 ++++++++----- frontend/src/components/ProposeModal.jsx | 3 ++- 3 files changed, 16 insertions(+), 7 deletions(-) diff --git a/frontend/src/App.jsx b/frontend/src/App.jsx index e7453cf..c1db389 100644 --- a/frontend/src/App.jsx +++ b/frontend/src/App.jsx @@ -66,6 +66,10 @@ export default function App() { // right project. Falls back to the deployment default off a project route. const _projMatch = location.pathname.match(/^\/p\/([^/]+)/) const currentProjectId = (_projMatch && _projMatch[1]) || deployment.defaultProjectId + // §22 S2 — the collection the viewer is currently in (from the /c// 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 2–3: the LinkedText create/contribute affordances route via // query params so they need no prop-threading from deep in a comment // list. `?propose=` opens the propose modal pre-filled; @@ -389,12 +393,13 @@ export default function App() { viewer={viewer} initialTitle={proposeParam || ''} projectId={currentProjectId} + collectionId={currentCollectionId} onClose={() => { setProposeOpen(false); clearParams('propose') }} onSubmitted={({ pr_number }) => { setProposeOpen(false) clearParams('propose') setCatalogVersion(v => v + 1) - navigate(proposalPath(currentProjectId, pr_number)) + navigate(proposalPath(currentProjectId, pr_number, currentCollectionId)) }} /> )} diff --git a/frontend/src/components/Catalog.jsx b/frontend/src/components/Catalog.jsx index 24bc470..607ae06 100644 --- a/frontend/src/components/Catalog.jsx +++ b/frontend/src/components/Catalog.jsx @@ -9,7 +9,7 @@ import { useEffect, useMemo, useState } from 'react' import { useParams, Link } from 'react-router-dom' import { listRFCs, listProposals } from '../api' -import { entryPath, proposalPath, useProjectId } from '../lib/entryPaths' +import { entryPath, proposalPath, useProjectId, useCollectionId } from '../lib/entryPaths' const STATE_CHIPS = [ { id: 'super-draft', label: 'Super-draft' }, @@ -32,11 +32,14 @@ export default function Catalog({ viewer, onProposeRFC, version }) { const [pendingOpen, setPendingOpen] = useState(true) const { slug, prNumber } = useParams() 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(() => { - 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([])) - }, [version, pid]) + }, [version, pid, cid]) const filtered = useMemo(() => { const needle = search.trim().toLowerCase() @@ -105,7 +108,7 @@ export default function Catalog({ viewer, onProposeRFC, version }) { return (
@@ -133,7 +136,7 @@ export default function Catalog({ viewer, onProposeRFC, version }) { {proposals.map(p => (
{p.title.replace(/^Propose:\s*/, '')}
diff --git a/frontend/src/components/ProposeModal.jsx b/frontend/src/components/ProposeModal.jsx index 0989a89..ef7a110 100644 --- a/frontend/src/components/ProposeModal.jsx +++ b/frontend/src/components/ProposeModal.jsx @@ -28,7 +28,7 @@ function slugify(title) { .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 ''" affordance pre-fills the title // (App passes the `?propose=` value here); the slug derives from it // via the same effect that drives manual typing. @@ -98,6 +98,7 @@ export default function ProposeModal({ viewer, onClose, onSubmitted, initialTitl pitch: pitch.trim(), tags, proposedUseCase: useCase.trim() || null, + collectionId, }) // v0.15.0 — analytics: fire on the §9.1 propose-RFC submit. // Slug is a stable, low-cardinality identifier (kebab-case