Browse Source

Migrate clone, create and edit activity dialogs to dedicated routes

pull/7343/head
Thomas Kaul 6 days ago
parent
commit
b67cbf92c1
  1. 7
      apps/client/src/app/app.component.ts
  2. 11
      apps/client/src/app/components/account-detail-dialog/account-detail-dialog.component.ts
  3. 4
      apps/client/src/app/components/account-detail-dialog/interfaces/interfaces.ts
  4. 14
      apps/client/src/app/components/holding-detail-dialog/holding-detail-dialog.component.ts
  5. 4
      apps/client/src/app/components/holding-detail-dialog/interfaces/interfaces.ts
  6. 14
      apps/client/src/app/pages/accounts/accounts-page.component.ts
  7. 109
      apps/client/src/app/pages/portfolio/activities/activity-dialog-host/activity-dialog-host.component.ts
  8. 1
      apps/client/src/app/pages/portfolio/activities/activity-dialog-host/types/activity-dialog-mode.type.ts
  9. 14
      apps/client/src/app/pages/portfolio/allocations/allocations-page.component.ts
  10. 2
      libs/common/src/lib/routes/interfaces/internal-route.interface.ts
  11. 6
      libs/common/src/lib/routes/routes.ts
  12. 14
      libs/ui/src/lib/activities-table/activities-table.component.html
  13. 26
      libs/ui/src/lib/activities-table/activities-table.component.ts
  14. 4
      libs/ui/src/lib/assistant/assistant.component.ts

7
apps/client/src/app/app.component.ts

@ -40,6 +40,7 @@ import { filter } from 'rxjs/operators';
import { GfFooterComponent } from './components/footer/footer.component'; import { GfFooterComponent } from './components/footer/footer.component';
import { GfHeaderComponent } from './components/header/header.component'; import { GfHeaderComponent } from './components/header/header.component';
import { GfHoldingDetailDialogComponent } from './components/holding-detail-dialog/holding-detail-dialog.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 { GfAppQueryParams } from './interfaces/interfaces';
import { ImpersonationStorageService } from './services/impersonation-storage.service'; import { ImpersonationStorageService } from './services/impersonation-storage.service';
import { UserService } from './services/user/user.service'; import { UserService } from './services/user/user.service';
@ -319,7 +320,11 @@ export class GfAppComponent implements OnInit {
dialogRef dialogRef
.afterClosed() .afterClosed()
.pipe(takeUntilDestroyed(this.destroyRef)) .pipe(takeUntilDestroyed(this.destroyRef))
.subscribe(() => { .subscribe((result: HoldingDetailDialogResult) => {
if (result?.isNavigating) {
return;
}
void this.router.navigate([], { void this.router.navigate([], {
queryParams: { queryParams: {
dataSource: null, dataSource: null,

11
apps/client/src/app/components/account-detail-dialog/account-detail-dialog.component.ts

@ -54,7 +54,10 @@ import { isNumber } from 'lodash';
import { NgxSkeletonLoaderModule } from 'ngx-skeleton-loader'; import { NgxSkeletonLoaderModule } from 'ngx-skeleton-loader';
import { filter, forkJoin } from 'rxjs'; import { filter, forkJoin } from 'rxjs';
import { AccountDetailDialogParams } from './interfaces/interfaces'; import {
AccountDetailDialogParams,
AccountDetailDialogResult
} from './interfaces/interfaces';
@Component({ @Component({
changeDetection: ChangeDetectionStrategy.OnPush, changeDetection: ChangeDetectionStrategy.OnPush,
@ -112,7 +115,9 @@ export class GfAccountDetailDialogComponent implements OnInit {
private readonly dataService = inject(DataService); private readonly dataService = inject(DataService);
private readonly destroyRef = inject(DestroyRef); private readonly destroyRef = inject(DestroyRef);
private readonly dialogRef = private readonly dialogRef =
inject<MatDialogRef<GfAccountDetailDialogComponent>>(MatDialogRef); inject<
MatDialogRef<GfAccountDetailDialogComponent, AccountDetailDialogResult>
>(MatDialogRef);
private readonly router = inject(Router); private readonly router = inject(Router);
private readonly userService = inject(UserService); private readonly userService = inject(UserService);
@ -125,7 +130,7 @@ export class GfAccountDetailDialogComponent implements OnInit {
takeUntilDestroyed(this.destroyRef) takeUntilDestroyed(this.destroyRef)
) )
.subscribe(() => { .subscribe(() => {
this.dialogRef.close(); this.dialogRef.close({ isNavigating: true });
}); });
this.userService.stateChanged this.userService.stateChanged

4
apps/client/src/app/components/account-detail-dialog/interfaces/interfaces.ts

@ -4,3 +4,7 @@ export interface AccountDetailDialogParams {
hasImpersonationId: boolean; hasImpersonationId: boolean;
hasPermissionToCreateActivity: boolean; hasPermissionToCreateActivity: boolean;
} }
export interface AccountDetailDialogResult {
isNavigating?: boolean;
}

14
apps/client/src/app/components/holding-detail-dialog/holding-detail-dialog.component.ts

@ -82,7 +82,10 @@ import { isNumber, round, uniqBy } from 'lodash';
import { NgxSkeletonLoaderModule } from 'ngx-skeleton-loader'; import { NgxSkeletonLoaderModule } from 'ngx-skeleton-loader';
import { filter, switchMap } from 'rxjs/operators'; import { filter, switchMap } from 'rxjs/operators';
import { HoldingDetailDialogParams } from './interfaces/interfaces'; import {
HoldingDetailDialogParams,
HoldingDetailDialogResult
} from './interfaces/interfaces';
@Component({ @Component({
changeDetection: ChangeDetectionStrategy.OnPush, changeDetection: ChangeDetectionStrategy.OnPush,
@ -186,9 +189,10 @@ export class GfHoldingDetailDialogComponent implements OnInit {
protected value: number; protected value: number;
protected readonly data = inject<HoldingDetailDialogParams>(MAT_DIALOG_DATA); protected readonly data = inject<HoldingDetailDialogParams>(MAT_DIALOG_DATA);
protected readonly dialogRef = inject( protected readonly dialogRef =
MatDialogRef<GfHoldingDetailDialogComponent> inject<
); MatDialogRef<GfHoldingDetailDialogComponent, HoldingDetailDialogResult>
>(MatDialogRef);
private tags: Tag[]; private tags: Tag[];
@ -208,7 +212,7 @@ export class GfHoldingDetailDialogComponent implements OnInit {
takeUntilDestroyed(this.destroyRef) takeUntilDestroyed(this.destroyRef)
) )
.subscribe(() => { .subscribe(() => {
this.dialogRef.close(); this.dialogRef.close({ isNavigating: true });
}); });
addIcons({ addIcons({

4
apps/client/src/app/components/holding-detail-dialog/interfaces/interfaces.ts

@ -15,3 +15,7 @@ export interface HoldingDetailDialogParams {
locale: string; locale: string;
symbol: string; symbol: string;
} }
export interface HoldingDetailDialogResult {
isNavigating?: boolean;
}

14
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 { 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 { ImpersonationStorageService } from '@ghostfolio/client/services/impersonation-storage.service';
import { UserService } from '@ghostfolio/client/services/user/user.service'; import { UserService } from '@ghostfolio/client/services/user/user.service';
import { import {
@ -243,7 +246,8 @@ export class GfAccountsPageComponent implements OnInit {
private openAccountDetailDialog(aAccountId: string) { private openAccountDetailDialog(aAccountId: string) {
const dialogRef = this.dialog.open< const dialogRef = this.dialog.open<
GfAccountDetailDialogComponent, GfAccountDetailDialogComponent,
AccountDetailDialogParams AccountDetailDialogParams,
AccountDetailDialogResult
>(GfAccountDetailDialogComponent, { >(GfAccountDetailDialogComponent, {
autoFocus: false, autoFocus: false,
data: { data: {
@ -262,7 +266,11 @@ export class GfAccountsPageComponent implements OnInit {
dialogRef dialogRef
.afterClosed() .afterClosed()
.pipe(takeUntilDestroyed(this.destroyRef)) .pipe(takeUntilDestroyed(this.destroyRef))
.subscribe(() => { .subscribe((result) => {
if (result?.isNavigating) {
return;
}
this.fetchAccounts(); this.fetchAccounts();
this.router.navigate(['.'], { relativeTo: this.route }); this.router.navigate(['.'], { relativeTo: this.route });

109
apps/client/src/app/pages/portfolio/activities/activity-dialog-host/activity-dialog-host.component.ts

@ -8,27 +8,29 @@ import {
ChangeDetectionStrategy, ChangeDetectionStrategy,
Component, Component,
DestroyRef, DestroyRef,
OnDestroy,
OnInit, OnInit,
inject inject
} from '@angular/core'; } from '@angular/core';
import { takeUntilDestroyed } from '@angular/core/rxjs-interop'; import { takeUntilDestroyed } from '@angular/core/rxjs-interop';
import { MatDialog } from '@angular/material/dialog'; import { MatDialog, MatDialogRef } from '@angular/material/dialog';
import { ActivatedRoute, Router } from '@angular/router'; import { ActivatedRoute, Router } from '@angular/router';
import { DeviceDetectorService } from 'ngx-device-detector'; import { DeviceDetectorService } from 'ngx-device-detector';
import { Observable, of } from 'rxjs'; import { Observable, of } from 'rxjs';
import { finalize, map, switchMap } from 'rxjs/operators'; import { map, switchMap } from 'rxjs/operators';
import { GfCreateOrUpdateActivityDialogComponent } from '../create-or-update-activity-dialog/create-or-update-activity-dialog.component'; 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 { CreateOrUpdateActivityDialogParams } from '../create-or-update-activity-dialog/interfaces/interfaces';
import { ActivityDialogMode } from './types/activity-dialog-mode.type';
type ActivityDialogMode = 'clone' | 'create' | 'update';
@Component({ @Component({
changeDetection: ChangeDetectionStrategy.OnPush, changeDetection: ChangeDetectionStrategy.OnPush,
selector: 'gf-activity-dialog-host', selector: 'gf-activity-dialog-host',
template: '' template: ''
}) })
export class GfActivityDialogHostComponent implements OnInit { export class GfActivityDialogHostComponent implements OnDestroy, OnInit {
private dialogRef: MatDialogRef<GfCreateOrUpdateActivityDialogComponent>;
private readonly dataService = inject(DataService); private readonly dataService = inject(DataService);
private readonly destroyRef = inject(DestroyRef); private readonly destroyRef = inject(DestroyRef);
private readonly deviceDetectorService = inject(DeviceDetectorService); private readonly deviceDetectorService = inject(DeviceDetectorService);
@ -49,7 +51,11 @@ export class GfActivityDialogHostComponent implements OnInit {
.get() .get()
.pipe( .pipe(
switchMap((user) => { switchMap((user) => {
return activity$.pipe(map((activity) => ({ activity, user }))); return activity$.pipe(
map((activity) => {
return { activity, user };
})
);
}), }),
takeUntilDestroyed(this.destroyRef) takeUntilDestroyed(this.destroyRef)
) )
@ -58,10 +64,48 @@ export class GfActivityDialogHostComponent implements OnInit {
this.navigateBack(); this.navigateBack();
}, },
next: ({ activity, user }) => { next: ({ activity, user }) => {
this.openDialog(mode, user, activity); 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() { private navigateBack() {
void this.router.navigate( void this.router.navigate(
@ -69,40 +113,31 @@ export class GfActivityDialogHostComponent implements OnInit {
); );
} }
private openDialog( private openDialog({
aMode: ActivityDialogMode, activity,
aUser: User, isUpdate,
aActivity?: Activity user
) { }: {
activity: CreateOrUpdateActivityDialogParams['activity'];
isUpdate: boolean;
user: User;
}) {
const deviceType = this.deviceDetectorService.getDeviceInfo().deviceType; const deviceType = this.deviceDetectorService.getDeviceInfo().deviceType;
const isUpdate = aMode === 'update';
const dialogRef = this.dialog.open< this.dialogRef = this.dialog.open<
GfCreateOrUpdateActivityDialogComponent, GfCreateOrUpdateActivityDialogComponent,
CreateOrUpdateActivityDialogParams CreateOrUpdateActivityDialogParams
>(GfCreateOrUpdateActivityDialogComponent, { >(GfCreateOrUpdateActivityDialogComponent, {
data: { data: {
accounts: aUser?.accounts, activity,
activity: user,
isUpdate && aActivity accounts: user?.accounts
? 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', height: deviceType === 'mobile' ? '98vh' : '80vh',
width: deviceType === 'mobile' ? '100vw' : '50rem' width: deviceType === 'mobile' ? '100vw' : '50rem'
}); });
dialogRef this.dialogRef
.afterClosed() .afterClosed()
.pipe(takeUntilDestroyed(this.destroyRef)) .pipe(takeUntilDestroyed(this.destroyRef))
.subscribe((result: CreateOrderDto | UpdateOrderDto | null) => { .subscribe((result: CreateOrderDto | UpdateOrderDto | null) => {
@ -116,15 +151,17 @@ export class GfActivityDialogHostComponent implements OnInit {
? this.dataService.putActivity(result as UpdateOrderDto) ? this.dataService.putActivity(result as UpdateOrderDto)
: this.dataService.postActivity(result as CreateOrderDto); : this.dataService.postActivity(result as CreateOrderDto);
request$ request$.pipe(takeUntilDestroyed(this.destroyRef)).subscribe({
.pipe( error: () => {
finalize(() => {
this.navigateBack(); this.navigateBack();
}) },
)
.subscribe({
next: () => { 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.userService.get(true).subscribe();
this.navigateBack();
} }
}); });
}); });

1
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';

14
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 { 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 { ImpersonationStorageService } from '@ghostfolio/client/services/impersonation-storage.service';
import { UserService } from '@ghostfolio/client/services/user/user.service'; import { UserService } from '@ghostfolio/client/services/user/user.service';
import { MAX_TOP_HOLDINGS, UNKNOWN_KEY } from '@ghostfolio/common/config'; import { MAX_TOP_HOLDINGS, UNKNOWN_KEY } from '@ghostfolio/common/config';
@ -604,7 +607,8 @@ export class GfAllocationsPageComponent implements OnInit {
private openAccountDetailDialog(aAccountId: string) { private openAccountDetailDialog(aAccountId: string) {
const dialogRef = this.dialog.open< const dialogRef = this.dialog.open<
GfAccountDetailDialogComponent, GfAccountDetailDialogComponent,
AccountDetailDialogParams AccountDetailDialogParams,
AccountDetailDialogResult
>(GfAccountDetailDialogComponent, { >(GfAccountDetailDialogComponent, {
autoFocus: false, autoFocus: false,
data: { data: {
@ -623,7 +627,11 @@ export class GfAllocationsPageComponent implements OnInit {
dialogRef dialogRef
.afterClosed() .afterClosed()
.pipe(takeUntilDestroyed(this.destroyRef)) .pipe(takeUntilDestroyed(this.destroyRef))
.subscribe(() => { .subscribe((result) => {
if (result?.isNavigating) {
return;
}
void this.router.navigate(['.'], { relativeTo: this.route }); void this.router.navigate(['.'], { relativeTo: this.route });
}); });
} }

2
libs/common/src/lib/routes/interfaces/internal-route.interface.ts

@ -3,7 +3,7 @@ import { User } from '@ghostfolio/common/interfaces';
export interface InternalRoute { export interface InternalRoute {
excludeFromAssistant?: boolean | ((aUser: User) => boolean); excludeFromAssistant?: boolean | ((aUser: User) => boolean);
path?: string; path?: string;
routerLink?: string[]; routerLink: string[] | ((...aParams: string[]) => string[]);
subRoutes?: Record<string, InternalRoute>; subRoutes?: Record<string, InternalRoute>;
title?: string; title?: string;
} }

6
libs/common/src/lib/routes/routes.ts

@ -128,6 +128,9 @@ export const internalRoutes = {
subRoutes: { subRoutes: {
clone: { clone: {
path: 'clone', path: 'clone',
routerLink: (aActivityId: string) => {
return ['/portfolio', 'activities', aActivityId, 'clone'];
},
title: $localize`Clone Activity` title: $localize`Clone Activity`
}, },
create: { create: {
@ -137,6 +140,9 @@ export const internalRoutes = {
}, },
update: { update: {
path: 'update', path: 'update',
routerLink: (aActivityId: string) => {
return ['/portfolio', 'activities', aActivityId, 'update'];
},
title: $localize`Update Activity` title: $localize`Update Activity`
} }
}, },

14
libs/ui/src/lib/activities-table/activities-table.component.html

@ -478,12 +478,7 @@
} }
<a <a
mat-menu-item mat-menu-item
[routerLink]=" [routerLink]="activityDialogRouterLinks().get(element.id)?.update"
getActivityDialogRouterLink(
element,
activityDialogRoutes.update.path
)
"
> >
<span class="align-items-center d-flex"> <span class="align-items-center d-flex">
<ion-icon class="mr-2" name="create-outline" /> <ion-icon class="mr-2" name="create-outline" />
@ -492,12 +487,7 @@
</a> </a>
<a <a
mat-menu-item mat-menu-item
[routerLink]=" [routerLink]="activityDialogRouterLinks().get(element.id)?.clone"
getActivityDialogRouterLink(
element,
activityDialogRoutes.clone.path
)
"
> >
<span class="align-items-center d-flex"> <span class="align-items-center d-flex">
<ion-icon class="mr-2" name="copy-outline" /> <ion-icon class="mr-2" name="copy-outline" />

26
libs/ui/src/lib/activities-table/activities-table.component.ts

@ -151,9 +151,25 @@ export class GfActivitiesTableComponent implements AfterViewInit, OnInit {
public readonly showCheckbox = input(false); public readonly showCheckbox = input(false);
public readonly showNameColumn = input(true); public readonly showNameColumn = input(true);
protected readonly activityDialogRoutes = protected readonly activityDialogRouterLinks = computed(() => {
const { clone, update } =
internalRoutes.portfolio.subRoutes.activities.subRoutes; internalRoutes.portfolio.subRoutes.activities.subRoutes;
const routerLinks = new Map<
string,
{ clone: string[]; update: string[] }
>();
for (const { id } of this.dataSource()?.data ?? []) {
routerLinks.set(id, {
clone: clone.routerLink(id),
update: update.routerLink(id)
});
}
return routerLinks;
});
protected readonly displayedColumns = computed(() => { protected readonly displayedColumns = computed(() => {
let columns = [ let columns = [
'select', 'select',
@ -268,14 +284,6 @@ export class GfActivitiesTableComponent implements AfterViewInit, OnInit {
); );
} }
public getActivityDialogRouterLink(aActivity: Activity, aPath: string) {
return [
...internalRoutes.portfolio.subRoutes.activities.routerLink,
aActivity.id,
aPath
];
}
public isExcludedFromAnalysis(activity: Activity) { public isExcludedFromAnalysis(activity: Activity) {
return ( return (
(activity.account && isAccountExcluded(activity.account)) ?? (activity.account && isAccountExcluded(activity.account)) ??

4
libs/ui/src/lib/assistant/assistant.component.ts

@ -708,7 +708,9 @@ export class GfAssistantComponent implements OnChanges, OnDestroy, OnInit {
return acc; return acc;
}, []) }, [])
.filter((route): route is InternalRoute & { routerLink: string[] } => { .filter((route): route is InternalRoute & { routerLink: string[] } => {
return route.routerLink !== undefined; // Routes with a parameterized router link cannot be linked to without
// their parameters and are therefore not offered as quick links
return Array.isArray(route.routerLink);
}); });
const fuse = new Fuse(allRoutes, { const fuse = new Fuse(allRoutes, {

Loading…
Cancel
Save