a plugin screen can declare its routes

Every screen got `/<route>` and `/<route>/:section` and nothing deeper, so a
section had to live in the query string. A screen may now name its routes in
the manifest, and give one its own arrangement of the screen's panels.

Absent, the old pair is unchanged — which is every plugin but fofos-admin.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-09-01 04:57:18 +01:00
co-authored by Claude Opus 5
parent 43f32a7440
commit 1fe767ea77
5 changed files with 200 additions and 16 deletions
+29 -1
View File
@@ -96,8 +96,25 @@ export function generatePluginsModule(states: PluginState[]): string {
// would accept the other's panels.
const screenId = screen.segment ? `${plugin.appName}/${screen.segment}` : plugin.appName;
const route = screen.segment ? `/${screen.segment}` : mountPrefix(plugin);
/*
The screen's route table, each entry carrying the layout it renders.
A route whose layout is the screen's own reuses `${alias}Layout` rather than importing the same
module twice — the specifier would be identical, and two imports of one file under two names is
two objects where the layout key assumes one.
*/
const routeEntries = screen.routes.map((route_, index) => {
let layoutRef = `${alias}Layout`;
if (route_.layout !== screen.layout) {
layoutRef = `${alias}Route${index}Layout`;
imports.push(`import { defaultLayout as ${layoutRef} } from '${specifier(route_.layout)}';`);
}
return `{ path: ${JSON.stringify(route_.path)}, layout: ${layoutRef} }`;
});
entries.push(
` { appName: '${plugin.appName}', screenId: '${screenId}', route: '${route}', panels: ${alias}Panels, layout: ${alias}Layout },`,
` { appName: '${plugin.appName}', screenId: '${screenId}', route: '${route}', panels: ${alias}Panels, layout: ${alias}Layout, routes: [${routeEntries.join(', ')}] },`,
);
}
}
@@ -133,6 +150,17 @@ export type GeneratedPlugin = {
panels: AppRegistryMeta[];
/** How they are arranged. The shell renders \`WorkspaceView\` around them. */
layout: LayoutNode;
/**
* The screen's own route table, when it declared one.
*
* Empty means the screen takes the pair every plugin has always had — \`/<route>\` and
* \`/<route>/:section\`. A table replaces that pair entirely: the plugin has said what its URLs are,
* so a guessed one alongside them would be a route nothing points at.
*
* \`path\` is react-router's, relative to \`route\`. It is also the LAYOUT KEY suffix, so two routes
* with different arrangements remember their own dividers.
*/
routes: { path: string; layout: LayoutNode }[];
};
export const plugins: GeneratedPlugin[] = [