From 052aa331de8e558655a7d1ab3c2087b99bff4684 Mon Sep 17 00:00:00 2001 From: Thomas Kaul <4159106+dtslvr@users.noreply.github.com> Date: Wed, 15 Jul 2026 23:34:52 +0200 Subject: [PATCH] Migrate clone, create and edit activity dialogs to dedicated routes --- .../account-detail-dialog.component.ts | 25 ---- .../account-detail-dialog.html | 2 - .../holding-detail-dialog.component.ts | 22 --- .../holding-detail-dialog.html | 2 - .../activities/activities-page.component.ts | 141 +----------------- .../portfolio/activities/activities-page.html | 11 +- .../activities/activities-page.routes.ts | 29 ++++ .../activity-dialog-host.component.ts | 127 ++++++++++++++++ .../activities/interfaces/interfaces.ts | 7 - .../interfaces/internal-route.interface.ts | 2 +- libs/common/src/lib/routes/routes.ts | 17 +++ .../activities-table.component.html | 24 ++- .../activities-table.component.ts | 27 ++-- .../src/lib/assistant/assistant.component.ts | 25 ++-- libs/ui/src/lib/fab/fab.component.html | 2 +- libs/ui/src/lib/fab/fab.component.ts | 3 +- 16 files changed, 243 insertions(+), 223 deletions(-) create mode 100644 apps/client/src/app/pages/portfolio/activities/activity-dialog-host/activity-dialog-host.component.ts delete mode 100644 apps/client/src/app/pages/portfolio/activities/interfaces/interfaces.ts diff --git a/apps/client/src/app/components/account-detail-dialog/account-detail-dialog.component.ts b/apps/client/src/app/components/account-detail-dialog/account-detail-dialog.component.ts index 5af8c9d00..fd1965b8b 100644 --- a/apps/client/src/app/components/account-detail-dialog/account-detail-dialog.component.ts +++ b/apps/client/src/app/components/account-detail-dialog/account-detail-dialog.component.ts @@ -15,7 +15,6 @@ import { User } from '@ghostfolio/common/interfaces'; import { hasPermission, permissions } from '@ghostfolio/common/permissions'; -import { internalRoutes } from '@ghostfolio/common/routes/routes'; import { GfAccountBalancesComponent } from '@ghostfolio/ui/account-balances'; import { GfActivitiesTableComponent } from '@ghostfolio/ui/activities-table'; import { GfDialogFooterComponent } from '@ghostfolio/ui/dialog-footer'; @@ -41,7 +40,6 @@ import { PageEvent } from '@angular/material/paginator'; import { Sort, SortDirection } from '@angular/material/sort'; import { MatTableDataSource } from '@angular/material/table'; import { MatTabsModule } from '@angular/material/tabs'; -import { Router } from '@angular/router'; import { IonIcon } from '@ionic/angular/standalone'; import { Big } from 'big.js'; import { format, parseISO } from 'date-fns'; @@ -114,7 +112,6 @@ export class GfAccountDetailDialogComponent implements OnInit { private readonly destroyRef = inject(DestroyRef); private readonly dialogRef = inject>(MatDialogRef); - private readonly router = inject(Router); private readonly userService = inject(UserService); public constructor() { @@ -155,17 +152,6 @@ export class GfAccountDetailDialogComponent implements OnInit { this.fetchActivities(); } - protected onCloneActivity(aActivity: Activity) { - this.router.navigate( - internalRoutes.portfolio.subRoutes.activities.routerLink, - { - queryParams: { activityId: aActivity.id, createDialog: true } - } - ); - - this.dialogRef.close(); - } - protected onClose() { this.dialogRef.close(); } @@ -208,17 +194,6 @@ export class GfAccountDetailDialogComponent implements OnInit { this.fetchActivities(); } - protected onUpdateActivity(aActivity: Activity) { - this.router.navigate( - internalRoutes.portfolio.subRoutes.activities.routerLink, - { - queryParams: { activityId: aActivity.id, editDialog: true } - } - ); - - this.dialogRef.close(); - } - protected showValuesInPercentage() { return ( this.data.hasImpersonationId || this.user?.settings?.isRestrictedView diff --git a/apps/client/src/app/components/account-detail-dialog/account-detail-dialog.html b/apps/client/src/app/components/account-detail-dialog/account-detail-dialog.html index 4b652db96..5c5be6cb7 100644 --- a/apps/client/src/app/components/account-detail-dialog/account-detail-dialog.html +++ b/apps/client/src/app/components/account-detail-dialog/account-detail-dialog.html @@ -132,8 +132,6 @@ [sortColumn]="sortColumn" [sortDirection]="sortDirection" [totalItems]="totalItems" - (activityToClone)="onCloneActivity($event)" - (activityToUpdate)="onUpdateActivity($event)" (export)="onExport()" (pageChanged)="onChangePage($event)" (sortChanged)="onSortChanged($event)" diff --git a/apps/client/src/app/components/holding-detail-dialog/holding-detail-dialog.component.ts b/apps/client/src/app/components/holding-detail-dialog/holding-detail-dialog.component.ts index 52e0a14d4..e3c138d5b 100644 --- a/apps/client/src/app/components/holding-detail-dialog/holding-detail-dialog.component.ts +++ b/apps/client/src/app/components/holding-detail-dialog/holding-detail-dialog.component.ts @@ -589,17 +589,6 @@ export class GfHoldingDetailDialogComponent implements OnInit { this.fetchActivities(); } - protected onCloneActivity(aActivity: Activity) { - this.router.navigate( - internalRoutes.portfolio.subRoutes.activities.routerLink, - { - queryParams: { activityId: aActivity.id, createDialog: true } - } - ); - - this.dialogRef.close(); - } - protected onClose() { this.dialogRef.close(); } @@ -661,17 +650,6 @@ export class GfHoldingDetailDialogComponent implements OnInit { } } - protected onUpdateActivity(aActivity: Activity) { - this.router.navigate( - internalRoutes.portfolio.subRoutes.activities.routerLink, - { - queryParams: { activityId: aActivity.id, editDialog: true } - } - ); - - this.dialogRef.close(); - } - private fetchActivities(filters: Filter[] = this.getActivityFilters()) { this.dataService .fetchActivities({ diff --git a/apps/client/src/app/components/holding-detail-dialog/holding-detail-dialog.html b/apps/client/src/app/components/holding-detail-dialog/holding-detail-dialog.html index f233df8b6..0a8ee6dce 100644 --- a/apps/client/src/app/components/holding-detail-dialog/holding-detail-dialog.html +++ b/apps/client/src/app/components/holding-detail-dialog/holding-detail-dialog.html @@ -374,8 +374,6 @@ [sortDirection]="sortDirection" [sortDisabled]="true" [totalItems]="activitiesCount" - (activityToClone)="onCloneActivity($event)" - (activityToUpdate)="onUpdateActivity($event)" (export)="onExport()" (pageChanged)="onChangePage($event)" /> diff --git a/apps/client/src/app/pages/portfolio/activities/activities-page.component.ts b/apps/client/src/app/pages/portfolio/activities/activities-page.component.ts index 45653ff80..3362dc5d5 100644 --- a/apps/client/src/app/pages/portfolio/activities/activities-page.component.ts +++ b/apps/client/src/app/pages/portfolio/activities/activities-page.component.ts @@ -2,7 +2,6 @@ import { IcsService } from '@ghostfolio/client/services/ics/ics.service'; import { ImpersonationStorageService } from '@ghostfolio/client/services/impersonation-storage.service'; import { UserService } from '@ghostfolio/client/services/user/user.service'; import { DEFAULT_PAGE_SIZE } from '@ghostfolio/common/config'; -import { CreateOrderDto, UpdateOrderDto } from '@ghostfolio/common/dtos'; import { downloadAsFile } from '@ghostfolio/common/helper'; import { Activity, @@ -10,6 +9,7 @@ import { User } from '@ghostfolio/common/interfaces'; import { hasPermission, permissions } from '@ghostfolio/common/permissions'; +import { internalRoutes } from '@ghostfolio/common/routes/routes'; import { DateRange } from '@ghostfolio/common/types'; import { GfActivitiesTableComponent } from '@ghostfolio/ui/activities-table'; import { GfFabComponent } from '@ghostfolio/ui/fab'; @@ -29,17 +29,12 @@ import { PageEvent } from '@angular/material/paginator'; import { MatSnackBarModule } from '@angular/material/snack-bar'; import { Sort, SortDirection } from '@angular/material/sort'; import { MatTableDataSource } from '@angular/material/table'; -import { ActivatedRoute, Router, RouterModule } from '@angular/router'; +import { Router, RouterModule } from '@angular/router'; import { format, parseISO } from 'date-fns'; import { DeviceDetectorService } from 'ngx-device-detector'; -import { of } from 'rxjs'; -import { map, switchMap } 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'; import { GfImportActivitiesDialogComponent } from './import-activities-dialog/import-activities-dialog.component'; import { ImportActivitiesDialogParams } from './import-activities-dialog/interfaces/interfaces'; -import { ActivitiesPageParams } from './interfaces/interfaces'; @Component({ changeDetection: ChangeDetectionStrategy.OnPush, @@ -59,6 +54,7 @@ export class GfActivitiesPageComponent implements OnInit { protected hasImpersonationId: boolean; protected hasPermissionToCreateActivity: boolean; protected hasPermissionToDeleteActivity: boolean; + protected readonly internalRoutes = internalRoutes; protected pageIndex = 0; protected readonly pageSize = DEFAULT_PAGE_SIZE; protected sortColumn = 'date'; @@ -77,37 +73,9 @@ export class GfActivitiesPageComponent implements OnInit { private readonly impersonationStorageService = inject( ImpersonationStorageService ); - private readonly route = inject(ActivatedRoute); private readonly router = inject(Router); private readonly userService = inject(UserService); - public constructor() { - this.route.queryParams - .pipe( - takeUntilDestroyed(this.destroyRef), - switchMap((params: ActivitiesPageParams) => { - if (params.activityId && (params.createDialog || params.editDialog)) { - return this.dataService - .fetchActivity(params.activityId) - .pipe(map((activity) => ({ activity, params }))); - } - - return of({ params, activity: undefined }); - }) - ) - .subscribe(({ activity, params }) => { - if (params.createDialog) { - this.openCreateActivityDialog(activity); - } else if (params.editDialog) { - if (activity) { - this.openUpdateActivityDialog(activity); - } else { - this.router.navigate(['.'], { relativeTo: this.route }); - } - } - }); - } - public ngOnInit() { this.deviceType = this.deviceDetectorService.getDeviceInfo().deviceType; @@ -149,10 +117,6 @@ export class GfActivitiesPageComponent implements OnInit { }); } - protected onCloneActivity(aActivity: Activity) { - this.openCreateActivityDialog(aActivity); - } - protected onDeleteActivities() { this.dataService .deleteActivities({ @@ -308,12 +272,6 @@ export class GfActivitiesPageComponent implements OnInit { this.fetchActivities(); } - protected onUpdateActivity(aActivity: Activity) { - this.router.navigate([], { - queryParams: { activityId: aActivity.id, editDialog: true } - }); - } - private fetchActivities() { // Reset dataSource and totalItems to show loading state this.dataSource = undefined; @@ -343,48 +301,16 @@ export class GfActivitiesPageComponent implements OnInit { this.hasPermissionToCreateActivity && this.user?.activitiesCount === 0 ) { - this.router.navigate([], { queryParams: { createDialog: true } }); + void this.router.navigate( + internalRoutes.portfolio.subRoutes.activities.subRoutes.create + .routerLink + ); } this.changeDetectorRef.markForCheck(); }); } - private openUpdateActivityDialog(aActivity: Activity) { - const dialogRef = this.dialog.open< - GfCreateOrUpdateActivityDialogComponent, - CreateOrUpdateActivityDialogParams - >(GfCreateOrUpdateActivityDialogComponent, { - data: { - activity: aActivity, - accounts: this.user?.accounts, - user: this.user - }, - height: this.deviceType === 'mobile' ? '98vh' : '80vh', - width: this.deviceType === 'mobile' ? '100vw' : '50rem' - }); - - dialogRef - .afterClosed() - .pipe(takeUntilDestroyed(this.destroyRef)) - .subscribe((activity: UpdateOrderDto) => { - if (activity) { - this.dataService - .putActivity(activity) - .pipe(takeUntilDestroyed(this.destroyRef)) - .subscribe({ - next: () => { - this.fetchActivities(); - - this.changeDetectorRef.markForCheck(); - } - }); - } - - this.router.navigate(['.'], { relativeTo: this.route }); - }); - } - private isCalendarYear(dateRange?: DateRange) { if (!dateRange) { return false; @@ -393,59 +319,6 @@ export class GfActivitiesPageComponent implements OnInit { return /^\d{4}$/.test(dateRange); } - private openCreateActivityDialog(aActivity?: Activity) { - this.userService - .get() - .pipe(takeUntilDestroyed(this.destroyRef)) - .subscribe((user) => { - this.updateUser(user); - - const dialogRef = this.dialog.open< - GfCreateOrUpdateActivityDialogComponent, - CreateOrUpdateActivityDialogParams - >(GfCreateOrUpdateActivityDialogComponent, { - data: { - accounts: this.user?.accounts, - activity: { - ...aActivity, - accountId: aActivity?.accountId, - assetProfile: aActivity?.assetProfile ?? null, - date: new Date(), - id: null, - fee: 0, - type: aActivity?.type ?? 'BUY', - unitPrice: null - }, - user: this.user - } satisfies CreateOrUpdateActivityDialogParams, - height: this.deviceType === 'mobile' ? '98vh' : '80vh', - width: this.deviceType === 'mobile' ? '100vw' : '50rem' - }); - - dialogRef - .afterClosed() - .pipe(takeUntilDestroyed(this.destroyRef)) - .subscribe((transaction: CreateOrderDto | null) => { - if (transaction) { - this.dataService.postActivity(transaction).subscribe({ - next: () => { - this.userService - .get(true) - .pipe(takeUntilDestroyed(this.destroyRef)) - .subscribe(); - - this.fetchActivities(); - - this.changeDetectorRef.markForCheck(); - } - }); - } - - this.router.navigate(['.'], { relativeTo: this.route }); - }); - }); - } - private updateUser(aUser: User) { this.user = aUser; diff --git a/apps/client/src/app/pages/portfolio/activities/activities-page.html b/apps/client/src/app/pages/portfolio/activities/activities-page.html index 23e0cef02..c69437742 100644 --- a/apps/client/src/app/pages/portfolio/activities/activities-page.html +++ b/apps/client/src/app/pages/portfolio/activities/activities-page.html @@ -25,8 +25,6 @@ (activitiesDeleted)="onDeleteActivities()" (activityClicked)="onClickActivity($event)" (activityDeleted)="onDeleteActivity($event)" - (activityToClone)="onCloneActivity($event)" - (activityToUpdate)="onUpdateActivity($event)" (export)="onExport()" (exportDrafts)="onExportDrafts($event)" (import)="onImport()" @@ -43,6 +41,13 @@ hasPermissionToCreateActivity && !user.settings.isRestrictedView ) { - + } + + diff --git a/apps/client/src/app/pages/portfolio/activities/activities-page.routes.ts b/apps/client/src/app/pages/portfolio/activities/activities-page.routes.ts index c96c8a558..f21f23ba4 100644 --- a/apps/client/src/app/pages/portfolio/activities/activities-page.routes.ts +++ b/apps/client/src/app/pages/portfolio/activities/activities-page.routes.ts @@ -4,10 +4,39 @@ import { internalRoutes } from '@ghostfolio/common/routes/routes'; import { Routes } from '@angular/router'; import { GfActivitiesPageComponent } from './activities-page.component'; +import { GfActivityDialogHostComponent } from './activity-dialog-host/activity-dialog-host.component'; + +const { clone, create, update } = + internalRoutes.portfolio.subRoutes.activities.subRoutes; export const routes: Routes = [ { canActivate: [AuthGuard], + children: [ + { + component: GfActivityDialogHostComponent, + data: { mode: 'create' }, + path: create.path, + title: create.title + }, + { + children: [ + { + component: GfActivityDialogHostComponent, + data: { mode: 'clone' }, + path: clone.path, + title: clone.title + }, + { + component: GfActivityDialogHostComponent, + data: { mode: 'update' }, + path: update.path, + title: update.title + } + ], + path: ':activityId' + } + ], component: GfActivitiesPageComponent, path: '', title: internalRoutes.portfolio.subRoutes.activities.title 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 new file mode 100644 index 000000000..bde9f6f71 --- /dev/null +++ b/apps/client/src/app/pages/portfolio/activities/activity-dialog-host/activity-dialog-host.component.ts @@ -0,0 +1,127 @@ +import { UserService } from '@ghostfolio/client/services/user/user.service'; +import { CreateOrderDto, UpdateOrderDto } from '@ghostfolio/common/dtos'; +import { Activity, User } from '@ghostfolio/common/interfaces'; +import { internalRoutes } from '@ghostfolio/common/routes/routes'; +import { DataService } from '@ghostfolio/ui/services'; + +import { + ChangeDetectionStrategy, + Component, + DestroyRef, + OnInit, + inject +} from '@angular/core'; +import { takeUntilDestroyed } from '@angular/core/rxjs-interop'; +import { MatDialog } 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 { GfCreateOrUpdateActivityDialogComponent } from '../create-or-update-activity-dialog/create-or-update-activity-dialog.component'; +import { CreateOrUpdateActivityDialogParams } from '../create-or-update-activity-dialog/interfaces/interfaces'; + +type ActivityDialogMode = 'clone' | 'create' | 'update'; + +@Component({ + changeDetection: ChangeDetectionStrategy.OnPush, + selector: 'gf-activity-dialog-host', + template: '' +}) +export class GfActivityDialogHostComponent implements OnInit { + private readonly dataService = inject(DataService); + private readonly destroyRef = inject(DestroyRef); + private readonly deviceDetectorService = inject(DeviceDetectorService); + private readonly dialog = inject(MatDialog); + 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() + .pipe( + switchMap((user) => { + return activity$.pipe(map((activity) => ({ activity, user }))); + }), + takeUntilDestroyed(this.destroyRef) + ) + .subscribe({ + error: () => { + this.navigateBack(); + }, + next: ({ activity, user }) => { + this.openDialog(mode, user, activity); + } + }); + } + + private navigateBack() { + void this.router.navigate( + internalRoutes.portfolio.subRoutes.activities.routerLink + ); + } + + private openDialog( + aMode: ActivityDialogMode, + aUser: User, + aActivity?: Activity + ) { + const deviceType = this.deviceDetectorService.getDeviceInfo().deviceType; + const isUpdate = aMode === 'update'; + + const dialogRef = this.dialog.open< + GfCreateOrUpdateActivityDialogComponent, + CreateOrUpdateActivityDialogParams + >(GfCreateOrUpdateActivityDialogComponent, { + data: { + accounts: aUser?.accounts, + activity: + isUpdate && aActivity + ? aActivity + : { + ...aActivity, + accountId: aActivity?.accountId, + assetProfile: aActivity?.assetProfile ?? null, + date: new Date(), + fee: 0, + id: null, + type: aActivity?.type ?? 'BUY', + unitPrice: null + }, + user: aUser + }, + height: deviceType === 'mobile' ? '98vh' : '80vh', + width: deviceType === 'mobile' ? '100vw' : '50rem' + }); + + dialogRef + .afterClosed() + .pipe(takeUntilDestroyed(this.destroyRef)) + .subscribe((result: CreateOrderDto | UpdateOrderDto | null) => { + if (result) { + const request$: Observable = isUpdate + ? this.dataService.putActivity(result as UpdateOrderDto) + : this.dataService.postActivity(result as CreateOrderDto); + + request$.pipe(takeUntilDestroyed(this.destroyRef)).subscribe({ + next: () => { + this.userService + .get(true) + .pipe(takeUntilDestroyed(this.destroyRef)) + .subscribe(); + } + }); + } + + this.navigateBack(); + }); + } +} diff --git a/apps/client/src/app/pages/portfolio/activities/interfaces/interfaces.ts b/apps/client/src/app/pages/portfolio/activities/interfaces/interfaces.ts deleted file mode 100644 index 51f240cb5..000000000 --- a/apps/client/src/app/pages/portfolio/activities/interfaces/interfaces.ts +++ /dev/null @@ -1,7 +0,0 @@ -import { Params } from '@angular/router'; - -export interface ActivitiesPageParams extends Params { - activityId?: string; - createDialog?: string; - editDialog?: string; -} diff --git a/libs/common/src/lib/routes/interfaces/internal-route.interface.ts b/libs/common/src/lib/routes/interfaces/internal-route.interface.ts index 8240db46a..9ff6b2ea4 100644 --- a/libs/common/src/lib/routes/interfaces/internal-route.interface.ts +++ b/libs/common/src/lib/routes/interfaces/internal-route.interface.ts @@ -3,7 +3,7 @@ import { User } from '@ghostfolio/common/interfaces'; export interface InternalRoute { excludeFromAssistant?: boolean | ((aUser: User) => boolean); path?: string; - routerLink: string[]; + routerLink?: string[]; subRoutes?: Record; title?: string; } diff --git a/libs/common/src/lib/routes/routes.ts b/libs/common/src/lib/routes/routes.ts index 8132520fc..4a2904187 100644 --- a/libs/common/src/lib/routes/routes.ts +++ b/libs/common/src/lib/routes/routes.ts @@ -125,6 +125,23 @@ export const internalRoutes = { activities: { path: 'activities', routerLink: ['/portfolio', 'activities'], + subRoutes: { + clone: { + excludeFromAssistant: true, + path: 'clone', + title: $localize`Clone Activity` + }, + create: { + path: 'create', + routerLink: ['/portfolio', 'activities', 'create'], + title: $localize`Add Activity` + }, + update: { + excludeFromAssistant: true, + path: 'update', + title: $localize`Update Activity` + } + }, title: $localize`Activities` }, allocations: { diff --git a/libs/ui/src/lib/activities-table/activities-table.component.html b/libs/ui/src/lib/activities-table/activities-table.component.html index 172059d1c..7104867f1 100644 --- a/libs/ui/src/lib/activities-table/activities-table.component.html +++ b/libs/ui/src/lib/activities-table/activities-table.component.html @@ -476,18 +476,34 @@ } - - +