Rename the admin example plugin to auth-admin, and let config/menu.ts hide or rename a profile-menu entry
CI / full-gate (push) Successful in 3m32s

This commit is contained in:
2026-11-08 21:25:11 +01:00
parent 34dfbea156
commit 05da1d7897
57 changed files with 317 additions and 270 deletions
+15
View File
@@ -113,3 +113,18 @@ test("profile menu entries are gated like nav nodes, translated by their plugin
assert.deepEqual(entries({ permissions: ["billing:read"] }).map((e) => e.label), ["Inställningar", "account.billing", "Help"]);
assert.deepEqual(entries({ signedIn: false }).map((e) => e.label), []); // an anonymous visitor has no profile menu
});
test("the central override hides and renames a profile-menu entry by its id, which is not rendered", () => {
const links = buildPluginChrome({
menu: { ...DEFAULT_MENU, override: { hide: ["help"], rename: { settings: "Preferences" } } },
plugins: [{
apiVersion: "1.0.0", id: "account",
profileMenu: [
{ href: "/account/settings", id: "settings", label: "account.settings", session: true },
{ href: "/account/help", id: "help", label: "Help", public: true },
],
}],
user: { email: "ada@x.io", id: "u1", permissions: [] },
}).user.profileLinks;
assert.deepEqual(links, [{ href: "/account/settings", label: "Preferences" }]);
});
+13 -6
View File
@@ -20,7 +20,9 @@ export interface ProfileLink {
}
// What a plugin's `profileMenu` declares: a link plus the one gate that decides who sees it.
export interface ProfileMenuEntry extends ProfileLink, Gate {}
export interface ProfileMenuEntry extends ProfileLink, Gate {
id?: string; // targets the entry from config/menu.ts's `hide` and `rename`, as on a nav node; not rendered
}
// `profileLinks` rides on the user, which every view already hands to the shell, so no view needs a new local.
export interface ShellUser {
@@ -86,13 +88,18 @@ export function buildPluginChrome(opts: ChromeOptions): PageChrome {
function profileLinks(opts: ChromeOptions, t: Translate, carryLocale: (href: string) => string): ProfileLink[] {
const user = opts.user ?? null;
if (!user) return [];
const { hide = [], rename = {} } = opts.menu.override;
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),
}));
const shown = (plugin.profileMenu ?? []).filter((entry) => allows(entry, user) && !(entry.id != null && hide.includes(entry.id)));
return shown.map((entry) => {
const renamed = entry.id != null ? rename[entry.id] : undefined;
return {
href: carryLocale(entry.href),
...(entry.icon != null ? { icon: entry.icon } : {}),
label: renamed != null ? t(renamed) : pluginT(entry.label),
};
});
});
}
+2 -2
View File
@@ -23,9 +23,9 @@ export interface NavNode extends Gate {
// order → hide, then the per-user gate filter runs last.
export interface NavOverride {
groups?: NavGroupSpec[]; // wrap top-level nodes (by id) under a new header
hide?: string[]; // remove nodes by id, at any depth (incl. a group's id)
hide?: string[]; // remove nodes by id, at any depth (incl. a group's id), and profile-menu entries
order?: string[]; // reorder top-level nodes by id; unlisted keep their order, after
rename?: Record<string, string>; // id → replacement label
rename?: Record<string, string>; // id → replacement label, for a node or a profile-menu entry
}
export interface NavGroupSpec {