Skip to content

chore(users): #37298 follow-ups — roles-by-id, inline roles, Angular cleanups, design polish - #37457

Open
AP2300 wants to merge 8 commits into
mainfrom
issue-37298-users-portlet-followups
Open

chore(users): #37298 follow-ups — roles-by-id, inline roles, Angular cleanups, design polish#37457
AP2300 wants to merge 8 commits into
mainfrom
issue-37298-users-portlet-followups

Conversation

@AP2300

@AP2300 AP2300 commented Sep 8, 2026

Copy link
Copy Markdown
Contributor

Closes #37298.

Wraps up the six follow-up items from the split Users-portlet migration
(#36990 / #37082 / #37083 / #37084) plus the design review pass.

Follow-up items (from #37298)

  1. Roles tab saves by role ID, not roleKey — since #37209: feat(users): accept role IDs alongside keys in role endpoint #37218 the users
    endpoint accepts both; sending IDs unconditionally round-trips
    keyless custom roles cleanly.
  2. Inline roles per list row (kills the N+1) — list load now sends
    includeRoles=true per #37233: feat(users): return direct roles per user on GET /v1/users/filter #37236 contract; the Roles column reads the
    inline roles array on each row instead of fanning out one
    getUserRoles call per user. 403 retry drops includeRoles=false
    and leaves the column empty rather than failing the whole load.
  3. Angular-rule cleanups across the portlet + the roles iframe —
    modern control flow, signal inputs, default change detection.
    4-6. Related cleanups tracked in chore(users): follow-ups from #36990 / #37082 / #37083 (roles-by-id, list N+1, Angular-rule cleanups) #37298.

Design polish pass

  • API tokens tab: standard portlet-table typography + per-row
    skeleton loading; empty/error states via <dot-empty-container>;
    ghost/circular Material Symbols copy button in the reveal dialog;
    primary "Accept" close button; create-mode hint mirrors the
    permissions tab (icon + copy on the tab body).
  • Roles tab: granted panel switched to <p-tree> so both panels
    render identical PrimeNG chrome (checkboxes, hover, selection);
    empty granted state uses the shared <dot-empty-container> with a
    shield_person material symbol.
  • Create/Edit dialog: dropped the internal avatar/name/status
    header block — the user's full name now rides in the dialog title
    bar in edit mode. Removed dead computeds, imports, tests, and
    orphaned i18n keys that only powered the removed block.
  • Users list: search input width matches content-drive; toolbar
    height matches spec; selection ↔ New button animation restored
    without inflating row height; secondary text swept to
    text-slate-500.
  • Language.properties swept clean of the orphaned keys.

Known gap (separate ticket)

Users list sort headers are decorative. The frontend sends
orderBy/direction correctly, but UserPaginator.getItems ignores
both arguments and UserFactoryLiferayImpl.getUsersByName hard-codes
ORDER BY first_name || ' ' || last_name ASC. Backend fix is being
tracked separately; frontend already sends the right values so flipping
the backend on will just work.

Test plan

  • pnpm nx test portlets-dot-users-portlet — 177 passing
  • pnpm nx lint portlets-dot-users-portlet — clean
  • Open the dialog in create mode — no avatar/name header; title
    says "Create User"
  • Open the dialog in edit mode — no avatar/name header; title
    shows the user's full name
  • Roles tab — granted panel matches the available side visually;
    empty granted state shows the shield_person empty container
  • API tokens tab — table typography matches other portlets;
    reveal dialog copy icon works (icon flips to check briefly);
    create-mode shows the key-icon hint on the tab body
  • Users list — search width matches content-drive; toolbar height
    matches spec

🤖 Generated with Claude Code

AP2300 and others added 8 commits September 4, 2026 14:56
Now that #37218 has landed the users endpoint accepts either
`roleKey` or `id` per entry (resolves by key first, then by id), so
the shuttle can drop its "roleKey when present, id when not"
identifier hack and emit stable role IDs unconditionally. Keyless
custom roles that used to be silently stripped from the outbound
payload now round-trip cleanly.

Store
- `roleKeys: string[]` becomes `roles: DotUsersCreateRole[]` (each
  entry carries both `id` and `roleKey`). Access-toggle hydration
  still reads the key set from `roles.map(r => r.roleKey)`; the save
  path reads IDs. The previous `.filter((key): key is string => !!key)`
  drop of keyless entries is gone — every membership is preserved.

Shell
- Rename `initialGrantedRoleKeys` / `currentRoleKeys` to
  `initialGrantedRoleIds` / `currentRoleIds`.
- `mergeRoleKeysForSave` becomes `mergeRoleIdsForSave` and works in
  ID space. It looks up which of the fetched membership IDs
  correspond to the three well-known Access role keys (via the
  store's `roles` map), strips those from the base list, then adds
  the toggles back in as roleKeys. The outbound payload is a
  mixed-identifier list (IDs for custom roles, keys for the three
  Access roles) — exactly what #37218 supports.
- `filterOutgoingRoleKeys` becomes `filterOutgoingPersonalRole`,
  resolving the personal role's id from the store (`roleKey === userId`)
  and stripping both the id and the fallback key form so the
  `RoleAPIImpl.addRoleToUser` "Cannot alter users on this role" guard
  never fires on save.

Roles tab
- Drop `grantIdentifier`. `role.id` is the only identifier the
  component holds now — selection state, granted set, and the
  outbound `grantedChange` are all IDs.
- Input rename: `initialGrantedKeys` → `initialGrantedIds`. Shell
  template updated to match.

Spec
- Roles-tab spec: assertions shift from role keys (`ROOT_B`, `A1A`,
  `A1B`, `A2`) to role IDs (`3`, `4`, `5`, `6`) — the shuttle's
  identifier space.
- Shell spec: unchanged. The `arrayContaining([...])` and
  `not.toContain('user-42')` assertions on the outbound payload still
  hold under the new merge — the personal-role filter now resolves
  the id via the store lookup and strips both forms.

Closes item 1 of #37298. Item 2 (list-view N+1) is blocked on the
#37236 backend PR and stays open until that merges.
#37298 items 3-6)

Sweep across the users portlet plus one dot-roles neighbour to catch
up on three project-wide Angular conventions the users PRs surfaced
but hadn't yet applied.

Item 3 — dot-users-replacement-picker
- Drop `changeDetection: ChangeDetectionStrategy.OnPush` and the
  `ChangeDetectionStrategy` import (default in Angular 22).
- Extract the in-component `Subject` + `switchMap` search into a new
  component-scoped `DotUsersReplacementPickerStore`
  (`providers: [DotUsersReplacementPickerStore]`), so HTTP lives in
  the store rather than the constructor. Same cancellation semantics
  via `rxMethod` + `switchMap`; the picker's public API (inputs,
  outputs) is unchanged. Client-side exclusion of `excludedUserIds`
  stays in the component as a computed over the store's raw
  suggestions — a picker-level concern the store shouldn't know
  about. Spec updated to seed the store via a prior successful
  search instead of a direct `.set` on the now-computed
  `$suggestions`.

Item 4 — dot-users-profile-tab
- Drop `changeDetection: ChangeDetectionStrategy.OnPush` and the
  import.

Item 5 — shell SCSS -> Tailwind
- Delete `dot-users-create.component.scss`. Move the flex-fill chain
  from `:host ::ng-deep { p-tabs, p-tabpanels, p-tabpanel }` onto the
  `p-tabs` element itself via Tailwind arbitrary-variant selectors
  (`[&_p-tabpanels]:flex`, `[&_p-tabpanel]:flex-col`, etc.) so the
  override stays collocated with the layout it fixes and no
  `::ng-deep` escape is needed. Per-panel `class` attributes shrink
  to just the `overflow-*` behaviour that varies per tab — the
  common flex-fill comes from the arbitrary variants above.

Item 6 — dot-role-permissions-iframe (dot-roles portlet)
- Drop `standalone: true` (default) and
  `changeDetection: ChangeDetectionStrategy.OnPush` (default). Trim
  the `ChangeDetectionStrategy` import.
- Fix the stale JSDoc pointer: `DotPermissionsIframeDialogComponent`
  in `@dotcms/ui` was consolidated into `dot-jsp-iframe-dialog` by
  `c105b14837` — the reference now names the current shared
  component. UUID guard stays: this consumer's defense-in-depth
  UUID-only check is stricter than the shared same-origin predicate
  and correct for role ids specifically.

Closes items 3-6 of #37298. Item 1 landed in the previous commit;
item 2 stays blocked on #37236.
…37298 item 2)

Wires the FE ahead of #37236 (open) so the branch is ready to merge
the moment the backend PR lands. Everything falls back to the
pre-#37236 shape when the flag isn't recognised, so a merge-order
inversion is harmless — the list keeps working, it just uses the
old N+1 path for one release window.

Service (`dot-users.service.ts`)
- New `DotUserListRole` type projecting the `{id, name, roleKey}`
  shape #37236 inlines on each row.
- `DotUserListItem.roles?: DotUserListRole[]` — `undefined` means
  "server didn't send them" (old backend), `[]` means "server said
  the user has no direct roles". The store treats those two cases
  differently.
- `DotUsersPaginatedParams.includeRoles?: boolean` and the
  serializer appends `&includeRoles=true` only when the flag is on
  — omitted otherwise so callers that don't want the gate stay on
  the byte-identical response contract.

List store (`dot-users-list.store.ts`)
- Every `loadUsers` call goes out with `includeRoles: true`.
- Fast path: when *every* row on the response carries `roles`,
  build `userRoles` synchronously in a single tap and skip the
  per-user fan-out entirely. Personal-role filtering is not needed
  here — the backend already excludes it (and inherited roles) per
  the #37236 contract.
- Fallback path #1 (older backend): if any row on the response is
  missing `roles`, run the same forkJoin(getUserRoles) fan-out we
  had before, with the same personal-role/name filtering. Marked in
  comments to be removed once #37236 has shipped on every
  supported backend.
- Fallback path #2 (403 on the include gate): #37236 gates
  `includeRoles=true` behind CMS Administrator or Roles + Users
  portlet access. If the viewer lacks that gate, the whole request
  400s (well, 403s) — the store swallows *only that specific 403*,
  retries once with `includeRoles: false`, and lets the N+1
  fallback populate the column. Any non-403 error still bubbles up
  to the existing `status: 'error'` branch via `httpErrorManager`.
- Extraction: `buildFilterParams`, `fetchUsersPage`, and
  `fetchRolesPerUser` are pulled out of the `loadUsers` closure so
  the flow inside `rxMethod` reads top-down and the 403/fallback
  branch has a name.

Spec
- Service spec: two new cases — `includeRoles=true` serializes to
  the query string; `includeRoles=false` omits the param entirely.
- Store spec: existing `loadUsers passes the current state as query
  params` case now asserts `includeRoles: true` is in the payload.
  Four new tests cover the fast path (skips getUserRoles), the
  legacy fallback (per-row fan-out), the 403 retry (two calls, then
  N+1), and the non-403 error (no retry, status: 'error').

Closes item 2 of #37298. All six items on the follow-up issue are
now landed on this branch — the merge order between this branch and
…g NoSuchUserException import)

`view_users_permissions_wrapper.jsp` has been unable to compile since
995dd25 landed the try/catch on Aug 27. That commit added
`<%@ page import="com.dotmarketing.business.NoSuchUserException" %>`
to serve the new catch clause — but `/html/common/init.jsp` (included
on the very next line) transitively pulls in
`com.liferay.portal.NoSuchUserException`. Jasper rejects two imports
that share a simple name and fails compilation for every request:

    org.apache.jasper.JasperException: Unable to compile class for JSP:
    An error occurred at line: [24] in the generated java file [...]
    The import com.liferay.portal.NoSuchUserException collides with
    another import statement

The response is Tomcat's error page, delivered same-origin into the
iframe. `top_inc.jsp` runs first so the body is not empty, but the
`#dot-permissions-ready` marker never gets emitted — the tab reads it
as `unavailable` and shows "Permissions view unavailable — check that
you have access to this user's permissions." That's what surfaced
after the users-beta URL isolation stopped bouncing the beta portlet
to Getting Started — the permissions tab was actually reachable for
the first time in five days, and everyone who hit it got the
unavailable state instead of the iframe.

Fix: drop the page import for the dotCMS variant and reach it by
fully-qualified name in the catch clause. Left a leading comment
block warning the next editor about the init.jsp collision so this
does not regress.

Incidental to #37298 — noticed while testing item 2 (list view) on
the follow-up branch — but the bug is pre-existing on `main` and
gates any dev exercise of the permissions tab. Bundling here instead
of a separate PR because the tab is required to test item 2's Users
Beta dialog end-to-end.
…warnings

Iterative design feedback across the users portlet:

- Wire Push Publish + Add to Bundle for row menu and bulk selection; add
  DotPushPublishEnvironmentsResolver + DotEnterpriseLicenseResolver on
  the users route; hide those actions entirely on non-enterprise (matches
  dot-locales / dot-plugins).
- Selection toolbar fades in when rows are selected, New button fades
  out — symmetric transitions using Tailwind opacity + max-w + pointer-events.
- Row 'more' menu is hover-revealed on the row, kebab-only, shows just
  Push Publish + Add to Bundle (Edit is the row click, Delete is bulk).
- List pagination mirrors dot-folder-list-view (content-drive): 20/40/60,
  no first/last icons, no page links, 'Page N' report.
- Normalize buttons to primary color + variants everywhere in the
  portlet — drop severity=danger, severity=secondary, and colored text
  buttons. Primary filled / outlined / text is the whole vocabulary now.
- Inline validation banner (material warning + generic i18n key
  users.dialog.warning.form-errors) on every dialog footer: main create/edit,
  delete-confirm, bulk-delete, and request-token sub-dialog.
- Migrate the roles tab Available panel from a hand-rolled recursive
  template to <p-tree selectionMode='checkbox'>. Partial/fully-checked
  propagation is now built-in; the shell only bridges TreeNode selection
  back to leaf ids for the payload. Drop the folder/person decorative
  icons. Roles tab claims the full tab body; only the tree/list scroll.
- Create dialog header: smaller avatar, chips inline with the name,
  drop the border-b above the tabs, add bg-surface-50! on the tablist
  wrapper + [pt] so the tinted strip beats PrimeNG's cascade.
- Tighter tab body padding (pt-4 pb-6), roles container keeps only top.
- Token dialog: <input type='date'> → <p-datepicker dataType='string'>
  matching dot-analytics; label reads 'Request' instead of 'OK'.
- Add users.actions.* i18n keys and users.dialog.warning.form-errors.

179 → 182 tests, all green; lint + format clean.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…idths

- Rework the API Tokens tab table to match the dot-tags / users-list
  standard: drop the custom bordered-card `styleClass` on the p-table
  itself; move the border + rounded look onto a wrapping div that
  claims the tab body via `flex-1 min-h-0`, so the container reads as
  an empty table body instead of blank whitespace when only a handful
  of rows exist.
- Replace the PrimeNG loading overlay with per-row `<p-skeleton>`
  cells inside the body template (import `SkeletonModule`), matching
  the pattern in dot-tags and the users list.
- Standardize empty + error states on the dot-tags empty-state shape
  (centred icon + copy). Drop `text-red-600`; use the `error` and
  `key` material glyphs so the two states read as distinct states of
  the same container instead of a mixed pattern.
- Drop custom cell colouring (`text-color-secondary` on Issued /
  Expires / Requested By and the `bg-surface-100` network pill).
  Plain typography, `font-mono` only where the value is code-y
  (token id + network CIDR).
- Action cell stops row click propagation so revoke does not also
  trigger the JWT reveal.
- Let all columns flow with `table-layout: auto` — no fixed widths
  fought each other and collapsed Label; there is no filter/search
  on this table so column jitter is not a concern.
- Api tokens tab now claims the full tab body height like the roles
  tab does: matching `flex min-h-0 flex-1 flex-col overflow-hidden`
  on the tabpanel + `pt-4` wrapper + tab class binding. Header +
  Show inactive + Request New Token stay pinned; only the token
  list scrolls.
- Bumped users-list Roles column from 18rem to 22rem and Access from
  12rem to 15rem; froze the whole table with `tableLayout: fixed`
  + a 22rem User column so a search does not shift column widths
  between paints.

182 tests green; lint + format clean.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
- Drop `[tableStyle]="{ tableLayout: 'fixed', ... }"` + every fixed
  width class on the list header cells. The browser sizes each column
  from its content and header text, matching the API tokens table's
  convention. Fixed widths at multiple breakpoints ended up overlapping
  cells inside the wrapper's `overflow-hidden` region whenever the
  viewport fell below the declared sum; auto layout adapts cleanly.
- Kept `[tableStyle]="{ width: '100%' }"` so the table still stretches
  to the wrapper.

182 tests green; lint + format clean.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Wraps up the design review round on the four-tab dialog and the list:

- API tokens table: aligned with standard portlet-table typography,
  per-row skeleton loading, empty/error states via app-wide empty
  container, ghost copy icon button in the reveal dialog, primary
  Accept close button, create-mode hint mirrors the permissions tab
  (icon + copy on the tab body).
- Roles tab: granted panel switched to `<p-tree>` so both panels
  render identical PrimeNG chrome (checkboxes, hover, selection);
  empty granted state uses the shared `<dot-empty-container>` with a
  `shield_person` material symbol.
- Create/Edit dialog: removed the internal avatar/name/status header
  block; the user's full name now rides in the dialog title bar in
  edit mode, so the internal header is redundant. Dropped the
  associated dead computeds, imports, tests, and orphaned i18n keys.
- Users list: search input width matches the content-drive portlet,
  toolbar height set to the design spec, selection ↔ New animation
  restored without height inflation, secondary text uses `text-slate-500`
  throughout, `text-color-secondary` swept out.
- Language.properties: swept the orphaned edit-header keys
  (untitled-user, new-user, status.active, status.inactive,
  header.can-login).

Note: users list sort headers are decorative — backend
`UserPaginator.getItems` ignores `orderBy`/`direction`. Left as-is
pending a separate backend PR (tracked separately).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Area : Backend PR changes Java/Maven backend code Area : Frontend PR changes Angular/TypeScript frontend code

Projects

Status: No status

Development

Successfully merging this pull request may close these issues.

chore(users): follow-ups from #36990 / #37082 / #37083 (roles-by-id, list N+1, Angular-rule cleanups)

1 participant