Compare commits
2 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 455ef33b29 | |||
| 9f548a340d |
@@ -23,6 +23,45 @@ skip versions are the composition of each intervening adjacent
|
||||
release's steps in order — no A-to-B path is pre-computed beyond
|
||||
that.
|
||||
|
||||
## 0.37.0 — 2026-06-04
|
||||
|
||||
**Minor — §22 M3-backend Plan B (2/2, read path): per-project RFC serving. A
|
||||
second project's corpus now renders under `/p/<id>/`, isolated by its own slug
|
||||
namespace. No upgrade steps; single-project deployments are unaffected.**
|
||||
|
||||
This is the slice that makes "multiple projects" *observable*: M3-frontend
|
||||
(v0.35.0) shipped the shell with a "not served" guard for any non-default
|
||||
project; v0.36.0 rebuilt the keys so project #2 can exist; this serves project
|
||||
#2's corpus.
|
||||
|
||||
Added:
|
||||
|
||||
- **Per-project read endpoints** (`app/api.py`): `GET /api/projects/{pid}/rfcs`
|
||||
(catalog scoped to one project) and `GET /api/projects/{pid}/rfcs/{slug}`
|
||||
(entry by `(project_id, slug)`), both behind the §22.5 read gate
|
||||
(gated project 404s a non-member). The unscoped `/api/rfcs[/{slug}]` remain as
|
||||
the default-project compat path.
|
||||
- **Per-project corpus mirror** (`app/cache.py`): `refresh_meta_repo` now
|
||||
iterates every `projects` row and mirrors each project's `content_repo` into
|
||||
`cached_rfcs` stamped with that `project_id` (was: the default project only).
|
||||
- **Tests:** `test_project_scoped_serving.py` — catalog scoping, entry
|
||||
isolation (same slug under two projects resolves distinctly; a slug present
|
||||
only in one 404s under the other), gated-project 404. 445 backend green.
|
||||
|
||||
Changed:
|
||||
|
||||
- **Frontend** reads the scoped routes: `api.listRFCs(projectId)` /
|
||||
`getRFC(projectId, slug)`; `Catalog` + `RFCView` pass `useProjectId()`. The
|
||||
**M3-frontend `NotServedPlaceholder` guard is removed** — every registry
|
||||
project renders its corpus. `ProjectLayout` keeps the 404/not-readable branch.
|
||||
|
||||
Known limitation (next slice): the **write** path (propose / branch / PR /
|
||||
graduate) and the **default-project-id re-stamp** (§22.13 step 1) are not yet
|
||||
project-scoped — write affordances still target the default project, so a
|
||||
non-default project is effectively read-only until Plan B's write slice. (No
|
||||
live impact: no deployment runs a second project yet.) Per
|
||||
`docs/superpowers/specs/2026-06-04-m3-backend-planb-design.md` §1 + §3 (write).
|
||||
|
||||
## 0.36.0 — 2026-06-04
|
||||
|
||||
**Minor — §22 M3-backend Plan B (1/2): the slug-keyed PK/UNIQUE rebuild that
|
||||
|
||||
@@ -708,6 +708,88 @@ def make_router(
|
||||
payload["proposed_use_case"] = uc["use_case"] if uc else None
|
||||
return payload
|
||||
|
||||
# ---------------------------------------------------------------
|
||||
# §22.4 (Plan B): per-project RFC serving — the catalog + entry view
|
||||
# scoped to one project, identified by its own slug namespace
|
||||
# (project_id, slug). The unscoped /api/rfcs[/{slug}] above stay as the
|
||||
# default-project compat path; the frontend reads these scoped routes so a
|
||||
# second project's corpus renders under /p/<id>/.
|
||||
# ---------------------------------------------------------------
|
||||
|
||||
@router.get("/api/projects/{project_id}/rfcs")
|
||||
async def list_project_rfcs(
|
||||
project_id: str, request: Request, unreviewed: str | None = None
|
||||
) -> dict[str, Any]:
|
||||
viewer = auth.current_user(request)
|
||||
# §22.5 read gate: a gated project's catalog 404s to a non-member.
|
||||
auth.require_project_readable(viewer, project_id)
|
||||
viewer_id = viewer.user_id if viewer else None
|
||||
unreviewed_clause = ""
|
||||
if unreviewed is not None and unreviewed.lower() in ("1", "true", "yes"):
|
||||
unreviewed_clause = " AND unreviewed = 1 AND state = 'active'"
|
||||
rows = db.conn().execute(
|
||||
f"""
|
||||
SELECT slug, title, state, rfc_id, repo,
|
||||
owners_json, arbiters_json, tags_json,
|
||||
last_main_commit_at, last_entry_commit_at, updated_at
|
||||
FROM cached_rfcs
|
||||
WHERE state IN ('super-draft', 'active')
|
||||
AND project_id = ?{unreviewed_clause}
|
||||
ORDER BY COALESCE(last_main_commit_at, last_entry_commit_at) DESC
|
||||
""",
|
||||
(project_id,),
|
||||
).fetchall()
|
||||
starred = set()
|
||||
if viewer_id is not None:
|
||||
starred = {
|
||||
r["rfc_slug"]
|
||||
for r in db.conn().execute(
|
||||
"SELECT rfc_slug FROM stars WHERE user_id = ? AND project_id = ?",
|
||||
(viewer_id, project_id),
|
||||
)
|
||||
}
|
||||
items = [
|
||||
{
|
||||
"slug": r["slug"],
|
||||
"title": r["title"],
|
||||
"state": r["state"],
|
||||
"id": r["rfc_id"],
|
||||
"repo": r["repo"],
|
||||
"owners": json.loads(r["owners_json"] or "[]"),
|
||||
"arbiters": json.loads(r["arbiters_json"] or "[]"),
|
||||
"tags": json.loads(r["tags_json"] or "[]"),
|
||||
"last_active_at": r["last_main_commit_at"] or r["last_entry_commit_at"] or r["updated_at"],
|
||||
"starred_by_me": r["slug"] in starred,
|
||||
"has_open_prs": False,
|
||||
}
|
||||
for r in rows
|
||||
]
|
||||
return {"items": items}
|
||||
|
||||
@router.get("/api/projects/{project_id}/rfcs/{slug}")
|
||||
async def get_project_rfc(project_id: str, slug: str, request: Request) -> dict[str, Any]:
|
||||
viewer = auth.current_user(request)
|
||||
auth.require_project_readable(viewer, project_id)
|
||||
row = db.conn().execute(
|
||||
"SELECT * FROM cached_rfcs WHERE project_id = ? AND slug = ?",
|
||||
(project_id, slug),
|
||||
).fetchone()
|
||||
if row is None:
|
||||
raise HTTPException(404, "Not found")
|
||||
if row["state"] == "retired" and (viewer is None or viewer.role != "owner"):
|
||||
raise HTTPException(404, "Not found")
|
||||
payload = _serialize_rfc(row)
|
||||
uc = db.conn().execute(
|
||||
"""
|
||||
SELECT use_case FROM proposed_use_cases
|
||||
WHERE scope = 'rfc' AND rfc_slug = ? AND project_id = ?
|
||||
ORDER BY id DESC LIMIT 1
|
||||
""",
|
||||
(slug, project_id),
|
||||
).fetchone()
|
||||
payload["proposed_use_case"] = uc["use_case"] if uc else None
|
||||
return payload
|
||||
|
||||
# ---------------------------------------------------------------
|
||||
# §22.4c: mark-reviewed — clear an active entry's `unreviewed` flag
|
||||
# ---------------------------------------------------------------
|
||||
|
||||
+33
-19
@@ -35,20 +35,29 @@ log = logging.getLogger(__name__)
|
||||
|
||||
|
||||
async def refresh_meta_repo(config: Config, gitea: Gitea) -> None:
|
||||
"""Re-read rfcs/ on the meta repo and reconcile cached_rfcs.
|
||||
"""Re-read rfcs/ on every project's content repo and reconcile cached_rfcs.
|
||||
|
||||
Idempotent. Safe to call on every meta-repo webhook and on every
|
||||
reconciler sweep.
|
||||
§22 (Plan B): a deployment has N projects (§22.1), each with its own
|
||||
content_repo (§22.3). Mirror each into cached_rfcs stamped with that
|
||||
project's id, so a second project's corpus renders under /p/<id>/. Idempotent;
|
||||
safe on every content-repo webhook and reconciler sweep.
|
||||
"""
|
||||
org = config.gitea_org
|
||||
repo = projects_mod.default_content_repo(config)
|
||||
if not repo:
|
||||
log.warning("refresh_meta_repo: default project has no content_repo yet; skipping")
|
||||
rows = db.conn().execute(
|
||||
"SELECT id, content_repo FROM projects WHERE content_repo IS NOT NULL AND content_repo != ''"
|
||||
).fetchall()
|
||||
if not rows:
|
||||
log.warning("refresh_meta_repo: no projects with a content_repo yet; skipping")
|
||||
return
|
||||
for prow in rows:
|
||||
await _refresh_project_corpus(org, prow["id"], prow["content_repo"], gitea)
|
||||
|
||||
|
||||
async def _refresh_project_corpus(org: str, project_id: str, repo: str, gitea: Gitea) -> None:
|
||||
try:
|
||||
files = await gitea.list_dir(org, repo, "rfcs", ref="main")
|
||||
except GiteaError as e:
|
||||
log.warning("refresh_meta_repo: cannot list rfcs/: %s", e)
|
||||
log.warning("refresh_meta_repo: project %s: cannot list rfcs/: %s", project_id, e)
|
||||
return
|
||||
|
||||
seen_slugs: set[str] = set()
|
||||
@@ -62,24 +71,28 @@ async def refresh_meta_repo(config: Config, gitea: Gitea) -> None:
|
||||
try:
|
||||
entry = entry_mod.parse(text)
|
||||
except Exception as parse_err:
|
||||
log.warning("refresh_meta_repo: skipping %s: %s", f["path"], parse_err)
|
||||
log.warning("refresh_meta_repo: %s: skipping %s: %s", project_id, f["path"], parse_err)
|
||||
continue
|
||||
if not entry.slug:
|
||||
log.warning("refresh_meta_repo: skipping %s: missing slug", f["path"])
|
||||
log.warning("refresh_meta_repo: %s: skipping %s: missing slug", project_id, f["path"])
|
||||
continue
|
||||
seen_slugs.add(entry.slug)
|
||||
_upsert_cached_rfc(entry, body_sha=sha)
|
||||
_upsert_cached_rfc(entry, body_sha=sha, project_id=project_id)
|
||||
|
||||
# Mark entries removed from the meta repo as withdrawn-without-trace.
|
||||
# In practice the spec keeps withdrawn entries in rfcs/ as historical
|
||||
# record (§3), so this branch fires only for entries deleted out of
|
||||
# band. We leave the row but flag it for reconciler attention.
|
||||
existing = {row["slug"] for row in db.conn().execute("SELECT slug FROM cached_rfcs")}
|
||||
# Entries removed from a project's rfcs/ — the spec keeps withdrawn entries
|
||||
# as historical record (§3), so this fires only for out-of-band deletes;
|
||||
# leave the row, scoped to this project, for reconciler attention.
|
||||
existing = {
|
||||
row["slug"]
|
||||
for row in db.conn().execute(
|
||||
"SELECT slug FROM cached_rfcs WHERE project_id = ?", (project_id,)
|
||||
)
|
||||
}
|
||||
for missing in existing - seen_slugs:
|
||||
log.info("refresh_meta_repo: %s no longer in rfcs/ — leaving cache row in place", missing)
|
||||
log.info("refresh_meta_repo: %s/%s no longer in rfcs/ — leaving cache row", project_id, missing)
|
||||
|
||||
|
||||
def _upsert_cached_rfc(entry: entry_mod.Entry, body_sha: str) -> None:
|
||||
def _upsert_cached_rfc(entry: entry_mod.Entry, body_sha: str, project_id: str = "default") -> None:
|
||||
# §6.6: models_json stays NULL when the frontmatter key is absent
|
||||
# (inherit operator universe) and '[]' for the explicit opt-out.
|
||||
models_json = json.dumps(entry.models) if entry.models is not None else None
|
||||
@@ -92,9 +105,9 @@ def _upsert_cached_rfc(entry: entry_mod.Entry, body_sha: str) -> None:
|
||||
(slug, title, state, rfc_id, repo, proposed_by, proposed_at,
|
||||
graduated_at, graduated_by, owners_json, arbiters_json, tags_json,
|
||||
models_json, funder_login, body, body_sha,
|
||||
unreviewed, reviewed_at, reviewed_by,
|
||||
unreviewed, reviewed_at, reviewed_by, project_id,
|
||||
last_entry_commit_at, updated_at)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, datetime('now'), datetime('now'))
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, datetime('now'), datetime('now'))
|
||||
ON CONFLICT(project_id, slug) DO UPDATE SET
|
||||
title = excluded.title,
|
||||
state = excluded.state,
|
||||
@@ -137,6 +150,7 @@ def _upsert_cached_rfc(entry: entry_mod.Entry, body_sha: str) -> None:
|
||||
1 if entry.unreviewed else 0,
|
||||
entry.reviewed_at,
|
||||
entry.reviewed_by,
|
||||
project_id,
|
||||
),
|
||||
)
|
||||
|
||||
|
||||
@@ -0,0 +1,67 @@
|
||||
"""§22.4 (Plan B) — per-project RFC serving. A second project's corpus renders
|
||||
under its own slug namespace via /api/projects/{pid}/rfcs[/{slug}], isolated
|
||||
from the default project and gated by §22.5 visibility."""
|
||||
from __future__ import annotations
|
||||
|
||||
from fastapi.testclient import TestClient
|
||||
|
||||
from test_propose_vertical import ( # noqa: F401
|
||||
app_with_fake_gitea, tmp_env, provision_user_row, sign_in_as,
|
||||
)
|
||||
|
||||
|
||||
def _add_project(pid, name, vis="public"):
|
||||
from app import db
|
||||
db.conn().execute(
|
||||
"INSERT OR IGNORE INTO projects (id, name, type, content_repo, visibility, initial_state) "
|
||||
"VALUES (?, ?, 'document', ?, ?, 'super-draft')",
|
||||
(pid, name, pid + "-content", vis),
|
||||
)
|
||||
|
||||
|
||||
def _add_rfc(slug, title, pid, state="active"):
|
||||
from app import db
|
||||
db.conn().execute(
|
||||
"INSERT INTO cached_rfcs (slug, title, state, project_id) VALUES (?, ?, ?, ?)",
|
||||
(slug, title, state, pid),
|
||||
)
|
||||
|
||||
|
||||
def test_catalog_scoped_to_one_project(app_with_fake_gitea):
|
||||
app, _ = app_with_fake_gitea
|
||||
with TestClient(app) as client:
|
||||
_add_project("ecomm", "Ecomm")
|
||||
_add_rfc("intro", "Default Intro", "default")
|
||||
_add_rfc("intro", "Ecomm Intro", "ecomm")
|
||||
_add_rfc("only-ecomm", "Ecomm Only", "ecomm")
|
||||
|
||||
d = client.get("/api/projects/default/rfcs").json()["items"]
|
||||
e = client.get("/api/projects/ecomm/rfcs").json()["items"]
|
||||
d_slugs = {i["slug"] for i in d}
|
||||
e_slugs = {i["slug"] for i in e}
|
||||
assert "intro" in d_slugs and "only-ecomm" not in d_slugs
|
||||
assert {"intro", "only-ecomm"} <= e_slugs
|
||||
|
||||
|
||||
def test_entry_is_isolated_by_project(app_with_fake_gitea):
|
||||
app, _ = app_with_fake_gitea
|
||||
with TestClient(app) as client:
|
||||
_add_project("ecomm", "Ecomm")
|
||||
_add_rfc("intro", "Default Intro", "default")
|
||||
_add_rfc("intro", "Ecomm Intro", "ecomm")
|
||||
_add_rfc("only-ecomm", "Ecomm Only", "ecomm")
|
||||
|
||||
assert client.get("/api/projects/default/rfcs/intro").json()["title"] == "Default Intro"
|
||||
assert client.get("/api/projects/ecomm/rfcs/intro").json()["title"] == "Ecomm Intro"
|
||||
# a slug that exists only in ecomm 404s under default
|
||||
assert client.get("/api/projects/ecomm/rfcs/only-ecomm").status_code == 200
|
||||
assert client.get("/api/projects/default/rfcs/only-ecomm").status_code == 404
|
||||
|
||||
|
||||
def test_gated_project_catalog_404s_for_anon(app_with_fake_gitea):
|
||||
app, _ = app_with_fake_gitea
|
||||
with TestClient(app) as client:
|
||||
_add_project("secret", "Secret", vis="gated")
|
||||
_add_rfc("hush", "Hush", "secret")
|
||||
assert client.get("/api/projects/secret/rfcs").status_code == 404
|
||||
assert client.get("/api/projects/secret/rfcs/hush").status_code == 404
|
||||
Generated
+2
-2
@@ -1,12 +1,12 @@
|
||||
{
|
||||
"name": "rfc-app-frontend",
|
||||
"version": "0.34.0",
|
||||
"version": "0.36.0",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "rfc-app-frontend",
|
||||
"version": "0.34.0",
|
||||
"version": "0.36.0",
|
||||
"dependencies": {
|
||||
"@amplitude/unified": "^1.1.9",
|
||||
"@codemirror/commands": "^6.10.3",
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "rfc-app-frontend",
|
||||
"private": true,
|
||||
"version": "0.36.0",
|
||||
"version": "0.37.0",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "vite",
|
||||
|
||||
+12
-4
@@ -182,12 +182,20 @@ export async function getProject(projectId) {
|
||||
return jsonOrThrow(await fetch(`/api/projects/${projectId}`))
|
||||
}
|
||||
|
||||
export async function listRFCs() {
|
||||
return jsonOrThrow(await fetch('/api/rfcs'))
|
||||
// §22.4 (Plan B): per-project serving. Given a projectId, read the
|
||||
// project-scoped routes so a non-default project's corpus renders; without
|
||||
// one, fall back to the default-project compat path.
|
||||
export async function listRFCs(projectId) {
|
||||
const url = projectId ? `/api/projects/${projectId}/rfcs` : '/api/rfcs'
|
||||
return jsonOrThrow(await fetch(url))
|
||||
}
|
||||
|
||||
export async function getRFC(slug) {
|
||||
return jsonOrThrow(await fetch(`/api/rfcs/${slug}`))
|
||||
export async function getRFC(projectId, slug) {
|
||||
// Back-compat: getRFC(slug) (one arg) still hits the unscoped default path.
|
||||
if (slug === undefined) {
|
||||
return jsonOrThrow(await fetch(`/api/rfcs/${projectId}`))
|
||||
}
|
||||
return jsonOrThrow(await fetch(`/api/projects/${projectId}/rfcs/${slug}`))
|
||||
}
|
||||
|
||||
export async function listProposals() {
|
||||
|
||||
@@ -34,9 +34,9 @@ export default function Catalog({ viewer, onProposeRFC, version }) {
|
||||
const pid = useProjectId()
|
||||
|
||||
useEffect(() => {
|
||||
listRFCs().then(d => setRfcs(d.items)).catch(() => setRfcs([]))
|
||||
listRFCs(pid).then(d => setRfcs(d.items)).catch(() => setRfcs([]))
|
||||
listProposals().then(d => setProposals(d.items)).catch(() => setProposals([]))
|
||||
}, [version])
|
||||
}, [version, pid])
|
||||
|
||||
const filtered = useMemo(() => {
|
||||
const needle = search.trim().toLowerCase()
|
||||
|
||||
@@ -1,18 +0,0 @@
|
||||
// §22.10 (M3) — the guard placeholder. A project that exists in the registry
|
||||
// but whose corpus the backend does not yet serve (everything except the
|
||||
// corpus-served default, until Plan B lands per-project RFC serving) renders
|
||||
// this instead of mislabeled default-project content.
|
||||
export default function NotServedPlaceholder({ project }) {
|
||||
const name = project?.name || 'This project'
|
||||
return (
|
||||
<main className="chrome-pane">
|
||||
<div className="welcome">
|
||||
<h1>{name}</h1>
|
||||
<p>
|
||||
This project is registered, but its content isn't being served
|
||||
here yet. Per-project content arrives in a later release.
|
||||
</p>
|
||||
</div>
|
||||
</main>
|
||||
)
|
||||
}
|
||||
@@ -16,8 +16,6 @@ import { createContext, useContext, useEffect, useState } from 'react'
|
||||
import { useParams } from 'react-router-dom'
|
||||
import { getProject } from '../api'
|
||||
import { brandTitle } from '../lib/brand'
|
||||
import { useDeployment } from '../context/DeploymentProvider'
|
||||
import NotServedPlaceholder from './NotServedPlaceholder.jsx'
|
||||
|
||||
const ProjectContext = createContext(null)
|
||||
|
||||
@@ -42,7 +40,6 @@ const THEME_TOKENS = {
|
||||
|
||||
export default function ProjectLayout({ children }) {
|
||||
const { projectId } = useParams()
|
||||
const deployment = useDeployment()
|
||||
const [project, setProject] = useState(null)
|
||||
const [status, setStatus] = useState('loading') // loading | ready | notfound
|
||||
|
||||
@@ -90,10 +87,11 @@ export default function ProjectLayout({ children }) {
|
||||
)
|
||||
}
|
||||
|
||||
const served = projectId === deployment.defaultProjectId
|
||||
// §22.4 (Plan B): every registry project now serves its own corpus, so
|
||||
// there is no "not served" guard — a readable project renders its children.
|
||||
return (
|
||||
<ProjectContext.Provider value={{ project, projectId, served, noun: entryNoun(project?.type) }}>
|
||||
{served ? children : <NotServedPlaceholder project={project} />}
|
||||
<ProjectContext.Provider value={{ project, projectId, noun: entryNoun(project?.type) }}>
|
||||
{children}
|
||||
</ProjectContext.Provider>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -43,11 +43,10 @@ describe('ProjectLayout', () => {
|
||||
expect(document.documentElement.style.getPropertyValue('--c-accent')).toBe('')
|
||||
})
|
||||
|
||||
it('shows the not-served placeholder for a non-default (Plan-B) project', async () => {
|
||||
it('renders the corpus for a non-default project too (Plan B: every project serves)', async () => {
|
||||
getProject.mockResolvedValue({ id: 'other', name: 'Ecomm', type: 'bdd', theme: {} })
|
||||
renderAt('/p/other/')
|
||||
expect(await screen.findByText(/isn.t being served here yet/i)).toBeInTheDocument()
|
||||
expect(screen.queryByTestId('corpus')).not.toBeInTheDocument()
|
||||
expect(await screen.findByTestId('corpus')).toBeInTheDocument()
|
||||
})
|
||||
|
||||
it('shows a not-found message when the project 404s', async () => {
|
||||
|
||||
@@ -125,7 +125,7 @@ export default function RFCView({ viewer }) {
|
||||
const [drawerOpen, setDrawerOpen] = useState(false)
|
||||
|
||||
useEffect(() => {
|
||||
getRFC(slug).then(entry => {
|
||||
getRFC(pid, slug).then(entry => {
|
||||
setEntry(entry)
|
||||
// v0.15.0 — analytics: fire RFC Viewed once per slug load.
|
||||
// We key on the slug param rather than the loaded entry so a
|
||||
@@ -140,7 +140,7 @@ export default function RFCView({ viewer }) {
|
||||
setSelectedModel(def || models?.[0]?.id || '')
|
||||
})
|
||||
.catch(() => {})
|
||||
}, [slug])
|
||||
}, [slug, pid])
|
||||
|
||||
// Per §9.4 / §17's routing-collapse rule, super-drafts render through
|
||||
// the same surface as active RFCs — the bot, the chat, the change
|
||||
@@ -179,7 +179,7 @@ export default function RFCView({ viewer }) {
|
||||
setActionError(null)
|
||||
try {
|
||||
const res = await unretireRFC(slug)
|
||||
getRFC(slug).then(setEntry).catch(() => {})
|
||||
getRFC(pid, slug).then(setEntry).catch(() => {})
|
||||
if (res?.state) navigate(entryPath(pid, slug))
|
||||
} catch (err) {
|
||||
setActionError(err.message)
|
||||
@@ -957,7 +957,7 @@ export default function RFCView({ viewer }) {
|
||||
onCompleted={() => {
|
||||
setShowGraduateDialog(false)
|
||||
// The catalog row and the RFC view now reflect `active`.
|
||||
getRFC(slug).then(setEntry).catch(() => {})
|
||||
getRFC(pid, slug).then(setEntry).catch(() => {})
|
||||
getRFCMain(slug).then(setMainView).catch(() => {})
|
||||
}}
|
||||
/>
|
||||
|
||||
Reference in New Issue
Block a user