Let a plugin add links to the profile menu, and list the setting conflict kind
CI / full-gate (push) Failing after 1m17s

This commit is contained in:
2026-11-08 19:17:57 +01:00
parent 6ba21b8409
commit 777c0bd86d
17 changed files with 141 additions and 34 deletions
+29 -1
View File
@@ -2,6 +2,7 @@
// ctx.chrome. `nav` is the global menu — Dashboard plus every plugin's fragment — run through
// composeNav (override + per-user filter) and current-marked for the request path.
import { allows, type Gate } from "../auth/gate.ts";
import type { User } from "../http/context.ts";
import { ENGLISH } from "../i18n/english.ts";
import type { Translate } from "../i18n/translate.ts";
@@ -11,9 +12,21 @@ import type { Plugin } from "../plugin-host/plugin.ts";
const DASHBOARD_NAV: NavNode = { href: "/dashboard", icon: "i-grid", id: "dashboard", label: "nav.dashboard", session: true };
// A link in the shell's profile dropdown, above Sign out.
export interface ProfileLink {
href: string;
icon?: string; // a Lucide sprite id (src/ui/icons.ts)
label: string;
}
// What a plugin's `profileMenu` declares: a link plus the one gate that decides who sees it.
export interface ProfileMenuEntry extends ProfileLink, Gate {}
// `menu` rides on the user, which every view already hands to the shell, so no view needs a new local.
export interface ShellUser {
email: string;
initials: string;
menu?: ProfileLink[]; // permission-filtered, translated and locale-carried; the host always sets it, a hand-built fixture may omit it
name: string;
}
@@ -66,10 +79,25 @@ export function buildPluginChrome(opts: ChromeOptions): PageChrome {
nav: carryLocaleInto(nav, carryLocale),
signInHref: carryLocale(returnTo),
...(theme != null ? { theme } : {}),
user: shellUser(opts.user, t),
user: { ...shellUser(opts.user, t), menu: profileLinks(opts, t, carryLocale) },
};
}
// A plugin's labels are keys in *its* catalog, hence its own translator. Only a signed-in visitor
// has a profile menu; the gate then decides per entry.
function profileLinks(opts: ChromeOptions, t: Translate, carryLocale: (href: string) => string): ProfileLink[] {
const user = opts.user ?? null;
if (!user) return [];
return (opts.plugins ?? []).flatMap((plugin) => {
const pluginT = opts.translatorFor?.(plugin.id) ?? t;
return (plugin.profileMenu ?? []).filter((entry) => allows(entry, user)).map((entry) => ({
href: carryLocale(entry.href),
...(entry.icon != null ? { icon: entry.icon } : {}),
label: pluginT(entry.label),
}));
});
}
// The JWT holds only id/email/permissions, so the profile name is the email's local part.
export function shellUser(user: User | null | undefined, t: Translate = ENGLISH): ShellUser {
if (!user) {