Skip to content

feat(users): Angular Beta portlet with list view and Filter by - #36862

Open
hmoreras wants to merge 11 commits into
mainfrom
issue-36716-users-portlet
Open

feat(users): Angular Beta portlet with list view and Filter by#36862
hmoreras wants to merge 11 commits into
mainfrom
issue-36716-users-portlet

Conversation

@hmoreras

@hmoreras hmoreras commented Aug 3, 2026

Copy link
Copy Markdown
Member

Summary

  • Scaffold the Angular Users portlet as an opt-in Beta (users-beta portletId, "Users (Beta)" label) with a <portlet-url>/users</portlet-url> override so the URL bar shows /users while the Dojo portlet stays untouched at #/c/users.
  • Deliver the list view: debounced search, single-select Filter by chip (All access / Back-end / Front-end), sortable columns, pagination, bulk delete, Inactive chip, Back-end / Front-end access chips.
  • Wire the search box and Filter by chip to /api/v1/users/filter using the actual backend param names (query, roleKey, per_page, orderby).
Screenshare.-.2026-08-03.11_44_56.AM.mp4

What ships

  • Nx library at core-web/libs/portlets/dot-users/:
    • DotUsersShellComponent, DotUsersListComponent + DotUsersListStore, DotUsersFilterByComponent, and a stub DotUsersCreateComponent.
    • DotUsersService covering the paginated list query and single-user delete.
  • Beta portlet registration:
    • portlet.xml gains a users-beta entry with <portlet-url>/users</portlet-url>.
    • Language.properties gets the portlet title and the portlet-scoped i18n keys (search placeholder, table headers, Filter by, chips, empty state, etc.).
    • app.routes.ts maps /users to the lazy-loaded module.
    • SerializationHelperTest.java bumps the expected portlet count and adds assertions for both users and users-beta.
  • 28 unit tests (Spectator + Jest) covering service, store, list view, and the Filter by component.

What defers to sibling work

  • The full Create / Edit User dialog with its four tabs (Profile, Roles, Permissions, API Tokens) — this PR ships the entry-point stub only; the tabs are delivered by their own tickets under Epic Dojo to Angular: Users Management Portlet #34736.
  • The Roles column renders for every row. /api/v1/users/filter does not include a user's granted roles in the response payload; populating this column requires either a batch endpoint or an inline addition to the user shape, which is captured in the spike and out of scope for the list-view ticket.
  • Add to Bundle bulk action is not wired — pending confirmation on the Bundle REST contract for user assets.

Notes

Test plan

  • pnpm nx test portlets-dot-users-portlet — 28/28 pass locally
  • pnpm nx lint portlets-dot-users-portlet — clean
  • Manual: open a dotCMS build from this branch, add the "Users (Beta)" portlet from Tools → Add Portlet, verify the URL bar shows /users
  • Manual: verify search filters as you type (against name, email, and user ID)
  • Manual: verify Filter by → Back-end shows only backend users; Front-end shows only front-end users; All access clears the filter
  • Manual: sort by User, Email, and Last login (asc + desc)
  • Manual: paginate through 20 / 40 / 100 page sizes
  • Manual: multi-select rows → Delete → confirmation → users are removed; N selected counter updates as rows toggle
  • Manual: verify inactive users show the Inactive chip inline with the name
  • Manual: verify the existing Dojo Users portlet is still reachable at #/c/users and unchanged

Closes #36716

hmoreras and others added 4 commits July 30, 2026 14:35
Register the Angular rewrite of the User Admin portlet as an opt-in Beta
portlet (users-beta / "Users (Beta)"). The Dojo portlet stays untouched;
admins add the Beta from "Add Portlet" in Tools.

- Nx library at libs/portlets/dot-users with Shell + List + Store + service
- List view: debounced search, sortable columns (User / Email / Last login),
  pagination, multi-select bulk delete, Inactive chip, Back-end / Front-end
  access chips
- Create/Edit dialog is a stub — the four-tab profile flow lands separately
- portlet.xml, Language.properties, app.routes.ts, SerializationHelperTest
  updated to register the users-beta portletId
- Not added to any default layout

Refs #36716

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
- Route the Angular portlet at /users via a <portlet-url> override on
  the users-beta portletId. Menu label stays "Users (Beta)" and Beta
  opt-in / permissions are preserved. The Dojo portlet remains at
  #/c/users via the legacy iframe path, so both coexist without
  conflict.
- Delete button uses a neutral outline style (no danger severity, no
  icon) to match the New button.
- Toolbar shows "N selected" next to Delete when the selection is
  non-empty; spacing between bulk actions is widened.
- New Filter by chip (single-select) with All access / Back-end /
  Front-end options, following the shared dot-chip-filter + popover +
  listbox pattern used by publishing-queue. Wires roleKey to
  /api/v1/users/filter — the backend param lands via #36794, until
  then the endpoint silently ignores it.

Refs #36716

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The /api/v1/users/filter endpoint expects query, per_page, and orderby
as its URL param names; the FE was sending filter, perPage, and orderBy,
so the backend silently ignored them and returned the unfiltered list on
every keystroke. Rename the wired params (internal DotUsersPaginatedParams
field names unchanged) and update the search placeholder to reflect that
matches now include user ID in addition to name and email.

Refs #36716, #36794

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

claude Bot commented Aug 3, 2026

Copy link
Copy Markdown
Contributor

Claude finished @hmoreras's task in 10m 3s —— View job


Code Review — PR #36862 (Users Beta portlet)

I reviewed the frontend library (service, store, list, filter-by, shell, create stub) and the backend touchpoints (portlet.xml, Language.properties, SerializationHelperTest.java, routes). I cross-checked the FE param names and pagination math against UserResource#filter, PaginationUtil, and UserPaginator.

New Issues

  • 🟡 Medium: core-web/libs/portlets/dot-users/src/lib/dot-users-list/store/dot-users-list.store.ts:104-112setFilter/setRoleFilter reset page to 1 and call loadUsers() directly. When the user is on a page > 1, the <p-table> [first] binding (dot-users-list.component.html:49) also changes to 0, which makes PrimeNG emit a second onLazyLoadapplyLazyLoad → a second loadUsers(). switchMap cancels the first request so there's no stale-data corruption, but it's a redundant HTTP round-trip on every filter/search change made from a non-first page. Non-blocking. Fix this →

  • 🟡 Medium: core-web/libs/portlets/dot-users/src/lib/dot-users-list/dot-users-list.component.html:82-91 — the loading skeleton is rendered inside the body template, which only iterates over store.users(). On the initial load users is empty, so no skeleton renders and the emptymessage template shows the "No users yet" empty state instead — a brief but misleading flash before the first page arrives. Subsequent loads (search/sort/paginate over an already-populated list) show skeletons correctly. Consider gating the empty state on status() === 'loaded'. Non-blocking.

Verified correct

  • Query params match the backend contract: query, page, per_page, orderby, direction, roleKey, includeanonymous, includedefault all line up with UserPaginator/PaginationUtil constants.
  • Pagination is consistently 1-based: FE page: 1PaginationUtil (page-1)*perPage = 0 offset. No off-by-one.
  • Sort fields (firstName, emailAddress, lastLoginDate) survive SQLUtil.sanitizeSortBy and resolve to real user_ columns under Postgres' identifier lowercasing.
  • deleteSelectedUsers guards against double-submit (status() === 'loading' early return) and against a success toast when every delete fails; partial failures surface via DotHttpErrorManagerService + WARNING toast. Error paths are handled.
  • rxMethod + switchMap with no take(1) inside — matches the portlet guidance; cancellation is preserved.
  • userId is encodeURIComponent-escaped in the delete URL.
  • SerializationHelperTest count bump (55→56) and the users / users-beta assertions are correct; both portlet ids exist in portlet.xml.
  • Beta pattern intact: legacy users portlet keeps #/c/users, new users-beta adds <portlet-url>/users</portlet-url>; no route collision.
  • All user-facing strings use i18n keys; data-testid present on interactive elements. Conventions followed.

The two mediums are non-blocking. Nice, well-tested PR.

• branch issue-36716-users-portlet

The two HTML files had drifted from the prettier canonical form because
subsequent template Edits after the lint-staged hook ran did not
re-invoke format-write. CI's format-test job caught the drift.

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

Copy link
Copy Markdown

🤖 No test run has been triggered as your Meticulous project has been deactivated (since you haven't viewed any test results in a while). Click here to reactivate.

Last updated for commit d2080c8. This comment will update as new commits are pushed.

- Replace the if-per-param chain in `getUsersPaginated` with a
  declarative `[key, value]` array folded through a single reduce.
  Same wire behavior (undefined and falsy values are skipped), fewer
  lines, easier to add a new query param.
- Revert unrelated changes the Nx library generator introduced into
  `.gitignore` (`.angular` — already covered by the repo-root ignore)
  and into `package.json` / `pnpm-lock.yaml` (a pinned downgrade of
  `jest-util` to 30.3.0 that isn't needed for the portlet to build,
  test, or lint).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
- Swap the hand-rolled remaining counter in DotUsersListStore.deleteSelectedUsers for forkJoin, mapping each deletion to true/false so per-request errors don't stall completion.
- Push a SUCCESS toast on full delete, WARNING (partial-success) toast when some fail; per-error toasts continue to flow through DotHttpErrorManagerService.
- Add users.delete.success / users.delete.partial-success i18n keys and cover both toast paths (plus the all-failed silent case) in the store spec.

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

Applies the medium-severity feedback from the automated PR review.

- Convert loadUsers to `rxMethod<void>` with `switchMap`. Rapid sort /
  filter / page changes now cancel the in-flight request so a stale
  response can't overwrite a fresh page. Setters (setFilter,
  setRoleFilter, setPagination, setSort) trigger loadUsers explicitly,
  and the reactive `withHooks/effect` bootstrap is removed — PrimeNG's
  onLazyLoad already fires the initial fetch on mount, so the previous
  design double-loaded the table on every visit.
- Guard `deleteSelectedUsers` against double-click by short-circuiting
  when `status === 'loading'` at the top of the method. Prevents two
  parallel forkJoin waves against the same selection.
- Replace the Gravatar `<img>` with PrimeNG `<p-avatar [label]="…" />`
  rendering the user's initials locally. No third-party request, no
  email-hash exfiltration, matches the design system.
- Wire `[sortField]` and `[sortOrder]` on `<p-table>` so the column
  sort indicator reflects the store's initial `lastLoginDate DESC` on
  mount instead of appearing unsorted.
- Extend the store spec: the "load on init" test now exercises
  `loadUsers()` directly, and a new test asserts the double-click
  guard using a `NEVER` observable to keep the first request in
  flight.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
- Replace `setPagination` + `setSort` with a single `applyLazyLoad`
  method that patches page/rows/sort atomically and dispatches one
  `loadUsers()`. Because `[sortField]/[sortOrder]` are bound on the
  table, PrimeNG includes `sortField` in every lazy-load event —
  including plain pagination — so the previous two-setter path fired
  `loadUsers()` twice on every table interaction. `switchMap` cancels
  the first HTTP request, but the round-trip was still wasteful.
- Move the response-consuming `tap` inside the source Observable's
  `.pipe()` instead of the standalone `pipe(...)` chain. Angular's
  strict production build (`nx build dotcms-ui --configuration=production`)
  cannot infer the `DotCMSAPIResponse<DotUserListItem[]>` type across a
  standalone `pipe(tap, switchMap, tap)` and fails with `TS2339:
  Property 'entity' does not exist on type 'unknown'`. Inside the
  strongly-typed `.pipe()` the response is inferred correctly.
- Update the store spec to test `applyLazyLoad` as the single mutator
  (one `getUsersPaginated` call verifies the batch), and refresh the
  component spec mock.

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 dotCMS : Admin Tools Team : Modernization

Projects

Status: No status

Development

Successfully merging this pull request may close these issues.

Users portlet: Beta scaffold and list view Users REST: extend /api/v1/users/filter to support userId search and role filter

2 participants