An action declared with ui.setActions may carry a badge, a whole number
from 0. On buttons it is drawn as the search's filter button draws its
count: the button tinted, the number in a bubble at its corner, "99+"
past two digits. In menus it follows the label, as the mobile header's
notification count does. 0 shows none.
Some user-facing strings still said "Bulwark" on a deployment with its own
APP_NAME: Settings > General > Default apps (protocol handlers), the "Open
Bulwark" notification, the rate-limit banner and toast, the plugin connect
dialog, one plugin permission text and the connector page.
These strings now take an {appName} placeholder, filled by productName():
the configured APP_NAME, or "Bulwark" when it is unset or the default
"Webmail" (the same rule as the TOTP issuer and the About card, #1160).
A stock install reads as before.
The settings search (the settings page and api.app.searchSettings) indexes
these texts with the name filled in, so "bulwark" or the configured name
finds Default apps and no result carries the raw placeholder.
Locales: the 13 keys in every locale that has "Bulwark" in them. Languages
that inflect the name (cs, et, hu, ko, ro, sk, tr) use a neutral form such
as "{appName} uygulamasında" instead of a declined "Bulwark'ta"; a native
check of those is welcome.
Not changed on purpose: the mobile app, push relay, Lite, demo and admin
texts, which speak about Bulwark itself.
Closes#1314
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- Add Combobox component supporting single and multiselect modes, search filtering, and clear button
- Implement floating dropdown bridge and host slot portal so dropdown renders cleanly outside plugin iframe
- Fix clear button and chip removals to not unexpectedly open the dropdown
- Add comprehensive unit tests covering Combobox options, clear behavior, multiselect, and bridge communication
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.