feat(users): Roles tab wired to real endpoints (#36718) - #37082
Queued
AP2300 wants to merge 5 commits into
Queued
Conversation
7 tasks
hmoreras
reviewed
Aug 20, 2026
AP2300
added a commit
that referenced
this pull request
Aug 25, 2026
… spec Actionable follow-ups from hmoreras' review on the roles branch; three roles-endpoint threads (L348 / L141 / L443) remain open, tracked against the profile PR's team-convo chain. Correctness - L483 editUsers guard: RoleOption now carries `editUsers` (defaults true when the field is missing so we don't over-hide). isGrantableLeaf returns false for editUsers=false leaves — no checkbox surfaces, so addRoleToUser can't throw `Cannot alter users on this role` and roll back the transactional PUT. - L97 @for track: `track role.id` on the granted list (id is always unique; roleKey can be empty for keyless roles → NG0955). Click + data-testid still use the grantIdentifier so `revoke()` still finds the entry in `selectedGranted`. UX - L349 defer role fetch: shell wraps the roles p-tabpanel body in `@defer (when activeTab() === 1)` so `loadRoles()` only fires the first time the user opens the tab, not on every dialog open. - L246 loading state: roles panel distinguishes loading from empty via `$isLoading`. New i18n key `users.dialog.roles.loading`. Hygiene (L69) - Drop `standalone: true`, `$`-prefix all signals + computeds, `#`-prefix injections, drop `block` from host class. Kept `changeDetection: OnPush` (matches profile branch decision — app is zoned). Docs - L105 inline HTML comment explaining why `text-*!` is needed on the Material Symbols icons (Google's `.material-symbols-rounded 24px` ties tailwind on specificity). Tests - L72 new roles-tab spec (15 tests → 82% stmts): grantableLeavesByRole memoisation via canSelectRole, isPartiallyChecked on multi-level parents, availableTree pruning (root drops when last grantable leaf moves across), grant/revoke, service error path. Held for team convo (roleKey vs id endpoint contract): - L348, L141, L443 — same chain as profile PR L440. Cross-referenced on the threads so the two branches move together. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
AP2300
force-pushed
the
issue-36718-users-portlet-roles-tab
branch
from
August 25, 2026 16:27
060e9b1 to
28c9a8c
Compare
AP2300
added a commit
that referenced
this pull request
Aug 25, 2026
… spec Actionable follow-ups from hmoreras' review on the roles branch; three roles-endpoint threads (L348 / L141 / L443) remain open, tracked against the profile PR's team-convo chain. Correctness - L483 editUsers guard: RoleOption now carries `editUsers` (defaults true when the field is missing so we don't over-hide). isGrantableLeaf returns false for editUsers=false leaves — no checkbox surfaces, so addRoleToUser can't throw `Cannot alter users on this role` and roll back the transactional PUT. - L97 @for track: `track role.id` on the granted list (id is always unique; roleKey can be empty for keyless roles → NG0955). Click + data-testid still use the grantIdentifier so `revoke()` still finds the entry in `selectedGranted`. UX - L349 defer role fetch: shell wraps the roles p-tabpanel body in `@defer (when activeTab() === 1)` so `loadRoles()` only fires the first time the user opens the tab, not on every dialog open. - L246 loading state: roles panel distinguishes loading from empty via `$isLoading`. New i18n key `users.dialog.roles.loading`. Hygiene (L69) - Drop `standalone: true`, `$`-prefix all signals + computeds, `#`-prefix injections, drop `block` from host class. Kept `changeDetection: OnPush` (matches profile branch decision — app is zoned). Docs - L105 inline HTML comment explaining why `text-*!` is needed on the Material Symbols icons (Google's `.material-symbols-rounded 24px` ties tailwind on specificity). Tests - L72 new roles-tab spec (15 tests → 82% stmts): grantableLeavesByRole memoisation via canSelectRole, isPartiallyChecked on multi-level parents, availableTree pruning (root drops when last grantable leaf moves across), grant/revoke, service error path. Held for team convo (roleKey vs id endpoint contract): - L348, L141, L443 — same chain as profile PR L440. Cross-referenced on the threads so the two branches move together. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
AP2300
force-pushed
the
issue-36718-users-portlet-roles-tab
branch
from
August 25, 2026 19:08
28c9a8c to
1f266f0
Compare
AP2300
added a commit
that referenced
this pull request
Aug 25, 2026
… spec Actionable follow-ups from hmoreras' review on the roles branch; three roles-endpoint threads (L348 / L141 / L443) remain open, tracked against the profile PR's team-convo chain. Correctness - L483 editUsers guard: RoleOption now carries `editUsers` (defaults true when the field is missing so we don't over-hide). isGrantableLeaf returns false for editUsers=false leaves — no checkbox surfaces, so addRoleToUser can't throw `Cannot alter users on this role` and roll back the transactional PUT. - L97 @for track: `track role.id` on the granted list (id is always unique; roleKey can be empty for keyless roles → NG0955). Click + data-testid still use the grantIdentifier so `revoke()` still finds the entry in `selectedGranted`. UX - L349 defer role fetch: shell wraps the roles p-tabpanel body in `@defer (when activeTab() === 1)` so `loadRoles()` only fires the first time the user opens the tab, not on every dialog open. - L246 loading state: roles panel distinguishes loading from empty via `$isLoading`. New i18n key `users.dialog.roles.loading`. Hygiene (L69) - Drop `standalone: true`, `$`-prefix all signals + computeds, `#`-prefix injections, drop `block` from host class. Kept `changeDetection: OnPush` (matches profile branch decision — app is zoned). Docs - L105 inline HTML comment explaining why `text-*!` is needed on the Material Symbols icons (Google's `.material-symbols-rounded 24px` ties tailwind on specificity). Tests - L72 new roles-tab spec (15 tests → 82% stmts): grantableLeavesByRole memoisation via canSelectRole, isPartiallyChecked on multi-level parents, availableTree pruning (root drops when last grantable leaf moves across), grant/revoke, service error path. Held for team convo (roleKey vs id endpoint contract): - L348, L141, L443 — same chain as profile PR L440. Cross-referenced on the threads so the two branches move together. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
AP2300
force-pushed
the
issue-36718-users-portlet-roles-tab
branch
from
August 25, 2026 21:04
1f266f0 to
56b80b2
Compare
Adds the Roles tab to the Create/Edit User dialog. Dual-panel shuttle picker (Available tree on the left, Granted list on the right) with independent filters and Grant/Revoke arrows. Currently backed by a mock catalog in dot-users-roles.data.ts — wiring against `GET /api/v1/roles` and the "user's assigned roles" source of truth is a follow-up decision (see the parent ticket discussion around DWR vs a small REST addition). Also swaps the placeholder tabpanel from #36717 for the real component, and adds the tab-specific i18n keys. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Replaces the mock catalog with real backend data and lifts state up
so the shell's save payload actually carries the granted role list.
Service:
- New `getUserRoles(userIdOrEmail)` — user's currently assigned roles
via GET /api/v1/roles/users/{userIdOrEmail}.
- New `getAllRoles()` — full system role tree. Uses
/api/v1/roles?loadChildrenRoles=true for roots + immediate children,
then recursively fetches /api/v1/roles/{id}?loadChildrenRoles=true
per non-root discovered so far until a round yields no new roles.
Bounded by the number of non-root roles in the system. `_search`
would be simpler but returns SmallRoleView (no parent, no hierarchy)
and can't be reconstructed into a client-side tree.
- New `DotRoleView` interface + `flattenRoleTree` no longer needed —
the recursive walker flattens on the fly and sets `parent` on every
descendant.
Roles tab:
- Deletes the visual-only `dot-users-roles.data.ts` mock catalog.
- `initialGrantedKeys: string[]` input + `grantedChange: string[]`
output. An effect seeds `granted` from the first non-empty input
value; later parent mutations don't clobber in-flight edits.
- Drops the synthetic System/Custom bucketing — root roles
(`System`, `Categories`, `Intranet`, `Publisher / Legal`) are the
natural top-level nodes and their children indent underneath.
- Bulk-select via parent checkbox: checking a parent selects every
grantable-leaf descendant in one action; indeterminate state when
some are selected; unchecking clears them all. Precomputed
`grantableLeavesByRole` keeps this O(1) per row.
- Grantable = leaf only (parents excluded so their checkbox is a
bulk-select shortcut, not a role grant). `roleKey` is preferred
when the backend has one; `role.id` used as fallback so legacy /
manually-imported keyless roles still work in the shuttle.
- Root rows get extra top spacing and a bold, slightly larger label
for visual hierarchy.
Shell:
- `loadUserDetail` now forkJoins getUser + getUserRoles. Roles fetch
catches errors to [] so a failed lookup doesn't block profile
hydration.
- New signals: `initialGrantedRoleKeys` (down to the tab) and
`currentRoleKeys` (mirror updated on every grantedChange). Both
seeded from the initial fetch so save preserves membership even
when the user never opens the Roles tab.
- `buildPayload` sends `payload.roles` only when the current key
list is non-empty — an empty array on PUT would wipe membership
(processRoles calls removeRoles then re-adds), a missing field
is the safer "leave untouched" signal.
Test coverage: 56 tests + lint clean.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
- Drop the user's personal role (roleKey === userId) from the save payload so `UserResource.processRoles` no longer trips its `Cannot alter users on this role` guard on editUsers=false roles. - Prune parents from the Available tree once every grantable descendant has been moved to Granted, so an empty root can no longer linger as a dead container. Workflow-only branches with zero grantable descendants (Publisher / Legal) are unaffected. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
… spec Actionable follow-ups from hmoreras' review on the roles branch; three roles-endpoint threads (L348 / L141 / L443) remain open, tracked against the profile PR's team-convo chain. Correctness - L483 editUsers guard: RoleOption now carries `editUsers` (defaults true when the field is missing so we don't over-hide). isGrantableLeaf returns false for editUsers=false leaves — no checkbox surfaces, so addRoleToUser can't throw `Cannot alter users on this role` and roll back the transactional PUT. - L97 @for track: `track role.id` on the granted list (id is always unique; roleKey can be empty for keyless roles → NG0955). Click + data-testid still use the grantIdentifier so `revoke()` still finds the entry in `selectedGranted`. UX - L349 defer role fetch: shell wraps the roles p-tabpanel body in `@defer (when activeTab() === 1)` so `loadRoles()` only fires the first time the user opens the tab, not on every dialog open. - L246 loading state: roles panel distinguishes loading from empty via `$isLoading`. New i18n key `users.dialog.roles.loading`. Hygiene (L69) - Drop `standalone: true`, `$`-prefix all signals + computeds, `#`-prefix injections, drop `block` from host class. Kept `changeDetection: OnPush` (matches profile branch decision — app is zoned). Docs - L105 inline HTML comment explaining why `text-*!` is needed on the Material Symbols icons (Google's `.material-symbols-rounded 24px` ties tailwind on specificity). Tests - L72 new roles-tab spec (15 tests → 82% stmts): grantableLeavesByRole memoisation via canSelectRole, isPartiallyChecked on multi-level parents, availableTree pruning (root drops when last grantable leaf moves across), grant/revoke, service error path. Held for team convo (roleKey vs id endpoint contract): - L348, L141, L443 — same chain as profile PR L440. Cross-referenced on the threads so the two branches move together. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
AP2300
force-pushed
the
issue-36718-users-portlet-roles-tab
branch
from
August 25, 2026 21:17
56b80b2 to
76ed444
Compare
hmoreras
approved these changes
Aug 26, 2026
AP2300
enabled auto-merge
August 26, 2026 13:48
AP2300
added this pull request to the merge queue
Aug 26, 2026
Any commits made after this event will not be merged.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Closes #36718
Depends on #36990 (Profile tab shell). This PR's diff currently includes the shell commits; once #36990 is merged to main, this branch will be rebased onto the new main and the diff will show only the Roles tab changes.
Summary
GET /api/v1/roles?loadChildrenRoles=true+ per-node fan-out on non-root roles. Parent nodes get a bulk-select checkbox that toggles every grantable leaf underneath.GET /api/v1/roles/users/{userId}and refreshed on every move; the parent dialog picks the change up viagrantedChangeand folds it intoPUT /api/v1/usersas therolesfield.Notable non-obvious calls
roleKey === userId,editUsers=false). The backend's `UserResource#processRoles` calls `removeAllRolesFromUser` then loops `addRoleToUser`; the personal role trips the `editUsers` guard and rolls the save back with "Cannot alter users on this role". Filtering it in the outbound payload sidesteps the guard. Proper fix belongs on the backend.Test plan
🤖 Generated with Claude Code