From 2c8337bbeed74c33eb95585f3748aa32941530d0 Mon Sep 17 00:00:00 2001 From: Thomas Kaul <4159106+dtslvr@users.noreply.github.com> Date: Sat, 25 Jul 2026 19:10:16 +0200 Subject: [PATCH] Task/improve user experience of users table in admin control panel (#7425) * Improve user experience of users table in admin control panel * Update changelog --- CHANGELOG.md | 1 + .../admin-users/admin-users.component.ts | 29 ++++++++++-------- .../src/app/pages/admin/admin-page.routes.ts | 30 ++++++++++++++----- 3 files changed, 40 insertions(+), 20 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 95679007d..ba675ba45 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -11,6 +11,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Included cash in the performance calculation of the portfolio - Moved the support for tags in the account from experimental to general availability +- Improved the user experience of the users table in the admin control panel by eliminating the reload when opening and closing the user detail dialog - Upgraded `countup.js` from version `2.10.0` to `2.10.1` - Upgraded `dotenv` from version `17.2.3` to `17.4.2` - Upgraded `dotenv-expand` from version `12.0.3` to `13.0.0` diff --git a/apps/client/src/app/components/admin-users/admin-users.component.ts b/apps/client/src/app/components/admin-users/admin-users.component.ts index 305c48dcf..0060b38ee 100644 --- a/apps/client/src/app/components/admin-users/admin-users.component.ts +++ b/apps/client/src/app/components/admin-users/admin-users.component.ts @@ -66,7 +66,7 @@ import ms from 'ms'; import { DeviceDetectorService } from 'ngx-device-detector'; import { NgxSkeletonLoaderModule } from 'ngx-skeleton-loader'; import { interval } from 'rxjs'; -import { switchMap, tap } from 'rxjs/operators'; +import { distinctUntilChanged, map, switchMap, tap } from 'rxjs/operators'; @Component({ changeDetection: ChangeDetectionStrategy.OnPush, @@ -173,11 +173,13 @@ export class GfAdminUsersComponent implements OnInit { this.changeDetectorRef.markForCheck(); } }), - switchMap(() => this.route.paramMap) + switchMap(() => this.route.paramMap), + map((params) => { + return params.get('userId'); + }), + distinctUntilChanged() ) - .subscribe((params) => { - const userId = params.get('userId'); - + .subscribe((userId) => { if (userId) { this.openUserDetailDialog(userId); } @@ -236,12 +238,17 @@ export class GfAdminUsersComponent implements OnInit { .deleteUser(aId) .pipe(takeUntilDestroyed(this.destroyRef)) .subscribe(() => { - this.router.navigate(['..'], { relativeTo: this.route }); + this.router.navigate(this.routerLinkAdminControlUsers); + + this.fetchUsers({ + pageIndex: this.paginator().pageIndex, + showLoading: false + }); }); }, confirmType: ConfirmationDialogType.Warn, discardFn: () => { - this.router.navigate(['..'], { relativeTo: this.route }); + this.router.navigate(this.routerLinkAdminControlUsers); }, title: $localize`Do you really want to delete this user?` }); @@ -283,9 +290,7 @@ export class GfAdminUsersComponent implements OnInit { } protected onOpenUserDetailDialog(userId: string) { - this.router.navigate( - internalRoutes.adminControl.subRoutes.users.routerLink.concat(userId) - ); + this.router.navigate(this.routerLinkAdminControlUsers.concat(userId)); } private fetchUsers({ @@ -341,9 +346,7 @@ export class GfAdminUsersComponent implements OnInit { if (data?.action === 'delete' && data?.userId) { this.onDeleteUser(data.userId); } else { - this.router.navigate( - internalRoutes.adminControl.subRoutes.users.routerLink - ); + this.router.navigate(this.routerLinkAdminControlUsers); } }); } diff --git a/apps/client/src/app/pages/admin/admin-page.routes.ts b/apps/client/src/app/pages/admin/admin-page.routes.ts index c5309edbb..30d6728ae 100644 --- a/apps/client/src/app/pages/admin/admin-page.routes.ts +++ b/apps/client/src/app/pages/admin/admin-page.routes.ts @@ -6,10 +6,31 @@ import { GfAdminUsersComponent } from '@ghostfolio/client/components/admin-users import { AuthGuard } from '@ghostfolio/client/core/auth.guard'; import { internalRoutes } from '@ghostfolio/common/routes/routes'; -import { Routes } from '@angular/router'; +import { Routes, UrlMatcher, UrlSegment } from '@angular/router'; import { AdminPageComponent } from './admin-page.component'; +// Matches both the users list and the user detail dialog route within a single +// route configuration so that the component is reused (and not re-created) when +// the user detail dialog is opened or closed +const usersMatcher: UrlMatcher = (segments: UrlSegment[]) => { + if ( + segments[0]?.path !== internalRoutes.adminControl.subRoutes.users.path || + segments.length > 2 + ) { + return null; + } + + if (segments.length === 2) { + return { + consumed: segments, + posParams: { userId: segments[1] } + }; + } + + return { consumed: segments }; +}; + export const routes: Routes = [ { canActivate: [AuthGuard], @@ -35,13 +56,8 @@ export const routes: Routes = [ title: internalRoutes.adminControl.subRoutes.settings.title }, { - path: internalRoutes.adminControl.subRoutes.users.path, - component: GfAdminUsersComponent, - title: internalRoutes.adminControl.subRoutes.users.title - }, - { - path: `${internalRoutes.adminControl.subRoutes.users.path}/:userId`, component: GfAdminUsersComponent, + matcher: usersMatcher, title: internalRoutes.adminControl.subRoutes.users.title } ],