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..978b6aff759a --- /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,27 @@ + + + {{ 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..896313abf7f8 --- /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,117 @@ +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