Browse Source

Migrate clone, create and edit activity dialogs to dedicated routes

pull/7343/head
Thomas Kaul 5 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. 119
      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. 28
      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 { 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,

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

4
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;
}

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 { 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<HoldingDetailDialogParams>(MAT_DIALOG_DATA);
protected readonly dialogRef = inject(
MatDialogRef<GfHoldingDetailDialogComponent>
);
protected readonly dialogRef =
inject<
MatDialogRef<GfHoldingDetailDialogComponent, HoldingDetailDialogResult>
>(MatDialogRef);
private tags: Tag[];
@ -208,7 +212,7 @@ export class GfHoldingDetailDialogComponent implements OnInit {
takeUntilDestroyed(this.destroyRef)
)
.subscribe(() => {
this.dialogRef.close();
this.dialogRef.close({ isNavigating: true });
});
addIcons({

4
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;
}

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 { 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 });

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

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

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 { 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 });
});
}

2
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<string, InternalRoute>;
title?: string;
}

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

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

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

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

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

@ -151,8 +151,24 @@ export class GfActivitiesTableComponent implements AfterViewInit, OnInit {
public readonly showCheckbox = input(false);
public readonly showNameColumn = input(true);
protected readonly activityDialogRoutes =
internalRoutes.portfolio.subRoutes.activities.subRoutes;
protected readonly activityDialogRouterLinks = computed(() => {
const { clone, update } =
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(() => {
let columns = [
@ -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) {
return (
(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;
}, [])
.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, {

Loading…
Cancel
Save