Compare commits
4 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| afa8d26378 | |||
| 948ee88160 | |||
| 7bcf784d06 | |||
| 8e207a60e6 |
@@ -23,6 +23,31 @@ skip versions are the composition of each intervening adjacent
|
|||||||
release's steps in order — no A-to-B path is pre-computed beyond
|
release's steps in order — no A-to-B path is pre-computed beyond
|
||||||
that.
|
that.
|
||||||
|
|
||||||
|
## 0.52.3 — 2026-06-09
|
||||||
|
|
||||||
|
**Patch — unmatched routes render a 404 instead of a blank page or a
|
||||||
|
silent redirect home (no operator action required).**
|
||||||
|
|
||||||
|
- **The top-level catch-all redirected any unknown path to `/`**, and the
|
||||||
|
nested `/admin/*`, `/p/:projectId/*`, and `/docs/*` route groups had no
|
||||||
|
catch-all (so an invalid subpath rendered an empty pane). Added a shared
|
||||||
|
`NotFound` 404 page and wired it into all four route groups, so a bad or
|
||||||
|
typo'd URL — including a stale `/admin/users/allowlist/graduation`-style
|
||||||
|
path — now shows a clear "page not found" with a link back to the catalog.
|
||||||
|
(Client-side 404 UI; the SPA still serves over HTTP 200, as before.)
|
||||||
|
|
||||||
|
## 0.52.2 — 2026-06-09
|
||||||
|
|
||||||
|
**Patch — admin sidebar nav links no longer accumulate URL segments (no
|
||||||
|
operator action required).**
|
||||||
|
|
||||||
|
- **The `/admin` left-rail links were relative**, so under the `/admin/*`
|
||||||
|
nested route each click resolved against the current URL and *appended* a
|
||||||
|
segment — e.g. clicking Users → Allowlist → Graduation produced
|
||||||
|
`/admin/users/allowlist/graduation` instead of `/admin/graduation`. Fixed:
|
||||||
|
the rail `NavLink`s now use absolute targets (`/admin/<tab>`) with `end`
|
||||||
|
for exact active-state matching.
|
||||||
|
|
||||||
## 0.52.1 — 2026-06-08
|
## 0.52.1 — 2026-06-08
|
||||||
|
|
||||||
**Patch — collection-scoped entry-detail fetch fix (no operator action
|
**Patch — collection-scoped entry-detail fetch fix (no operator action
|
||||||
|
|||||||
+15
-6
@@ -43,15 +43,22 @@ test('SLICE-4: edit one entry\'s priority from the detail panel', async ({ page
|
|||||||
|
|
||||||
const panel = page.locator('.metadata-fields-panel')
|
const panel = page.locator('.metadata-fields-panel')
|
||||||
await expect(panel).toBeVisible()
|
await expect(panel).toBeVisible()
|
||||||
// Seeded at P1; change to P2 and save (direct sidecar commit, D7).
|
const select = panel.locator('#mf-priority')
|
||||||
await expect(panel.locator('#mf-priority')).toHaveValue('P1')
|
// Value-agnostic so the test is idempotent across retries and re-runs: a
|
||||||
await panel.locator('#mf-priority').selectOption('P2')
|
// prior (committed) edit may have already moved it off the P1 seed. Pick a
|
||||||
|
// target distinct from the current value so Save is a real change.
|
||||||
|
const current = await select.inputValue()
|
||||||
|
const target = current === 'P2' ? 'P1' : 'P2'
|
||||||
|
await select.selectOption(target)
|
||||||
await panel.getByRole('button', { name: 'Save' }).click()
|
await panel.getByRole('button', { name: 'Save' }).click()
|
||||||
await expect(panel.getByText('Saved')).toBeVisible()
|
// The save is a real git sidecar commit via the bot (D7); over the deployed
|
||||||
|
// edge the round trip can take tens of seconds — wait generously for the
|
||||||
|
// confirmation rather than the default expect timeout.
|
||||||
|
await expect(panel.getByText('Saved')).toBeVisible({ timeout: 60_000 })
|
||||||
|
|
||||||
// Persisted across a reload (read back from the committed sidecar).
|
// Persisted across a reload (read back from the committed sidecar).
|
||||||
await page.reload()
|
await page.reload()
|
||||||
await expect(page.locator('.metadata-fields-panel #mf-priority')).toHaveValue('P2')
|
await expect(page.locator('.metadata-fields-panel #mf-priority')).toHaveValue(target)
|
||||||
})
|
})
|
||||||
|
|
||||||
// SLICE-5 (PUC-2) — multi-select + bulk action bar, one commit (authed).
|
// SLICE-5 (PUC-2) — multi-select + bulk action bar, one commit (authed).
|
||||||
@@ -71,8 +78,10 @@ test('SLICE-5: bulk-set priority on multiple selected entries', async ({ page })
|
|||||||
await expect(bar.getByText('2 selected')).toBeVisible()
|
await expect(bar.getByText('2 selected')).toBeVisible()
|
||||||
|
|
||||||
// Set priority P1 across both → one commit; a toast reports the result.
|
// Set priority P1 across both → one commit; a toast reports the result.
|
||||||
|
// Like SLICE-4, the bulk write is a real git commit via the bot; allow the
|
||||||
|
// deployed-edge round trip generous time before the result toast appears.
|
||||||
await bar.getByLabel('Set Priority').selectOption('P1')
|
await bar.getByLabel('Set Priority').selectOption('P1')
|
||||||
await expect(page.getByText(/2 updated/)).toBeVisible()
|
await expect(page.getByText(/2 updated/)).toBeVisible({ timeout: 60_000 })
|
||||||
|
|
||||||
// The change is reflected server-side: the Priority P1 facet now counts the
|
// The change is reflected server-side: the Priority P1 facet now counts the
|
||||||
// two newly-updated entries plus the pre-existing P1 (checkout-returning was
|
// two newly-updated entries plus the pre-existing P1 (checkout-returning was
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"name": "rfc-app-frontend",
|
"name": "rfc-app-frontend",
|
||||||
"private": true,
|
"private": true,
|
||||||
"version": "0.52.1",
|
"version": "0.52.3",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "vite",
|
"dev": "vite",
|
||||||
|
|||||||
@@ -2687,3 +2687,19 @@ select:focus-visible,
|
|||||||
align-items: flex-start;
|
align-items: flex-start;
|
||||||
gap: 12px;
|
gap: 12px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* 404 / not-found page (client-side, for unmatched routes) */
|
||||||
|
.not-found {
|
||||||
|
max-width: 32rem;
|
||||||
|
margin: 4rem auto;
|
||||||
|
padding: 0 1.5rem;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
.not-found-code {
|
||||||
|
font-size: 3.5rem;
|
||||||
|
line-height: 1;
|
||||||
|
margin: 0 0 0.5rem;
|
||||||
|
color: var(--c-gray-900, #111);
|
||||||
|
}
|
||||||
|
.not-found-message { color: var(--c-gray-700, #444); margin: 0 0 1.25rem; }
|
||||||
|
.not-found-actions a { color: var(--c-gray-900, #111); text-decoration: underline; }
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ import { brandTitle } from './lib/brand'
|
|||||||
import { entryPath, proposalPath, DEFAULT_COLLECTION } from './lib/entryPaths'
|
import { entryPath, proposalPath, DEFAULT_COLLECTION } from './lib/entryPaths'
|
||||||
import { useDeployment } from './context/DeploymentProvider'
|
import { useDeployment } from './context/DeploymentProvider'
|
||||||
import ProjectLayout from './components/ProjectLayout.jsx'
|
import ProjectLayout from './components/ProjectLayout.jsx'
|
||||||
|
import NotFound from './components/NotFound.jsx'
|
||||||
import Directory from './components/Directory.jsx'
|
import Directory from './components/Directory.jsx'
|
||||||
import ProjectSwitcher from './components/ProjectSwitcher.jsx'
|
import ProjectSwitcher from './components/ProjectSwitcher.jsx'
|
||||||
import Catalog from './components/Catalog.jsx'
|
import Catalog from './components/Catalog.jsx'
|
||||||
@@ -381,13 +382,14 @@ export default function App() {
|
|||||||
<Route path="c/:collectionId/e/:slug" element={<RFCView viewer={viewer} />} />
|
<Route path="c/:collectionId/e/:slug" element={<RFCView viewer={viewer} />} />
|
||||||
<Route path="c/:collectionId/e/:slug/pr/:prNumber" element={<PRView viewer={viewer} />} />
|
<Route path="c/:collectionId/e/:slug/pr/:prNumber" element={<PRView viewer={viewer} />} />
|
||||||
<Route path="c/:collectionId/proposals/:prNumber" element={<ProposalView viewer={viewer} onChange={() => setCatalogVersion(v => v + 1)} />} />
|
<Route path="c/:collectionId/proposals/:prNumber" element={<ProposalView viewer={viewer} onChange={() => setCatalogVersion(v => v + 1)} />} />
|
||||||
|
<Route path="*" element={<NotFound />} />
|
||||||
</Routes>
|
</Routes>
|
||||||
</main>
|
</main>
|
||||||
</ProjectLayout>
|
</ProjectLayout>
|
||||||
} />
|
} />
|
||||||
{/* Any other path (incl. the retired bare-slug corpus URLs that
|
{/* Any unmatched path is a real 404 — surface it rather than
|
||||||
somehow reach the SPA) lands on the deployment landing. */}
|
silently bouncing home, so a bad/typo'd URL is visible. */}
|
||||||
<Route path="*" element={<Navigate to="/" replace />} />
|
<Route path="*" element={<NotFound />} />
|
||||||
</Routes>
|
</Routes>
|
||||||
</div>
|
</div>
|
||||||
{(proposeOpen || proposeParam != null) && viewer && (
|
{(proposeOpen || proposeParam != null) && viewer && (
|
||||||
@@ -507,6 +509,7 @@ function DocsWithSidebar({ viewer }) {
|
|||||||
client-side redirect to the first configured spec. */}
|
client-side redirect to the first configured spec. */}
|
||||||
<Route path="specs" element={<DocsSpecsIndex />} />
|
<Route path="specs" element={<DocsSpecsIndex />} />
|
||||||
<Route path="specs/:name" element={<DocsSpec />} />
|
<Route path="specs/:name" element={<DocsSpec />} />
|
||||||
|
<Route path="*" element={<NotFound />} />
|
||||||
</Route>
|
</Route>
|
||||||
</Routes>
|
</Routes>
|
||||||
</main>
|
</main>
|
||||||
|
|||||||
@@ -29,6 +29,7 @@ import {
|
|||||||
} from '../api.js'
|
} from '../api.js'
|
||||||
import { EVENTS, track } from '../lib/analytics.js'
|
import { EVENTS, track } from '../lib/analytics.js'
|
||||||
import { entryPath, useProjectId } from '../lib/entryPaths'
|
import { entryPath, useProjectId } from '../lib/entryPaths'
|
||||||
|
import NotFound from './NotFound.jsx'
|
||||||
|
|
||||||
// v0.17.0 — roadmap item #16. The max length the backend enforces
|
// v0.17.0 — roadmap item #16. The max length the backend enforces
|
||||||
// (Pydantic body bound + `invites.CUSTOM_MESSAGE_MAX_LENGTH`); kept
|
// (Pydantic body bound + `invites.CUSTOM_MESSAGE_MAX_LENGTH`); kept
|
||||||
@@ -59,7 +60,8 @@ export default function Admin({ viewer }) {
|
|||||||
{tabs.map(t => (
|
{tabs.map(t => (
|
||||||
<li key={t.path}>
|
<li key={t.path}>
|
||||||
<NavLink
|
<NavLink
|
||||||
to={t.path}
|
to={`/admin/${t.path}`}
|
||||||
|
end
|
||||||
className={({ isActive }) => `admin-rail-link ${isActive ? 'active' : ''}`}
|
className={({ isActive }) => `admin-rail-link ${isActive ? 'active' : ''}`}
|
||||||
>
|
>
|
||||||
{t.label}
|
{t.label}
|
||||||
@@ -81,6 +83,7 @@ export default function Admin({ viewer }) {
|
|||||||
{isSiteOwner && <Route path="retired" element={<RetiredTab />} />}
|
{isSiteOwner && <Route path="retired" element={<RetiredTab />} />}
|
||||||
<Route path="audit" element={<AuditTab />} />
|
<Route path="audit" element={<AuditTab />} />
|
||||||
<Route path="permissions" element={<PermissionsTab />} />
|
<Route path="permissions" element={<PermissionsTab />} />
|
||||||
|
<Route path="*" element={<NotFound message="That admin page doesn't exist." />} />
|
||||||
</Routes>
|
</Routes>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -0,0 +1,21 @@
|
|||||||
|
// Client-side 404 for routes that don't match. An SPA serves index.html
|
||||||
|
// for every non-API path (HTTP 200), so an unknown URL would otherwise
|
||||||
|
// render blank — or, worse, silently bounce home. This surfaces a clear
|
||||||
|
// "page not found" instead. Used by the top-level router and by the
|
||||||
|
// nested /admin, /p/:projectId, and /docs route groups so an invalid
|
||||||
|
// subpath (e.g. /admin/users/allowlist/graduation) lands here too.
|
||||||
|
import { Link } from 'react-router-dom'
|
||||||
|
|
||||||
|
export default function NotFound({ message }) {
|
||||||
|
return (
|
||||||
|
<div className="not-found" role="alert">
|
||||||
|
<h1 className="not-found-code">404</h1>
|
||||||
|
<p className="not-found-message">
|
||||||
|
{message || "We couldn't find that page."}
|
||||||
|
</p>
|
||||||
|
<p className="not-found-actions">
|
||||||
|
<Link to="/">Return to the catalog</Link>
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -103,6 +103,17 @@ put_file() {
|
|||||||
201 "create $_repo/$_path"
|
201 "create $_repo/$_path"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
# delete_file <repo> <path> — remove the file if it exists (no-op if absent).
|
||||||
|
delete_file() {
|
||||||
|
_repo="$1"; _path="$2"
|
||||||
|
_sha=$(file_sha "$_repo" "$_path")
|
||||||
|
[ -n "$_sha" ] || { echo "seed-ppe: $_repo/$_path absent, nothing to delete"; return 0; }
|
||||||
|
echo "seed-ppe: deleting $_repo/$_path"
|
||||||
|
mutate DELETE "$GITEA/api/v1/repos/$ORG/$_repo/contents/$_path" \
|
||||||
|
"{\"message\":\"reseed: drop sidecar $_path\",\"sha\":\"$_sha\",\"branch\":\"main\"}" \
|
||||||
|
200 "delete $_repo/$_path"
|
||||||
|
}
|
||||||
|
|
||||||
ensure_repo "$REGISTRY_REPO"
|
ensure_repo "$REGISTRY_REPO"
|
||||||
ensure_repo "$CONTENT_REPO"
|
ensure_repo "$CONTENT_REPO"
|
||||||
|
|
||||||
@@ -182,4 +193,17 @@ tags: [search]
|
|||||||
Faceted search scenario.
|
Faceted search scenario.
|
||||||
" "$RESEED"
|
" "$RESEED"
|
||||||
|
|
||||||
|
# RESEED also drops any metadata sidecars (<slug>.meta.yaml) left by prior
|
||||||
|
# SLICE-4/5 edits. A sidecar takes precedence over the .md frontmatter
|
||||||
|
# (dual-read, §22.4a), so without this a re-run inherits the edited
|
||||||
|
# priorities (everything ends up P1) and the faceted preconditions drift —
|
||||||
|
# SLICE-3 expects P0 count = 2. Dropping the sidecars restores the
|
||||||
|
# frontmatter as the source of truth: checkout-guest=P0, search-facets=P0,
|
||||||
|
# checkout-returning=P1.
|
||||||
|
if [ "$RESEED" = "1" ]; then
|
||||||
|
for _slug in checkout-guest checkout-returning search-facets; do
|
||||||
|
delete_file "$CONTENT_REPO" "bdd/rfcs/$_slug.meta.yaml"
|
||||||
|
done
|
||||||
|
fi
|
||||||
|
|
||||||
echo "seed-ppe: done. Set REGISTRY_REPO=$REGISTRY_REPO on rfc-app-ppe and redeploy."
|
echo "seed-ppe: done. Set REGISTRY_REPO=$REGISTRY_REPO on rfc-app-ppe and redeploy."
|
||||||
|
|||||||
Reference in New Issue
Block a user