give :section back — the splat took it from fifteen plugins

`/<route>` and `/<route>/:section` are what every plugin has always had, and
fifteen of them read useParams().section. Replacing the pair with a splat took
that parameter away from all of them at once: the screens still mounted and every
one lost its selection, which is worse than a 404 because nothing says so.

The splat stays, for depth. React-router ranks a static or dynamic segment above
it, so a plugin with one segment gets `section` again and only deeper paths fall
through.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-09-03 12:37:14 +01:00
co-authored by Claude Opus 5
parent b2349525fd
commit eaa92738dc
+19 -12
View File
@@ -101,24 +101,31 @@ export function App() {
{/* One route per SCREEN, not per plugin — `screenId` is the key, because a plugin with two {/* One route per SCREEN, not per plugin — `screenId` is the key, because a plugin with two
screens (caldav: Calendar and Contacts) contributes two entries sharing one `appName`. */} screens (caldav: Calendar and Contacts) contributes two entries sharing one `appName`. */}
{/* {/*
A SPLAT, so the whole subtree is the plugin's. Three routes, and the third is the only new one.
It was a pair — the bare route and `/:section` — which gave a plugin exactly one segment and `/<route>` and `/<route>/:section` are what every plugin has always had, and fifteen of
made the platform the author of what it meant. Anything deeper had to go in the query them read `useParams().section`. A splat ALONE replaced that for a day and took the
string, and `/fofos-admin/layout/header` was not expressible at all. parameter away from all of them at once — the screens still mounted and every one of them
lost its selection, which is a worse failure than a 404 because nothing says so.
A splat matches zero segments too, so this is one route where there were two, and the The splat is added for depth rather than instead of them: `/fofos-admin/layout/header` was
plugin reads its own position off the location. Routing inside a plugin is the plugin's, not expressible before, and a plugin that wants its own subtree reads the location.
which is the same line `mountPrefix` already draws around its API: the platform owns where React-router ranks a static or dynamic segment above a splat, so a plugin with one segment
a plugin STARTS and nothing after it. still gets `section` and only deeper paths fall through.
*/} */}
{installedPlugins.map((plugin) => ( {installedPlugins.flatMap((plugin) => [
<Route key={plugin.screenId} path={plugin.route} element={<Dashboard.PluginScreen {...plugin} />} />,
<Route <Route
key={plugin.screenId} key={`${plugin.screenId}-section`}
path={`${plugin.route}/:section`}
element={<Dashboard.PluginScreen {...plugin} />}
/>,
<Route
key={`${plugin.screenId}-deep`}
path={`${plugin.route}/*`} path={`${plugin.route}/*`}
element={<Dashboard.PluginScreen {...plugin} />} element={<Dashboard.PluginScreen {...plugin} />}
/> />,
))} ])}
<Route path="/system-monitor" element={<Dashboard.SystemMonitorScreen />} /> <Route path="/system-monitor" element={<Dashboard.SystemMonitorScreen />} />
<Route path="/system-monitor/:scope" element={<Dashboard.SystemMonitorScreen />} /> <Route path="/system-monitor/:scope" element={<Dashboard.SystemMonitorScreen />} />
{/* TEMPORARY / EXPERIMENTAL — offline file transfer over animated QR codes */} {/* TEMPORARY / EXPERIMENTAL — offline file transfer over animated QR codes */}