diff --git a/CHANGELOG.md b/CHANGELOG.md index 27e4f92d2..b63fcd03c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,6 +9,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Changed +- Migrated the clone, create and edit activity dialogs to dedicated routes - Improved the language localization in the historical market data table of the admin control panel ### Fixed diff --git a/apps/client/src/app/app.component.ts b/apps/client/src/app/app.component.ts index 90ff2f1bc..ff67daf5e 100644 --- a/apps/client/src/app/app.component.ts +++ b/apps/client/src/app/app.component.ts @@ -40,6 +40,7 @@ import { filter } from 'rxjs/operators'; import { GfFooterComponent } from './components/footer/footer.component'; import { GfHeaderComponent } from './components/header/header.component'; import { GfHoldingDetailDialogComponent } from './components/holding-detail-dialog/holding-detail-dialog.component'; +import { HoldingDetailDialogResult } from './components/holding-detail-dialog/interfaces/interfaces'; import { GfAppQueryParams } from './interfaces/interfaces'; import { ImpersonationStorageService } from './services/impersonation-storage.service'; import { UserService } from './services/user/user.service'; @@ -319,7 +320,11 @@ export class GfAppComponent implements OnInit { dialogRef .afterClosed() .pipe(takeUntilDestroyed(this.destroyRef)) - .subscribe(() => { + .subscribe((result: HoldingDetailDialogResult) => { + if (result?.isNavigating) { + return; + } + void this.router.navigate([], { queryParams: { dataSource: null, 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..62b87cfc0 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,7 @@ 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 { NavigationStart, Router } from '@angular/router'; import { IonIcon } from '@ionic/angular/standalone'; import { Big } from 'big.js'; import { format, parseISO } from 'date-fns'; @@ -53,9 +52,12 @@ import { } from 'ionicons/icons'; import { isNumber } from 'lodash'; import { NgxSkeletonLoaderModule } from 'ngx-skeleton-loader'; -import { forkJoin } from 'rxjs'; +import { filter, forkJoin } from 'rxjs'; -import { AccountDetailDialogParams } from './interfaces/interfaces'; +import { + AccountDetailDialogParams, + AccountDetailDialogResult +} from './interfaces/interfaces'; @Component({ changeDetection: ChangeDetectionStrategy.OnPush, @@ -113,11 +115,24 @@ export class GfAccountDetailDialogComponent implements OnInit { private readonly dataService = inject(DataService); private readonly destroyRef = inject(DestroyRef); private readonly dialogRef = - inject>(MatDialogRef); + inject< + MatDialogRef + >(MatDialogRef); private readonly router = inject(Router); private readonly userService = inject(UserService); public constructor() { + this.router.events + .pipe( + filter((event) => { + return event instanceof NavigationStart; + }), + takeUntilDestroyed(this.destroyRef) + ) + .subscribe(() => { + this.dialogRef.close({ isNavigating: true }); + }); + this.userService.stateChanged .pipe(takeUntilDestroyed(this.destroyRef)) .subscribe((state) => { @@ -155,17 +170,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 +212,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/account-detail-dialog/interfaces/interfaces.ts b/apps/client/src/app/components/account-detail-dialog/interfaces/interfaces.ts index 01c84e956..2f80dac36 100644 --- a/apps/client/src/app/components/account-detail-dialog/interfaces/interfaces.ts +++ b/apps/client/src/app/components/account-detail-dialog/interfaces/interfaces.ts @@ -4,3 +4,7 @@ export interface AccountDetailDialogParams { hasImpersonationId: boolean; hasPermissionToCreateActivity: boolean; } + +export interface AccountDetailDialogResult { + isNavigating?: boolean; +} 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..944b1cb1d 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 @@ -63,7 +63,7 @@ import { PageEvent } from '@angular/material/paginator'; import { SortDirection } from '@angular/material/sort'; import { MatTableDataSource } from '@angular/material/table'; import { MatTabsModule } from '@angular/material/tabs'; -import { Router, RouterModule } from '@angular/router'; +import { NavigationStart, Router, RouterModule } from '@angular/router'; import { IonIcon } from '@ionic/angular/standalone'; import { Account, MarketData, Tag } from '@prisma/client'; import { isUUID } from 'class-validator'; @@ -80,9 +80,12 @@ import { } from 'ionicons/icons'; import { isNumber, round, uniqBy } from 'lodash'; import { NgxSkeletonLoaderModule } from 'ngx-skeleton-loader'; -import { switchMap } from 'rxjs/operators'; +import { filter, switchMap } from 'rxjs/operators'; -import { HoldingDetailDialogParams } from './interfaces/interfaces'; +import { + HoldingDetailDialogParams, + HoldingDetailDialogResult +} from './interfaces/interfaces'; @Component({ changeDetection: ChangeDetectionStrategy.OnPush, @@ -186,9 +189,10 @@ export class GfHoldingDetailDialogComponent implements OnInit { protected value: number; protected readonly data = inject(MAT_DIALOG_DATA); - protected readonly dialogRef = inject( - MatDialogRef - ); + protected readonly dialogRef = + inject< + MatDialogRef + >(MatDialogRef); private tags: Tag[]; @@ -200,6 +204,17 @@ export class GfHoldingDetailDialogComponent implements OnInit { private readonly userService = inject(UserService); public constructor() { + this.router.events + .pipe( + filter((event) => { + return event instanceof NavigationStart; + }), + takeUntilDestroyed(this.destroyRef) + ) + .subscribe(() => { + this.dialogRef.close({ isNavigating: true }); + }); + addIcons({ arrowDownCircleOutline, createOutline, @@ -589,17 +604,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 +665,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/components/holding-detail-dialog/interfaces/interfaces.ts b/apps/client/src/app/components/holding-detail-dialog/interfaces/interfaces.ts index 527b13636..40c94ca60 100644 --- a/apps/client/src/app/components/holding-detail-dialog/interfaces/interfaces.ts +++ b/apps/client/src/app/components/holding-detail-dialog/interfaces/interfaces.ts @@ -15,3 +15,7 @@ export interface HoldingDetailDialogParams { locale: string; symbol: string; } + +export interface HoldingDetailDialogResult { + isNavigating?: boolean; +} diff --git a/apps/client/src/app/pages/accounts/accounts-page.component.ts b/apps/client/src/app/pages/accounts/accounts-page.component.ts index 7d5e2fff7..1cf0e44a7 100644 --- a/apps/client/src/app/pages/accounts/accounts-page.component.ts +++ b/apps/client/src/app/pages/accounts/accounts-page.component.ts @@ -1,5 +1,8 @@ import { GfAccountDetailDialogComponent } from '@ghostfolio/client/components/account-detail-dialog/account-detail-dialog.component'; -import { AccountDetailDialogParams } from '@ghostfolio/client/components/account-detail-dialog/interfaces/interfaces'; +import { + AccountDetailDialogParams, + AccountDetailDialogResult +} from '@ghostfolio/client/components/account-detail-dialog/interfaces/interfaces'; import { ImpersonationStorageService } from '@ghostfolio/client/services/impersonation-storage.service'; import { UserService } from '@ghostfolio/client/services/user/user.service'; import { @@ -243,7 +246,8 @@ export class GfAccountsPageComponent implements OnInit { private openAccountDetailDialog(aAccountId: string) { const dialogRef = this.dialog.open< GfAccountDetailDialogComponent, - AccountDetailDialogParams + AccountDetailDialogParams, + AccountDetailDialogResult >(GfAccountDetailDialogComponent, { autoFocus: false, data: { @@ -262,7 +266,11 @@ export class GfAccountsPageComponent implements OnInit { dialogRef .afterClosed() .pipe(takeUntilDestroyed(this.destroyRef)) - .subscribe(() => { + .subscribe((result) => { + if (result?.isNavigating) { + return; + } + this.fetchAccounts(); this.router.navigate(['.'], { relativeTo: this.route }); 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..e0e3fc950 --- /dev/null +++ b/apps/client/src/app/pages/portfolio/activities/activity-dialog-host/activity-dialog-host.component.ts @@ -0,0 +1,169 @@ +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, + OnDestroy, + OnInit, + inject +} from '@angular/core'; +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 { GfCreateOrUpdateActivityDialogComponent } from '../create-or-update-activity-dialog/create-or-update-activity-dialog.component'; +import { CreateOrUpdateActivityDialogParams } from '../create-or-update-activity-dialog/interfaces/interfaces'; +import { ActivityDialogMode } from './types/activity-dialog-mode.type'; + +@Component({ + changeDetection: ChangeDetectionStrategy.OnPush, + selector: 'gf-activity-dialog-host', + template: '' +}) +export class GfActivityDialogHostComponent implements OnDestroy, OnInit { + private dialogRef: MatDialogRef; + + 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) => { + return { activity, user }; + }) + ); + }), + takeUntilDestroyed(this.destroyRef) + ) + .subscribe({ + error: () => { + this.navigateBack(); + }, + next: ({ activity, user }) => { + if (mode === 'update') { + if (!activity) { + this.navigateBack(); + + return; + } + + this.openDialog({ activity, user, isUpdate: true }); + + return; + } + + if (mode === 'clone' && !activity) { + this.navigateBack(); + + return; + } + + this.openDialog({ + user, + activity: { + ...activity, + accountId: activity?.accountId, + assetProfile: activity?.assetProfile ?? null, + date: new Date(), + fee: 0, + id: null, + type: activity?.type ?? 'BUY', + unitPrice: null + }, + isUpdate: false + }); + } + }); + } + + public ngOnDestroy() { + // The dialog lives in an overlay outside of this component, so it needs to + // be closed explicitly when leaving the route (for example via the browser + // navigation) + this.dialogRef?.close(); + } + + private navigateBack() { + void this.router.navigate( + internalRoutes.portfolio.subRoutes.activities.routerLink + ); + } + + private openDialog({ + activity, + isUpdate, + user + }: { + activity: CreateOrUpdateActivityDialogParams['activity']; + isUpdate: boolean; + user: User; + }) { + const deviceType = this.deviceDetectorService.getDeviceInfo().deviceType; + + this.dialogRef = this.dialog.open< + GfCreateOrUpdateActivityDialogComponent, + CreateOrUpdateActivityDialogParams + >(GfCreateOrUpdateActivityDialogComponent, { + data: { + activity, + user, + accounts: user?.accounts + }, + height: deviceType === 'mobile' ? '98vh' : '80vh', + width: deviceType === 'mobile' ? '100vw' : '50rem' + }); + + this.dialogRef + .afterClosed() + .pipe(takeUntilDestroyed(this.destroyRef)) + .subscribe((result: CreateOrderDto | UpdateOrderDto | null) => { + if (!result) { + this.navigateBack(); + + return; + } + + const request$: Observable = isUpdate + ? this.dataService.putActivity(result as UpdateOrderDto) + : this.dataService.postActivity(result as CreateOrderDto); + + request$.pipe(takeUntilDestroyed(this.destroyRef)).subscribe({ + error: () => { + this.navigateBack(); + }, + next: () => { + // Deliberately not bound to the destroy reference: navigating back + // destroys this component and the refreshed user is what makes the + // activities page reload its data + this.userService.get(true).subscribe(); + + this.navigateBack(); + } + }); + }); + } +} diff --git a/apps/client/src/app/pages/portfolio/activities/activity-dialog-host/types/activity-dialog-mode.type.ts b/apps/client/src/app/pages/portfolio/activities/activity-dialog-host/types/activity-dialog-mode.type.ts new file mode 100644 index 000000000..03d6305a5 --- /dev/null +++ b/apps/client/src/app/pages/portfolio/activities/activity-dialog-host/types/activity-dialog-mode.type.ts @@ -0,0 +1 @@ +export type ActivityDialogMode = 'clone' | 'create' | 'update'; 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/apps/client/src/app/pages/portfolio/allocations/allocations-page.component.ts b/apps/client/src/app/pages/portfolio/allocations/allocations-page.component.ts index f1dfed942..52f051e77 100644 --- a/apps/client/src/app/pages/portfolio/allocations/allocations-page.component.ts +++ b/apps/client/src/app/pages/portfolio/allocations/allocations-page.component.ts @@ -1,5 +1,8 @@ import { GfAccountDetailDialogComponent } from '@ghostfolio/client/components/account-detail-dialog/account-detail-dialog.component'; -import { AccountDetailDialogParams } from '@ghostfolio/client/components/account-detail-dialog/interfaces/interfaces'; +import { + AccountDetailDialogParams, + AccountDetailDialogResult +} from '@ghostfolio/client/components/account-detail-dialog/interfaces/interfaces'; import { ImpersonationStorageService } from '@ghostfolio/client/services/impersonation-storage.service'; import { UserService } from '@ghostfolio/client/services/user/user.service'; import { MAX_TOP_HOLDINGS, UNKNOWN_KEY } from '@ghostfolio/common/config'; @@ -604,7 +607,8 @@ export class GfAllocationsPageComponent implements OnInit { private openAccountDetailDialog(aAccountId: string) { const dialogRef = this.dialog.open< GfAccountDetailDialogComponent, - AccountDetailDialogParams + AccountDetailDialogParams, + AccountDetailDialogResult >(GfAccountDetailDialogComponent, { autoFocus: false, data: { @@ -623,7 +627,11 @@ export class GfAllocationsPageComponent implements OnInit { dialogRef .afterClosed() .pipe(takeUntilDestroyed(this.destroyRef)) - .subscribe(() => { + .subscribe((result) => { + if (result?.isNavigating) { + return; + } + void this.router.navigate(['.'], { relativeTo: this.route }); }); } 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..14538bb8e 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[] | ((...aParams: string[]) => 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..86cb2480b 100644 --- a/libs/common/src/lib/routes/routes.ts +++ b/libs/common/src/lib/routes/routes.ts @@ -125,6 +125,27 @@ export const internalRoutes = { activities: { path: 'activities', routerLink: ['/portfolio', 'activities'], + subRoutes: { + clone: { + path: 'clone', + routerLink: (aActivityId: string) => { + return ['/portfolio', 'activities', aActivityId, 'clone']; + }, + title: $localize`Clone Activity` + }, + create: { + path: 'create', + routerLink: ['/portfolio', 'activities', 'create'], + title: $localize`Add Activity` + }, + update: { + path: 'update', + routerLink: (aActivityId: string) => { + return ['/portfolio', 'activities', aActivityId, '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..2266e758b 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,24 @@ } - - +