feat(shared-ui): move the role label from the portal-shell sidebar into the user menu
CI / scan (pull_request) Successful in 3m16s
CI / commits (pull_request) Successful in 3m54s
CI / check (pull_request) Successful in 4m11s
CI / a11y (pull_request) Successful in 2m19s
CI / perf (pull_request) Successful in 5m8s

The "Role: Anonymous / User / Administrator" widget at the bottom of
the portal-shell sidebar always struck the same audience as the user
menu — it told the reader who they were and what they could do. With
the menu now carrying the full identity card (displayName, username,
role-curated actions like "Open Portal Admin"), keeping the role
in the sidebar duplicates the surface and surfaces the label on
anonymous traffic where it carries no real information ("Role:
Anonymous" was a verbose synonym for "Sign in").

This PR moves the role chip into the user-menu panel header, where
it sits between the username and the menu rows. Hidden on anonymous
traffic by construction — the `<lib-user-menu>` only renders for
the authenticated case in both apps' headers.

Shape:

* `UserMenu` (shared-ui) gains an **optional `role` input**. When
  set, a small brand-tinted pill renders inside the panel header
  below the username (`data-testid="user-menu-role"`). When
  undefined, the entire chip is omitted — keeps backwards compat
  with any future caller that doesn't carry a role concept.

* `portal-shell`'s header computes `roleLabel` from the existing
  `CapabilitiesService.canAccessAdmin` signal (the same logic that
  previously lived in the sidebar), passing the localised string
  through the new input. The sidebar role widget — HTML +
  `roleLabel` / `roleAriaLabel` computeds + the `AuthService` and
  `CapabilitiesService` injections — is removed.

* `portal-admin`'s header passes a hardcoded `'Administrator'`.
  Every reader who reaches the admin app already carries
  `Portal.Admin` (guard precondition); no need to re-derive. No
  i18n marks per ADR-0020's source-locale-only chrome.

* The `sidebar.role.{anonymous,administrator,user,aria,label}`
  translation units are removed from `messages.fr.xlf`. Replaced by
  `common.role.{administrator,user}` (the new owners are the user
  menu in either app, hence the generic prefix). The "Anonymous"
  string is gone too — the chip is hidden on anonymous traffic, the
  label served no purpose without the user menu rendering it.

* The portal-shell sidebar spec drops its three "role label" tests
  (the widget is gone) and grows a guard test asserting the
  `data-testid="sidebar-role"` element no longer exists. The
  setup is simplified back to the pre-#151 shape — no Http
  testing infrastructure needed, since the sidebar no longer
  injects `AuthService` / `CapabilitiesService`.

* The portal-shell header spec gains two assertions for the role
  chip inside the open menu panel (Administrator / User).

* The shared `UserMenu` spec gains two assertions covering the new
  input's presence/absence semantics.

Verification:
  * 43 portal-shell specs pass (header +2, sidebar -2 net, balanced).
  * 50 portal-admin specs pass (no spec edits).
  * 10 shared-ui specs pass (was 8, +2 for the role chip).
  * `pnpm nx run portal-shell:build:production` (i18n-strict) green
    — confirms the xlf cleanup + new `common.role.*` keys are
    consistent with the source-locale usage.
This commit is contained in:
Julien Gautier
2026-05-16 03:05:22 +02:00
parent 10c80f189d
commit c49a807966
13 changed files with 138 additions and 157 deletions
@@ -32,6 +32,7 @@
[username]="state.user.username"
[initials]="initials()"
[items]="userMenuItems"
[role]="roleLabel"
[signedInAsLabel]="signedInAsLabel"
[signOutLabel]="signOutLabel"
[triggerAriaLabel]="triggerAriaLabel(state.user.displayName)"
@@ -56,6 +56,13 @@ export class AdminHeader {
protected readonly signedInAsLabel = 'Signed in as';
protected readonly signOutLabel = 'Sign out';
// Static role label — every reader who reaches portal-admin holds
// the `Portal.Admin` Entra app role (that's the `AdminRoleGuard`
// precondition for `/api/admin/*`), so the chip is unconditional
// and doesn't need to read `req.session.user.roles`. No i18n marks
// either, per ADR-0020's source-locale-only chrome.
protected readonly roleLabel = 'Administrator';
protected triggerAriaLabel(displayName: string): string {
return `User menu — signed in as ${displayName}`;
}