feat(users): Angular Beta portlet with list view and Filter by - #36862
feat(users): Angular Beta portlet with list view and Filter by#36862hmoreras wants to merge 11 commits into
Conversation
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 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 ( New Issues
Verified correct
The two mediums are non-blocking. Nice, well-tested PR. • branch |
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>
- 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>
Summary
users-betaportletId, "Users (Beta)" label) with a<portlet-url>/users</portlet-url>override so the URL bar shows/userswhile the Dojo portlet stays untouched at#/c/users.Inactivechip,Back-end/Front-endaccess chips./api/v1/users/filterusing the actual backend param names (query,roleKey,per_page,orderby).Screenshare.-.2026-08-03.11_44_56.AM.mp4
What ships
core-web/libs/portlets/dot-users/:DotUsersShellComponent,DotUsersListComponent+DotUsersListStore,DotUsersFilterByComponent, and a stubDotUsersCreateComponent.DotUsersServicecovering the paginated list query and single-user delete.portlet.xmlgains ausers-betaentry with<portlet-url>/users</portlet-url>.Language.propertiesgets the portlet title and the portlet-scoped i18n keys (search placeholder, table headers, Filter by, chips, empty state, etc.).app.routes.tsmaps/usersto the lazy-loaded module.SerializationHelperTest.javabumps the expected portlet count and adds assertions for bothusersandusers-beta.What defers to sibling work
—for every row./api/v1/users/filterdoes 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.Notes
usersportletId, the Angular build is opt-in via "Add Portlet" in Tools, and the disruptive rename-and-swap is deferred to a separate follow-up.Filter bychip and the userId-aware search rely on the endpoint changes merged in #36794: feat(users): extend /api/v1/users/filter with userId search and role filter #36846 (fixes Users REST: extend /api/v1/users/filter to support userId search and role filter #36794). Without those the filter param is silently ignored; with them the FE works end-to-end.DELETE /api/v1/users/{userId}in parallel per selected row.replacementUserIddefaults to the acting admin on the backend, matching the current single-user delete behavior.Test plan
pnpm nx test portlets-dot-users-portlet— 28/28 pass locallypnpm nx lint portlets-dot-users-portlet— clean/usersN selectedcounter updates as rows toggleInactivechip inline with the name#/c/usersand unchangedCloses #36716