From c3237e0c7d5cc5674f8d62760e7aa870e810309e Mon Sep 17 00:00:00 2001 From: AP2300 Date: Mon, 10 Aug 2026 16:12:47 -0400 Subject: [PATCH 1/6] feat(users): Profile tab, dialog shell, list CRUD wiring (#36717) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Ships the Users portlet Create/Edit dialog shell with the Profile tab fully wired and placeholders for the three sibling tabs (delivered by #36718, #36719, #36720). Dialog / Profile tab: - 4-tab strip with Profile as the only functional tab; Roles, Permissions, and API Tokens render "Coming soon" placeholders - Header with avatar + name + Active status chip - Account section: first/last name, email, password + confirm, Active - Additional Info section: prefix/suffix/title/company/website - Access section: disabled (values informational only), shows admin / backend / frontend / hasConsoleAccess pulled from the loaded user - Delete User section (edit mode) with required replacement-user picker and email-typed confirmation List CRUD: - DotUsersService gains getUser/createUser/updateUser using POST/PUT/GET /api/v1/users; roles field intentionally omitted on update so backend preserves role membership (see UserResource#processRoles) - DotUsersListStore gains createUser / updateUser / deleteSingleUser - Bulk delete on the list toolbar now shows the same replacement picker instead of the old p-confirmDialog - /users route now resolves to the new users-beta portlet id Shared: - DotUsersReplacementPickerComponent — server-backed autocomplete used by both delete flows; excludes the users being deleted from suggestions client-side Test coverage: 56 tests in the portlet + 46 in data-access all pass. Co-Authored-By: Claude Opus 4.7 (1M context) --- .../lib/dot-router/dot-router.service.spec.ts | 6 + .../src/lib/dot-router/dot-router.service.ts | 7 +- ...ot-users-replacement-picker.component.html | 24 ++ .../dot-users-replacement-picker.component.ts | 110 +++++ .../dot-users-create.component.html | 175 +++++++- .../dot-users-create.component.scss | 30 ++ .../dot-users-create.component.spec.ts | 255 ++++++++++++ .../dot-users-create.component.ts | 379 +++++++++++++++++- .../dot-users-create/dot-users-form.model.ts | 72 ++++ .../dot-users-profile-tab.component.html | 320 +++++++++++++++ .../dot-users-profile-tab.component.scss | 9 + .../dot-users-profile-tab.component.ts | 112 ++++++ .../dot-users-list.component.html | 49 ++- .../dot-users-list.component.spec.ts | 74 +++- .../dot-users-list.component.ts | 134 +++++-- .../store/dot-users-list.store.spec.ts | 117 +++++- .../store/dot-users-list.store.ts | 95 ++++- .../src/lib/services/dot-users.service.ts | 91 +++++ .../libs/portlets/dot-users/src/test-setup.ts | 6 + .../WEB-INF/messages/Language.properties | 72 +++- 20 files changed, 2058 insertions(+), 79 deletions(-) create mode 100644 core-web/libs/portlets/dot-users/src/lib/components/dot-users-replacement-picker/dot-users-replacement-picker.component.html create mode 100644 core-web/libs/portlets/dot-users/src/lib/components/dot-users-replacement-picker/dot-users-replacement-picker.component.ts create mode 100644 core-web/libs/portlets/dot-users/src/lib/dot-users-create/dot-users-create.component.scss create mode 100644 core-web/libs/portlets/dot-users/src/lib/dot-users-create/dot-users-create.component.spec.ts create mode 100644 core-web/libs/portlets/dot-users/src/lib/dot-users-create/dot-users-form.model.ts create mode 100644 core-web/libs/portlets/dot-users/src/lib/dot-users-create/tabs/dot-users-profile-tab/dot-users-profile-tab.component.html create mode 100644 core-web/libs/portlets/dot-users/src/lib/dot-users-create/tabs/dot-users-profile-tab/dot-users-profile-tab.component.scss create mode 100644 core-web/libs/portlets/dot-users/src/lib/dot-users-create/tabs/dot-users-profile-tab/dot-users-profile-tab.component.ts diff --git a/core-web/libs/data-access/src/lib/dot-router/dot-router.service.spec.ts b/core-web/libs/data-access/src/lib/dot-router/dot-router.service.spec.ts index 0a67d588d8e1..a34350db092f 100644 --- a/core-web/libs/data-access/src/lib/dot-router/dot-router.service.spec.ts +++ b/core-web/libs/data-access/src/lib/dot-router/dot-router.service.spec.ts @@ -336,6 +336,12 @@ describe('DotRouterService', () => { ); }); + it('should resolve the /users URL slug to the users-beta portlet ID', () => { + expect(service.getPortletId('/users')).toBe('users-beta'); + expect(service.getPortletId('/c/users')).toBe('users-beta'); + expect(service.getPortletId('#/users?test=value')).toBe('users-beta'); + }); + it('should navigate replacing URL params', () => { const params = { id: 'content' }; service.replaceQueryParams(params); diff --git a/core-web/libs/data-access/src/lib/dot-router/dot-router.service.ts b/core-web/libs/data-access/src/lib/dot-router/dot-router.service.ts index 5c6740239914..c4d6aa2459a4 100644 --- a/core-web/libs/data-access/src/lib/dot-router/dot-router.service.ts +++ b/core-web/libs/data-access/src/lib/dot-router/dot-router.service.ts @@ -480,5 +480,10 @@ const PORTLET_ID_RESOLVERS: Record string> = // URL is kebab-case; backend portlet-name stays snake_case for DB compat (#35809). 'velocity-playground': () => { return 'velocity_playground'; - } + }, + // The Angular Users portlet is registered under `users-beta` in portlet.xml + // with a `/users` override, so the URL is `/users` + // while the menu id stays `users-beta`. Without this mapping the + // MenuGuardService can't match the menu entry and bounces to Getting Started. + users: () => 'users-beta' }; diff --git a/core-web/libs/portlets/dot-users/src/lib/components/dot-users-replacement-picker/dot-users-replacement-picker.component.html b/core-web/libs/portlets/dot-users/src/lib/components/dot-users-replacement-picker/dot-users-replacement-picker.component.html new file mode 100644 index 000000000000..7fd1153bd16b --- /dev/null +++ b/core-web/libs/portlets/dot-users/src/lib/components/dot-users-replacement-picker/dot-users-replacement-picker.component.html @@ -0,0 +1,24 @@ + + + {{ displayName(candidate) }} + + +
+ {{ displayName(candidate) }} + {{ candidate.emailAddress }} +
+
+
diff --git a/core-web/libs/portlets/dot-users/src/lib/components/dot-users-replacement-picker/dot-users-replacement-picker.component.ts b/core-web/libs/portlets/dot-users/src/lib/components/dot-users-replacement-picker/dot-users-replacement-picker.component.ts new file mode 100644 index 000000000000..a5d12c09c752 --- /dev/null +++ b/core-web/libs/portlets/dot-users/src/lib/components/dot-users-replacement-picker/dot-users-replacement-picker.component.ts @@ -0,0 +1,110 @@ +import { + ChangeDetectionStrategy, + Component, + DestroyRef, + inject, + input, + output, + signal +} from '@angular/core'; +import { takeUntilDestroyed } from '@angular/core/rxjs-interop'; +import { FormsModule } from '@angular/forms'; + +import { AutoCompleteCompleteEvent, AutoCompleteModule } from 'primeng/autocomplete'; + +import { take } from 'rxjs/operators'; + +import { DotMessagePipe } from '@dotcms/ui'; + +import { DotUserListItem, DotUsersService } from '../../services/dot-users.service'; + +/** + * Server-backed replacement-user picker used by the delete flows. + * Reused by the single-user delete confirm (inside the profile + * dialog) and the bulk-delete confirm (on the list toolbar). + * + * The excluded-ids input keeps deletion targets out of the + * suggestion list on the client — the backend also rejects invalid + * replacements, but pre-filtering avoids showing picks that would + * fail on submit. + */ +@Component({ + selector: 'dot-users-replacement-picker', + standalone: true, + imports: [FormsModule, AutoCompleteModule, DotMessagePipe], + templateUrl: './dot-users-replacement-picker.component.html', + changeDetection: ChangeDetectionStrategy.OnPush, + host: { class: 'block' } +}) +export class DotUsersReplacementPickerComponent { + private readonly usersService = inject(DotUsersService); + private readonly destroyRef = inject(DestroyRef); + + /** ID passed to the underlying so an external