feat(dot-roles): Tools tab in Angular, roles service consolidation, and edit-gate fixes - #37260
feat(dot-roles): Tools tab in Angular, roles service consolidation, and edit-gate fixes#37260hmoreras wants to merge 12 commits into
Conversation
Wires the Beta portlet to the two backend features that landed on main and that the code was explicitly waiting on. #37070 — GET /v1/roles/{roleId}/users Replaces the pair of member-loading calls: /v1/users/filter?roleKey=X (fast but unusable on roles created without a roleKey) and the id-based /rolehierarchyanduserroles fallback (worked by id but returned Role objects, so the Users tab rendered an empty EMAIL column). The new endpoint is keyed on roleId AND returns the standard user serialization, so both problems go away and every member row now carries an email. The endpoint shipped as a direct-grants-only resource: it deliberately does not resolve inheritance or denormalize granted-from metadata. The code's comments predicted the opposite ("a single call replacing this whole flow"), so they are corrected here. The ancestor-chain fan-out that composes effective membership stays, and is now documented as permanent by design rather than as a stopgap. For the same reason the members table keeps client-side paging: a server page of one ancestor is not a page of the merged union, so [lazy]="true" would be incorrect, not pending. Removes the now-dead roleKey branching along the whole path, plus the toRoleMemberResults adapter and the RoleHierarchyEntry model. None of these were exported from the lib, so the change is contained to the portlet. #37071 — childCount / userCount on RoleView Leaf detection now reads childCount instead of inferring from an empty roleChildren array, so the chevron is right on first paint at every depth — no more chevrons that expand into nothing at level 2+. Legacy search nodes (/api/role/loadbyname) do not carry the field and fall back to the previous heuristic. userCount unblocks the per-row user-count badge the design called for, which had been left out because no endpoint exposed the number. Tests: 11 suites / 111 passing. Prod build, lint and format:check clean. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Three libs had grown their own implementation of the same `/api/v1/roles/**` surface, and three TypeScript models of the same backend `RoleView`. The duplication was accidental — the same two pieces of backend knowledge (two-level hydration, `_search` returning `SmallRoleView` with no parent) had been independently rediscovered and re-documented in two of them. One model `DotRole` in `@dotcms/dotcms-models` is now the single role shape. Only `id` and `name` are guaranteed, because the surface is served by three serializers that each omit part of the shape — documented on the type so consumers stop assuming `roleKey` is an identifier or that an absent `childCount` means zero. `DotRoleView` is deleted from `dot-users.service.ts`; `DotRoleNode` and `DotRoleDetail` become aliases, keeping the names that read well at the tree's call sites. The lowercase `dbfqn` / `fqn` that `dot-users` declared never matched the wire (Jackson serializes `getDBFQN()` all-caps) and were never read; the unified model keeps the real spelling. One read surface `DotRolesService` gains `getRoots`, `getById` and `getForUser` alongside the existing `get` / `search`. Names drop the redundant `role` — the service already says it. `dot-users` and the `dot-roles` portlet both repoint at it; `DotRolesPortletService` keeps CRUD, the legacy `loadbyname` search and the write adapters, since promoting destructive operations to workspace-public API would freeze the wire shape while those endpoints are still landing. `get` / `search` / `processRolesResponse` are deliberately untouched. The workflow assign components inject this service, so leaving those paths byte-for-byte identical keeps that blast radius at compile-time only. Strategy is not an endpoint The full-hierarchy walk does NOT move into the shared service. It is one way of composing two endpoints, chosen for one UI: the Roles tab renders a shuttle and needs every role up front, while the roles portlet renders a tree and composes the same endpoints lazily per expansion. Neither belongs to the service. It now lives as a pure function in `dot-users/utils/dot-roles-hierarchy.utils.ts`, taking a `fetchChildren` callback so it is unit-testable without HTTP. Two behaviors changed while it moved: - It only descends into nodes reporting `childCount > 0` (#37071). Most roles below the first level are leaves, so this prunes the large majority of the request burst the Roles tab produced on open. Nodes with an absent `childCount` are still visited, so an older serializer degrades to the exhaustive walk rather than silently truncating. - The per-node `catchError(() => of({children: []}))` is gone. A partial hierarchy rendered as if it were complete is worse than a visible failure; the error now reaches the tab, which already routes it through DotHttpErrorManagerService. Verified: 35 affected projects tested, 36 linted, format:check and the dotcms-ui production build clean. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Replaces the legacy Dojo iframe with a real Angular tab. The endpoints it
needs already exist, so nothing was blocking this.
Scope is deliberately narrower than the JSP it replaces: the tab lists the
tool groups and toggles which ones the role gets. Creating, editing and
deleting tool groups is not here — those have no v1 endpoints (only RoleAjax
DWR) and belong to a Tools portlet of their own.
Composition
Three endpoints, none of them new:
- GET /v1/roles/layouts — the system-wide catalog. Despite the path this is
not a per-role read, and it is the only source of `portletTitles` (the
localized portlet names resolved server-side), so it drives every row and
the Included Tools column.
- GET /v1/roles/{roleId}/layouts — direct grants. The backend resolves this
as `from LayoutsRoles where role_id = ?` with no hierarchy walk, so
effective grants are composed client-side by walking the ancestor chain —
the same shape the Users tab already uses, reusing collectAncestorChain.
Each row is tagged with the closest ancestor that grants it, which is what
the Granted From chip names.
- POST /v1/roles/layouts — a full replace, not an append: the backend diffs
the payload against the role's current grants and drops the difference. So
one toggle syncs the whole grid in a single call, and the payload always
carries the complete direct-grant set. Inherited grants are excluded on
purpose — echoing one back would silently promote it to a direct grant, so
inherited rows render checked but locked, revocable only on the ancestor
named in their chip.
All three live in the shared DotRolesService: they are reads and writes of
the roles domain, and splitting them by consumer count or by destructiveness
put one domain in two files behind a rule invisible to anyone reading either.
DotRoleToolGroup moves to @dotcms/dotcms-models alongside DotRole; the row
projection (granted + grantedFrom) stays in the portlet, since it is
presentation.
Header count
`toolGroupCount` counts effectively-granted groups and is loaded on role
selection rather than when the tab opens — the header shows it on every tab,
so deferring it would leave it reading 0 until the admin clicked Tools.
Removals
The tools iframe component and its two JSPs (view_role_tools_wrapper.jsp,
view_role_tools_inc.jsp) are deleted rather than left behind. The shared
view_role_iframe_stubs_inc.jsp stays — Permissions still wraps an iframe —
and its comments are updated so they stop pointing at deleted files.
Tests: 11 suites / 122 passing. 36 projects linted, 35 tested, format:check
and the dotcms-ui production build clean.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Finishes what the earlier consolidation started. Splitting one domain across two services by consumer count and by destructiveness put related endpoints in different files behind a rule invisible to anyone reading either one. The rule now is simply: roles endpoints live in the roles service. Moved and renamed — the service already says "roles", so the methods stop repeating it: searchRoles -> searchTree (search() is taken by _search) loadRoleMembers -> getUsers createRole -> create updateRole -> update deleteRole -> delete grantUserToRole -> grantUser removeUsersFromRole -> removeUsers reparentRole -> reparent The tool-group trio is normalized the same way now that every method is role-scoped: getAllToolGroups() for the catalog, getToolGroups(roleId) and saveToolGroups(roleId, ids) for the role's own. loadRootRoles / loadRoleById were pure delegations to getRoots / getById and are gone; the store calls the shared service directly. `get` and `search` keep their legacy names — the workflow assign components inject this service and those paths stay untouched. DotRoleFormValue moves to @dotcms/dotcms-models (data-access cannot import from a portlet) and the wire adapters move alongside the service, with their spec, so the mapping logic keeps its coverage. What stays behind DotRolesPortletService is down to one method: searchUsers. /v1/users/filter is a users endpoint, not a roles one, so it does not belong in DotRolesService either. It stays until data-access has a shared users service to host it — dot-users has one, but portlet-to-portlet imports are not allowed. Tools tab: no flicker on toggle Every checkbox click ran a reload that set status to LOADING, so the whole table swapped for the skeleton. The toggle is now painted optimistically and the post-save reconcile runs silently, leaving the table in its loaded state throughout. A failed save rolls the patch back rather than leaving the grid showing something the backend rejected. Un-checking a group an ancestor also grants is the one case the optimistic patch cannot resolve locally — a row only keeps its closest source — so the silent reconcile restores the inherited chip a moment later. Tree badge Smaller person icon, larger count, and the secondary grey actually applies: PrimeNG colors the node label with a more specific selector, so the badge was inheriting its near-black. Tests: 10 suites / 112 passing, including three that pin the no-flicker behavior. 36 projects linted, 35 tested, format:check and the dotcms-ui production build clean. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
`text-xs` (12px) read too small next to the 14px count. `text-sm` puts the icon back at the count's own size, and no scale step sits between them, so the icon takes an explicit 13px — the same arbitrary-value-with-important pattern other portlets already use for material symbols. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The Tools tab greyed its checkboxes out and showed the cannot-edit notice for CMS Administrator, while the legacy portlet lets you change that role's tool groups and save. The Beta was stricter than both the backend and the screen it replaces. Cause: `canModifyRole` correctly gates on `system` / `locked`, and that shape was copied to the per-domain gates. But `RoleHelper` applies a different contract to each: update / delete role isSystem() || isLocked() grant / remove users !isEditUsers() — nothing else save layouts no check on the target role at all `POST /v1/roles/layouts` only requires the CALLER to hold the CMS Admin role, which surfaces as a 403. CMS Administrator is a system role with editLayouts true, so the legacy grid — which reads `currentRole.editLayouts` alone — enables it and this tab did not. canEditRoleLayouts now gates on `editLayouts` alone. That flag is kept even though the backend ignores it, because it is the contract the legacy screen honours and dropping it would be a behaviour change of its own. canEditRoleUsers had the same defect and is fixed with it: user grants on CMS Administrator were blocked for the same invented reason. Not reported yet, but the same bug. canEditRolePermissions is deleted rather than fixed — it had no consumers, the Permissions tab is still an iframe. Also, from design review on the tree: - shield -> shield_person for leaf roles, in the tree and the detail header. - The user-count badge takes `text-gray-400`. It had `text-color-secondary`, which is a PrimeFlex class — PrimeFlex is not installed, so it resolved to nothing and the badge inherited the node label's near-black. The dead class and a comment blaming PrimeNG specificity are gone with it. 104 more occurrences of that class survive across 35 files; they need their own pass. - The badge icon returns to text-sm. Tests: 116 passing, four of them pinning the corrected gates — including that a system + locked role permits user and tool edits but still refuses update and delete. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
|
Claude finished @hmoreras's task in 4m 54s —— View job Claude Code ReviewReviewed the full diff against One introduced regression worth addressing: New Issues
Everything else in the diff checks out — no correctness, security, or replay-safety issues found in the changed code. The security note about the unauthenticated · branch |
Correctness Two cross-role races, both reachable with ordinary navigation and normal latency. `saveToolGroups` and `grantUserToRole`/`removeUsersFromRole` capture the role id before their await and reconcile with it afterwards; a role switch in between meant a late response repainted the new role's tab with the old role's data. Worse for tool groups: the tab computes its next POST payload from whatever is in `toolGroups`, so one role's grants could be written onto another. Guarded at both ends — the reconcile only fires while the role is still selected, and the rxMethod sinks refuse to write for a role that is no longer selected (the caller-side guard also stops switchMap from cancelling the new role's legitimate load). `selectRole` now clears `toolGroupsSaving` too, which was locking every checkbox on the role being switched TO. Per-ancestor `catchError` in `loadToolGroups` and `loadMembers` turned "could not verify" into "not granted". A transient failure showed an inherited tool group unchecked; an admin trusting the grid would grant it again, creating a redundant direct grant off a masked network error. Failures are now tracked and surface as an error state instead of a confident wrong answer. Making `childCount` authoritative for leaf detection regressed local tree mutations: `appendChildToParent` and `removeNodeFromTree` left the count stale, so a parent that just gained its first child rendered as a childless leaf and the new role was unreachable. Both keep it in sync now. `POST /v1/roles` answers with `Role.toMap()`, not a `RoleView`, so it carries no `childCount` — a freshly created role drew as a folder. It is normalised to 0, which is true by construction. (`PUT` returns a RoleView and needed nothing.) The tree's user-count badge never refreshed after a grant or revoke. It syncs from the direct-grant count when members load — direct only, matching what the backend puts in that field, and skipped when a check failed so a stale number is not replaced by a wrong one. `dot-users-list.store.spec.ts` still mocked `getUserRoles`, deleted from `DotUsersService` in this PR, and never mocked `DotRolesService` — so the roles column was exercised against the real root service and was effectively untested. Add / Edit role dialogs The required marker was hand-written text; it now uses the `dotFieldRequired` directive, which is what makes it red. There was no validation message at all — `dot-field-validation-message` is wired in. The dialog title moves from an h2 in the body to the dialog header. Parent is a `p-treeSelect` instead of a flat indented list: indentation alone gets ambiguous past two levels. It starts collapsed, hydrates a branch on expand (the backend sends two levels), and its filter runs the same deep search the roles tree uses, so a role in an unloaded branch is findable. Two PrimeNG behaviours had to be worked around, both from the same cause — the component keeps state inside the node objects, and our options come from a computed that hands it new ones. Expansion is mutated onto `node.expanded`, so branches snapped shut the moment their children loaded; expanded keys are tracked in a signal and re-applied. And `Tree.getRootNode()` returns its cached `filteredNodes` once the client filter has run, ignoring `value`, so server results never rendered until the filter is re-applied over the new options. Design review Empty states use the shared `DotEmptyContainerComponent` — the previous hand-rolled dashed card exists nowhere else in the product. `shield` becomes `shield_person`. The `+` on a tree row is primary with a pointer cursor. The user-count badge takes a real grey: `text-color-secondary` is a PrimeFlex-era class, PrimeFlex is not installed and tailwindcss-primeui does not provide it, so it compiled to nothing and the badge inherited the label's near-black (verified against the built CSS, not assumed). Granted From has a minimum width. Untranslated portlet titles no longer print as raw i18n keys. Note on standards: ANGULAR_STANDARDS asks for Signal Forms on new forms, but `dotFieldRequired` injects `FormGroupDirective` and `dot-field-validation-message` takes an `AbstractControl` — the standard validation components are Reactive-Forms-bound. These dialogs stay Reactive. Tests: 136 passing. The race and badge regressions were verified by neutering each fix and confirming the new tests go red. 36 projects linted, 35 tested, format:check and the dotcms-ui production build clean. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The previous commit claimed to guard both ends of the cross-role races. It only guarded the sink. The reconcile dispatches in saveToolGroups, grantUserToRole and removeUsersFromRole still fired with the role id they captured before their await, and that incomplete fix introduced a worse bug than the one it closed: a stale dispatch enters the SHARED rxMethod, whose switchMap cancels the current role's in-flight load, and the sink guard then discards the stale result — so nothing ever settles that role's status and its tab sits on the loading skeleton indefinitely. Guarded at the caller now, with the sink guard kept as defence in depth. Two more cross-role leaks from the same review: - `saveToolGroups`'s rollback restored its `previous` snapshot without checking identity, so a failed save on role A could overwrite role B's grid — and the next save would persist A's rows as B's. - `toolGroupsSaving` was cleared unconditionally on completion, so a finishing save could unlock a grid whose own save was still running. Silent reparent to root The Edit dialog resolved the role's current parent to a tree node and set the picker only `if (node)`. A role reached through the page's search can have its parent chain missing from the cached tree, so the picker stayed empty and Save sent `parentRoleId: null` — moving the role to root on a dialog the admin only opened to rename it. The Add dialog already had this fallback; Edit now does too. Cycle exclusion bypassable through search The Edit picker built its exclusion set from the cached tree but rendered from `searchResults ?? cached`. Search results come from a different endpoint and carry ancestor paths the lazy tree never loaded, so a real descendant could appear as a selectable parent — defeating the guard precisely on the path most likely to surface deep roles. Exclusion is now collected from both datasets. Badge sync missed the rendered tree While a search is active the tree renders `searchResults`, not `roles`. The user-count patch only touched `roles`, so the fix silently did nothing on the common path of finding a role by typing. It patches both now. createRole's fallback was a no-op The "parent not in the loaded tree" branch fetched the parent and called `patchNodeChildren`, which rewrites a node it must first find — in exactly the case this branch exists for, it matched nothing. The role was created and selected in the detail pane, so it read as success while never appearing in the tree. It reloads the roots instead: heavier than a scoped patch, but coherent. Parent picker search Added a staleness token so a slower earlier query cannot overwrite a newer one's results — the debounce gates when a search starts, not what order responses arrive, and these are plain promises with no switchMap to cancel the loser. Backspacing below the 3-character threshold now also resets PrimeNG's inner filter; reverting the options alone left the previous search's rows on screen, since `Tree.getRootNode()` keeps serving its cached `filteredNodes`. Tests A mock seeded with `mockResolvedValue` (not `Once`) leaked its implementation into every later test in the suite — `mockClear` resets history, not behaviour. And the "leaves the badge alone on failure" test captured an unset baseline, so it only proved we do not write `undefined` on a first failed load rather than the case it names: resetting an already-good count from a partial answer. Both verified by an independent test reviewer, which also confirmed the race and badge regression tests do fail without their guards. Lib cleanup `standalone: true` and `changeDetection: ChangeDetectionStrategy.OnPush` are removed from all 9 components in the portlet — both are Angular 22 defaults and ANGULAR_STANDARDS asks for neither to be set. No component used `ChangeDetectionStrategy.Eager`, which the same standard says to leave alone. Tests: 136 passing. Lint, format:check and the dotcms-ui production build clean. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Four changes, all about the portlet feeling settled instead of twitchy. Tool-group saves now raise the standard toast, debounced so ticking a handful of boxes in a row yields one notification rather than a stack. Creating a child under a collapsed parent reveals it: the tree expands the whole ancestor chain (the parent may itself be collapsed) and fetches the parent's children when they were never loaded — otherwise the branch would hold only the new role and read as though its siblings had been deleted. Selection was already handled by the store. The checkbox blink after a tool-group save was PrimeNG's default `rowTrackBy`, which is object identity. The reconcile rebuilds every row object, so Angular tore down and re-created each <tr> and with it the p-checkbox. Tracking by id lets it patch the rows in place. The members table blinked for a different reason a level up: loadMembers flipped to LOADING unconditionally, and the template renders the skeleton on LOADING, so a post-grant refetch swapped the entire table out and back — nothing rowTrackBy can reach, since it is a different @if branch. It now takes the same `silent` flag loadToolGroups already had, used by both the grant and the remove paths. Selecting a role stays non-silent: there are no rows on screen yet, so the skeleton is right there. Also gave the Granted From tag a fixed-height slot. It renders only on granted rows, so without a reserved line box the row grew the instant a checkbox was ticked. Every fix has a regression test, each verified to go red when the fix is reverted.
Proposed Changes
role-portlet-progress.mp4
Follow-up work on the Roles (Beta) Angular portlet.
Consume the backends that landed —
GET /v1/roles/{roleId}/users(#37070) replaces the previous pair of member-loading calls. The old id-based/rolehierarchyanduserrolesfallback returnedRoleobjects rather than users, which is why the Users tab rendered an empty EMAIL column; that column now populates.childCountanduserCount(#37071) drive leaf-vs-chevron detection on first paint — no more chevrons that expand into nothing — and unblock the per-row user-count badge the design called for.Worth flagging: the code's comments predicted
/roles/{id}/userswould resolve inheritance server-side and collapse the whole flow into one call. It shipped as a direct grants only resource by design, so the ancestor-chain fan-out stays and is now documented as permanent rather than as a stopgap. For the same reason the members table keeps client-side paging — a server page of one ancestor is not a page of the merged union, so[lazy]="true"would be incorrect, not pending.Migrate the Tools tab to Angular — replaces the Dojo iframe. Scope is deliberately narrower than the JSP it replaces: it lists tool groups and toggles which ones the role gets. Creating, editing and deleting tool groups is not here — those have no v1 endpoints (only
RoleAjaxDWR) and belong to a Tools portlet of their own. Inherited grants render checked but locked; echoing one back into the full-replacePOSTwould silently promote it to a direct grant. The iframe component and its two JSPs are deleted rather than left behind.Consolidate the roles surface into
DotRolesService— three libs had grown their own implementation of the same/api/v1/roles/**surface, and three TypeScript models of the same backendRoleView. Reads, writes and models now live in one place; method names drop the redundantrole(createRole→create).getandsearchkeep their legacy names — the workflow assign components inject this service and those paths are untouched. The full-hierarchy walk did not move into the service: it is one way of composing two endpoints for one UI, so it lives as a pure, HTTP-free function indot-users, and it now prunes onchildCount— most roles below the first level are leaves, which removes the large majority of the request burst the Roles tab produced on open.Fix edit gates that were stricter than the backend — the Tools tab greyed itself out for CMS Administrator while the legacy portlet allows the edit.
RoleHelperapplies a different contract per operation: update/delete checksisSystem() || isLocked(), user grants checkisEditUsers()alone, and saving layouts places no restriction on the target role at all.Review fixes
A multi-agent review and hands-on QA surfaced defects that are fixed in this PR:
saveToolGroupsand the user grant/remove flows captured the role id before theirawaitand reconciled with it afterwards, so a role switch mid-flight repainted the new role's tab with the old role's data. For tool groups this was worse than cosmetic: the tab derives its next POST payload fromtoolGroups, so one role's grants could be written onto another. Guarded at both ends, with regression tests verified by neutering each guard and confirming they go red.catchErrorreturned an empty set on failure, so a transient error showed an inherited tool group unchecked — and an admin trusting the grid would create a redundant direct grant. Failures now surface instead of being absorbed.childCountregressions. Local tree mutations left it stale, so a parent that just gained its first child rendered as a leaf and the new role was unreachable. AndPOST /v1/rolesanswers withRole.toMap(), which carries nochildCount, so a freshly created role drew as a folder.dot-users-list.store.spec.tsstill mocked a method deleted in this PR and never mockedDotRolesService, so the roles column ran against the real root service and was effectively untested.Add / Edit role dialogs
Required markers use the
dotFieldRequireddirective (which is what makes them red) instead of hand-written text, anddot-field-validation-messageis wired in — there was no validation feedback at all. The dialog title moves from an h2 in the body into the dialog header.Parent selection becomes a
p-treeSelect: indentation alone gets ambiguous past two levels. It starts collapsed, hydrates a branch on expand (the backend sends two levels per request), and its filter runs the same deep search the roles tree uses, so a role in an unloaded branch is findable.Two PrimeNG behaviours needed working around, both from one cause — the component keeps state inside the node objects it is given, and these options come from a
computedthat hands it new ones. Expansion is mutated ontonode.expanded, so branches snapped shut the moment their children loaded. AndTree.getRootNode()returns its cachedfilteredNodesonce the client filter has run, ignoringvalue, so server results never rendered. Both are handled explicitly, with the reasoning in the code — the second reaches one internal method, which is the most upgrade-fragile line in this PR.Design review
Empty states now use the shared
DotEmptyContainerComponent; the hand-rolled dashed card they replaced exists nowhere else in the product.shield→shield_person. The+on a tree row is primary with a pointer cursor. Untranslated portlet titles no longer print as raw i18n keys.The user-count badge takes a real grey.
text-color-secondaryis a PrimeFlex-era class — PrimeFlex is not installed andtailwindcss-primeuidoes not provide it either, so it compiles to nothing and elements inherit the label's near-black. Verified against the built CSS rather than assumed. 104 more occurrences survive across 35 files and need their own pass.Checklist
Additional Info
Verification — 36 projects linted, 35 tested,
nx format:checkclean, and thedotcms-uiproduction build green. Thedot-rolessuite is at 136 tests.Security note. While tracing the tool-group endpoints,
GET /api/v1/roles/layoutsturned out to have no authentication gate — the only one of 16 endpoints inRoleResourcewithout aWebResource.InitBuilderblock. Confirmed at runtime: it returns200with the full tool-group catalog to an unauthenticated caller, while three sibling endpoints return401under identical conditions. It predates this work (introduced 2023-05-26, shipped since v23.06) and is filed separately as #37259. This PR does not change that behaviour, but it does put the endpoint on a more exercised path.Standards note.
ANGULAR_STANDARDS.mdasks for Signal Forms on new forms, but the standard validation components are Reactive-Forms-bound —dotFieldRequiredinjectsFormGroupDirectiveanddot-field-validation-messagetakes anAbstractControl. These dialogs stay on Reactive Forms; adopting Signal Forms would mean dropping both.Follow-ups not included here
loadRootRoles(true)→falseis now possible thanks tochildCount, and would lighten the initial payload considerably on installs with many roles. It changes lazy-load behaviour, so it deserves measuring on its own.onNodeExpandmarks a node fetched before the load resolves, so a failed lazy-load never retries.text-color-secondarysweep described above.🤖 Generated with Claude Code
This PR fixes: #36930