The admin page offered "User Plugin Uploads" and "Require Admin
Approval", but nothing in Settings used them: there was no upload, no
uninstall and no way to ask for approval, and the admin had no list of
requests. The approval code that existed pinned a hash of the code only
and showed the admin a manifest the browser sent, never the code.
Now:
- Settings > Plugins has an install button (when uploads are allowed),
a remove button for the user's own plugins, and lists in plain
language what each plugin may do, high-risk items first
- an install sends the ZIP to the server, which reads the manifest and
code itself and pins the request to a hash of both
- the admin Plugins tab lists requests with the code and approves or
denies them; a user may have 10 open requests
- a user plugin is checked again on every start, so a revoked approval
or turning user plugins off takes effect
- the consent dialog and the plugin settings are translated, use the
theme colours, and show the sites a plugin may send data to
Every existing plugin slot renders wherever it's placed in the page (a
toolbar row, an attachment's hover-action bar, etc.) and is constrained
by that spot's own CSS - fine for small controls, but there's no way
for a plugin to show a large custom UI (a graphical file browser, a
multi-step wizard) since even a position:fixed overlay rendered from
inside such a slot is clipped to the surrounding row's own layout, not
the real page (confirmed live building the webdav-storage plugin: a
folder browser rendered but was squeezed into an unusable sliver).
Adds:
- host-dialog.ts: new 'custom' DialogKind + awaitCustomDialog(), reusing
the existing confirm/alert/prompt queue plumbing.
- host-api.ts: 'ui.openDialog' API method.
- runtime.tsx: sandbox-side api.ui.openDialog(opts) proxy.
- plugin-types.ts: new 'plugin-dialog' SlotName - a plugin implements
this slot with its actual custom UI.
- plugin-dialog-host.tsx: renders the requested slot via the existing
PluginIframeSlot INSIDE the dialog's real app-root fixed overlay
(position: fixed; inset: 0, above everything) instead of the
confirm/prompt form, for kind: 'custom'. Resolves via a generic
onResult callback in extraProps (marshalled the same way
composer-attachment-source's onAttach already is - no new plumbing
needed there).
A plugin calls api.ui.openDialog({ slot: 'plugin-dialog', extraProps,
title, width }) and awaits the Promise, which resolves to whatever its
own slot component passes to props.onResult(), or null if the user
closes the dialog without doing so.
The plugin runtime received the active locale (init payload + 'locale-change')
and plugins could declare a `locales` map, but none of it was usable: the
locales never reached the runtime, and buildPluginApi exposed no i18n. So
plugin code calling pluginApi.i18n.t(...) (as the External Link Warning plugin
does) always got undefined and fell back to English.
Thread plugin locales end to end and surface an i18n API:
- ServerPlugin gains `locales`; the upload route persists manifest.locales
(alongside configSchema/settingsSchema), and /api/plugins surfaces it to the
client so it flows registry -> client -> sandbox host-bridge -> runtime.
- runtime sets __PLUGIN_LOCALE__ at init (not only on later 'locale-change')
and buildPluginApi exposes `i18n.locale` + `i18n.t(key, vars)` resolving
against the plugin's declared locales (manifest.locales) with English/key
fallback and {placeholder} interpolation.
Lets any sandboxed plugin localize its strings from its manifest.