Rename ShellUser.menu to profileLinks, so menu keeps one meaning in the shell
CI / full-gate (push) Successful in 3m29s
Mirror / github-mirror (push) Successful in 5s

This commit was merged in pull request #220.
This commit is contained in:
2026-11-08 19:35:14 +01:00
parent b7e9780437
commit 77e3d46769
5 changed files with 7 additions and 7 deletions
+1 -1
View File
@@ -608,7 +608,7 @@ interface RequestContext {
} }
``` ```
**`ctx.chrome`** — `{ brand, csrfToken, nav, signInHref, theme, user }` (`user.menu` holds the profile-dropdown links). Hand each field to the **`ctx.chrome`** — `{ brand, csrfToken, nav, signInHref, theme, user }` (`user.profileLinks` holds the profile-dropdown links). Hand each field to the
matching `partials/shell` local and a `view` result renders the **native app shell**, exactly as matching `partials/shell` local and a `view` result renders the **native app shell**, exactly as
`examples/plugins/scheduling/views/overview.ejs` does; a field you omit falls back to its shell `examples/plugins/scheduling/views/overview.ejs` does; a field you omit falls back to its shell
default rather than erroring. `chrome.nav` is the whole global menu — every installed plugin's default rather than erroring. `chrome.nav` is the whole global menu — every installed plugin's
+1 -1
View File
@@ -103,7 +103,7 @@ const entries = (opts: { permissions?: string[]; signedIn?: boolean }): { href:
plugins: [account], plugins: [account],
translatorFor: () => (key) => (key === "account.settings" ? "Inställningar" : key), translatorFor: () => (key) => (key === "account.settings" ? "Inställningar" : key),
...(opts.signedIn === false ? {} : { user: { email: "ada@x.io", id: "u1", permissions: opts.permissions ?? [] } }), ...(opts.signedIn === false ? {} : { user: { email: "ada@x.io", id: "u1", permissions: opts.permissions ?? [] } }),
}).user.menu!; }).user.profileLinks!;
test("profile menu entries are gated like nav nodes, translated by their plugin and carry the locale", () => { test("profile menu entries are gated like nav nodes, translated by their plugin and carry the locale", () => {
assert.deepEqual(entries({}), [ assert.deepEqual(entries({}), [
+3 -3
View File
@@ -22,11 +22,11 @@ export interface ProfileLink {
// What a plugin's `profileMenu` declares: a link plus the one gate that decides who sees it. // 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 {}
// `menu` rides on the user, which every view already hands to the shell, so no view needs a new local. // `profileLinks` rides on the user, which every view already hands to the shell, so no view needs a new local.
export interface ShellUser { export interface ShellUser {
email: string; email: string;
initials: string; initials: string;
menu?: ProfileLink[]; // gate-filtered, translated, locale-carried. The host always sets it; a hand-built fixture may omit it profileLinks?: ProfileLink[]; // gate-filtered, translated, locale-carried. The host always sets it; a hand-built fixture may omit it
name: string; name: string;
} }
@@ -79,7 +79,7 @@ export function buildPluginChrome(opts: ChromeOptions): PageChrome {
nav: carryLocaleInto(nav, carryLocale), nav: carryLocaleInto(nav, carryLocale),
signInHref: carryLocale(returnTo), signInHref: carryLocale(returnTo),
...(theme != null ? { theme } : {}), ...(theme != null ? { theme } : {}),
user: { ...shellUser(opts.user, t), menu: profileLinks(opts, t, carryLocale) }, user: { ...shellUser(opts.user, t), profileLinks: profileLinks(opts, t, carryLocale) },
}; };
} }
+1 -1
View File
@@ -137,7 +137,7 @@ test("profile menu entries render as links above Sign out, with their icon", asy
csrfToken: "tok.sig", csrfToken: "tok.sig",
user: { user: {
email: "ada@acme.io", initials: "AD", name: "ada", email: "ada@acme.io", initials: "AD", name: "ada",
menu: [{ href: "/account/settings", icon: "i-user", label: "My <settings>" }, { href: "/account/help", label: "Help" }], profileLinks: [{ href: "/account/settings", icon: "i-user", label: "My <settings>" }, { href: "/account/help", label: "Help" }],
}, },
}); });
const panel = html.slice(html.indexOf('id="profile-menu"'), html.indexOf("</aside>")); const panel = html.slice(html.indexOf('id="profile-menu"'), html.indexOf("</aside>"));
+1 -1
View File
@@ -71,7 +71,7 @@
</button> </button>
<div id="profile-menu" class="menu-pop left up" popover style="min-width:220px"> <div id="profile-menu" class="menu-pop left up" popover style="min-width:220px">
<div class="menu-head"><%= t("shell.signedInAs", { name: user.name }) %></div> <div class="menu-head"><%= t("shell.signedInAs", { name: user.name }) %></div>
<% (user.menu || []).forEach((link) => { %> <% (user.profileLinks || []).forEach((link) => { %>
<a class="menu-item" href="<%= link.href %>"><% if (link.icon) { %><svg class="ico"><use href="#<%= link.icon %>" /></svg><% } %><%= link.label %></a> <a class="menu-item" href="<%= link.href %>"><% if (link.icon) { %><svg class="ico"><use href="#<%= link.icon %>" /></svg><% } %><%= link.label %></a>
<% }) %> <% }) %>
<%# Sign out is a state change → a POST form (not a GET link), CSRF-guarded by app.ts %> <%# Sign out is a state change → a POST form (not a GET link), CSRF-guarded by app.ts %>