From 982324ec28ce695abe107b2585281582de00bc9e Mon Sep 17 00:00:00 2001 From: Thomas Kaul <4159106+dtslvr@users.noreply.github.com> Date: Thu, 30 Jul 2026 15:25:48 +0200 Subject: [PATCH] Align with account dialog host --- .../activity-dialog-host.component.ts | 93 +++++++++++++++---- 1 file changed, 76 insertions(+), 17 deletions(-) diff --git a/apps/client/src/app/pages/portfolio/activities/activity-dialog-host/activity-dialog-host.component.ts b/apps/client/src/app/pages/portfolio/activities/activity-dialog-host/activity-dialog-host.component.ts index e0e3fc950..8a06938d2 100644 --- a/apps/client/src/app/pages/portfolio/activities/activity-dialog-host/activity-dialog-host.component.ts +++ b/apps/client/src/app/pages/portfolio/activities/activity-dialog-host/activity-dialog-host.component.ts @@ -1,6 +1,8 @@ +import { ImpersonationStorageService } from '@ghostfolio/client/services/impersonation-storage.service'; import { UserService } from '@ghostfolio/client/services/user/user.service'; import { CreateOrderDto, UpdateOrderDto } from '@ghostfolio/common/dtos'; import { Activity, User } from '@ghostfolio/common/interfaces'; +import { hasPermission, permissions } from '@ghostfolio/common/permissions'; import { internalRoutes } from '@ghostfolio/common/routes/routes'; import { DataService } from '@ghostfolio/ui/services'; @@ -16,8 +18,14 @@ import { takeUntilDestroyed } from '@angular/core/rxjs-interop'; import { MatDialog, MatDialogRef } from '@angular/material/dialog'; import { ActivatedRoute, Router } from '@angular/router'; import { DeviceDetectorService } from 'ngx-device-detector'; -import { Observable, of } from 'rxjs'; -import { map, switchMap } from 'rxjs/operators'; +import { Observable, of, Subject } from 'rxjs'; +import { + distinctUntilChanged, + map, + switchMap, + takeUntil, + tap +} from 'rxjs/operators'; import { GfCreateOrUpdateActivityDialogComponent } from '../create-or-update-activity-dialog/create-or-update-activity-dialog.component'; import { CreateOrUpdateActivityDialogParams } from '../create-or-update-activity-dialog/interfaces/interfaces'; @@ -31,29 +39,45 @@ import { ActivityDialogMode } from './types/activity-dialog-mode.type'; export class GfActivityDialogHostComponent implements OnDestroy, OnInit { private dialogRef: MatDialogRef; + private readonly dialogClosed = new Subject(); + private readonly dataService = inject(DataService); private readonly destroyRef = inject(DestroyRef); private readonly deviceDetectorService = inject(DeviceDetectorService); private readonly dialog = inject(MatDialog); + private readonly impersonationStorageService = inject( + ImpersonationStorageService + ); private readonly route = inject(ActivatedRoute); private readonly router = inject(Router); private readonly userService = inject(UserService); public ngOnInit() { const mode = this.route.snapshot.data.mode as ActivityDialogMode; - const activityId = this.route.snapshot.paramMap.get('activityId'); - - const activity$: Observable = activityId - ? this.dataService.fetchActivity(activityId) - : of(undefined); - this.userService - .get() + // The router reuses this component when only the activity id changes, so + // the parameters are observed instead of read from the snapshot once + this.route.paramMap .pipe( - switchMap((user) => { - return activity$.pipe( - map((activity) => { - return { activity, user }; + map((paramMap) => { + return paramMap.get('activityId'); + }), + distinctUntilChanged(), + tap(() => { + this.closeDialog(); + }), + switchMap((activityId) => { + const activity$: Observable = activityId + ? this.dataService.fetchActivity(activityId) + : of(undefined); + + return this.userService.get().pipe( + switchMap((user) => { + return activity$.pipe( + map((activity) => { + return { activity, user }; + }) + ); }) ); }), @@ -65,7 +89,11 @@ export class GfActivityDialogHostComponent implements OnDestroy, OnInit { }, next: ({ activity, user }) => { if (mode === 'update') { - if (!activity) { + if ( + !activity || + !hasPermission(user?.permissions, permissions.updateActivity) || + this.isReadOnlyMode(user) + ) { this.navigateBack(); return; @@ -82,6 +110,16 @@ export class GfActivityDialogHostComponent implements OnDestroy, OnInit { return; } + // Cloning creates a new activity as well + if ( + !hasPermission(user?.permissions, permissions.createActivity) || + this.isReadOnlyMode(user) + ) { + this.navigateBack(); + + return; + } + this.openDialog({ user, activity: { @@ -105,6 +143,25 @@ export class GfActivityDialogHostComponent implements OnDestroy, OnInit { // be closed explicitly when leaving the route (for example via the browser // navigation) this.dialogRef?.close(); + + this.dialogClosed.complete(); + } + + private closeDialog() { + // Tear down the subscription of the dialog which is about to be replaced, + // so that its result is not mistaken for the user closing it + this.dialogClosed.next(); + + this.dialogRef?.close(); + } + + private isReadOnlyMode(user: User) { + // Mirrors the conditions under which the activities page offers the + // creation and the modification of an activity + return ( + !!this.impersonationStorageService.getId() || + !!user?.settings?.isRestrictedView + ); } private navigateBack() { @@ -124,7 +181,7 @@ export class GfActivityDialogHostComponent implements OnDestroy, OnInit { }) { const deviceType = this.deviceDetectorService.getDeviceInfo().deviceType; - this.dialogRef = this.dialog.open< + const dialogRef = this.dialog.open< GfCreateOrUpdateActivityDialogComponent, CreateOrUpdateActivityDialogParams >(GfCreateOrUpdateActivityDialogComponent, { @@ -137,9 +194,11 @@ export class GfActivityDialogHostComponent implements OnDestroy, OnInit { width: deviceType === 'mobile' ? '100vw' : '50rem' }); - this.dialogRef + this.dialogRef = dialogRef; + + dialogRef .afterClosed() - .pipe(takeUntilDestroyed(this.destroyRef)) + .pipe(takeUntil(this.dialogClosed), takeUntilDestroyed(this.destroyRef)) .subscribe((result: CreateOrderDto | UpdateOrderDto | null) => { if (!result) { this.navigateBack();