Files
platform/src/apps/officer-web/Screens/Dashboard/PluginScreen.tsx
T
pastilhas 6b5c87da10 a plugin can be more than one screen
a plugin was one screen because the two things it declares — a mount prefix and
a frontend — happened to be the same string. caldav is where that stops holding:
one sidecar, one permission, one install, and two screens that have nothing to
do with each other on screen. calendar and contacts are not sections of one
page, and forcing them into one would be a worse product than the core screens
they replace.

nothing else moved. all eighteen plugins with a frontend use flat
web/panels.ts, which is now "one screen, at the mount prefix, labelled by the
manifest" — the same file, the same route, the same persisted layout key. the
nested form is opt-in and caldav is its only user.

the split of who declares what follows the rule this file already had. the
DIRECTORY declares that a screen exists (web/<segment>/panels.ts); the MANIFEST
says what to call it and which glyph to draw, because a directory listing cannot
say either and those are a human's choice. the two are cross-checked and a
mismatch is refused rather than reconciled: a segment with no directory renders
nothing, a directory with no entry has no tile and no label, and both are the
quiet kind of broken.

the part that needed the most care is that a segment is a URL the plugin CHOSE
— /calendar, not /caldav/calendar. mountPrefix is what makes it impossible for a
plugin to shadow a core route, and a chosen segment steps straight out of it. so
this is first-party only, refused at discovery, and tested: without that check
this field is a way for any installed plugin to take over /files.

screenId, not appName, is now the layout key and the panel allow-list's owner.
two screens sharing one would persist over each other's sizes and each would
accept the other's panels. existing plugins get screenId === appName, so no
stored layout is orphaned — verified by regenerating.
2026-08-27 04:15:51 +00:00

51 lines
2.5 KiB
TypeScript

import type { LayoutNode, AppRegistryMeta } from 'officerdev';
import { WorkspaceView } from 'officerdev';
import { useAuthoredLayout } from 'state/useAuthoredLayout';
// The screen every plugin route renders. THE PLUGIN DOES NOT RENDER A SCREEN.
//
// ── The rule, and why it is shape rather than policy ──
//
// Every plugin route renders a Workspace with at least one panel. A plugin that exported a component
// could render anything at all — a bare div, a full-page form, its own navigation — and the platform
// would be a shell hosting strangers' layouts rather than one application. So a plugin does not get to
// render the screen: it contributes panels and says how they are arranged, and this renders the
// Workspace around them.
//
// Non-compliance is therefore not refused, it is unrepresentable. There is nowhere to put a screen.
//
// `locked`, like every core screen: a plugin's layout is its author's design, not a workspace the user
// rearranges — and `appTypes.allowed` pins it to that plugin's own panels, so a persisted layout naming
// something else falls back rather than rendering another plugin's panel inside this one.
export function PluginScreen({
screenId,
panels,
layout,
}: {
/**
* This SCREEN's id, not the plugin's — `caldav/calendar`, not `caldav`.
*
* A plugin with two screens has two of these, and they must differ: the id is both the layout
* persistence key and the owner of the panel allow-list below, so sharing one would have Calendar and
* Contacts persisting over each other's sizes and each accepting the other's panels.
*/
screenId: string;
panels: AppRegistryMeta[];
layout: LayoutNode;
}) {
// Per-user and per-plugin, so two plugins never share a layout and a user's arrangement is their own.
//
// `useAuthoredLayout`, not `useDashboardState`: the STRUCTURE here is the plugin author's and comes from
// `layout` on every render, while only the sizes are the user's and persisted. That is what lets a plugin
// ship a new layout and have it reach people who have already opened the screen — and it is what stops
// the `appTypes` repair below from being written back as though the user had chosen it. See the hook.
const workspace = useAuthoredLayout(`screens/plugin/${screenId}`, layout);
const allowed = panels.map((panel) => panel.key);
return (
<div className="h-full w-full pt-2">
<WorkspaceView workspace={workspace} locked appTypes={{ allowed, fallback: allowed[0] ?? '' }} />
</div>
);
}