diff --git a/CHANGELOG.md b/CHANGELOG.md
index f363f4cc5..9bac4d2e1 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -10,6 +10,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
### Changed
- Improved the loading state of the activity count in the portfolio summary
+- Migrated the create dialog of the watchlist to a dedicated route
- Changed the holdings endpoint to return active and closed holdings by default and reuse a single snapshot for both types
- Upgraded `bull-board` from version `9.0.1` to `9.5.0`
- Upgraded `zod` from version `4.4.3` to `4.5.4`
diff --git a/apps/client/src/app/components/home-watchlist/home-watchlist.component.ts b/apps/client/src/app/components/home-watchlist/home-watchlist.component.ts
index d3a7fb203..b56d8d66e 100644
--- a/apps/client/src/app/components/home-watchlist/home-watchlist.component.ts
+++ b/apps/client/src/app/components/home-watchlist/home-watchlist.component.ts
@@ -6,6 +6,7 @@ import {
User
} from '@ghostfolio/common/interfaces';
import { hasPermission, permissions } from '@ghostfolio/common/permissions';
+import { internalRoutes } from '@ghostfolio/common/routes/routes';
import { hasScope, scopes } from '@ghostfolio/common/scopes';
import { GfBenchmarkComponent } from '@ghostfolio/ui/benchmark';
import { GfFabComponent } from '@ghostfolio/ui/fab';
@@ -23,12 +24,10 @@ import {
OnInit
} from '@angular/core';
import { takeUntilDestroyed } from '@angular/core/rxjs-interop';
-import { MatDialog } from '@angular/material/dialog';
-import { ActivatedRoute, Router, RouterModule } from '@angular/router';
+import { RouterModule } from '@angular/router';
import { DeviceDetectorService } from 'ngx-device-detector';
-import { GfCreateWatchlistItemDialogComponent } from './create-watchlist-item-dialog/create-watchlist-item-dialog.component';
-import { CreateWatchlistItemDialogParams } from './create-watchlist-item-dialog/interfaces/interfaces';
+import { HomeWatchlistService } from './home-watchlist.service';
@Component({
changeDetection: ChangeDetectionStrategy.OnPush,
@@ -38,6 +37,7 @@ import { CreateWatchlistItemDialogParams } from './create-watchlist-item-dialog/
GfPremiumIndicatorComponent,
RouterModule
],
+ providers: [HomeWatchlistService],
schemas: [CUSTOM_ELEMENTS_SCHEMA],
selector: 'gf-home-watchlist',
styleUrls: ['./home-watchlist.scss'],
@@ -45,6 +45,7 @@ import { CreateWatchlistItemDialogParams } from './create-watchlist-item-dialog/
})
export class GfHomeWatchlistComponent implements OnInit {
protected readonly DEFAULT_LOCALE = DEFAULT_LOCALE;
+ protected readonly internalRoutes = internalRoutes;
protected hasPermissionToCreateWatchlistItem: boolean;
protected hasPermissionToDeleteWatchlistItem: boolean;
@@ -59,18 +60,14 @@ export class GfHomeWatchlistComponent 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 homeWatchlistService = inject(HomeWatchlistService);
private readonly userService = inject(UserService);
public constructor() {
- this.route.queryParams
+ this.homeWatchlistService.refresh$
.pipe(takeUntilDestroyed(this.destroyRef))
- .subscribe((params) => {
- if (params['createWatchlistItemDialog']) {
- this.openCreateWatchlistItemDialog();
- }
+ .subscribe(() => {
+ this.loadWatchlistData();
});
this.userService.stateChanged
@@ -130,49 +127,4 @@ export class GfHomeWatchlistComponent implements OnInit {
}
});
}
-
- private openCreateWatchlistItemDialog() {
- this.userService
- .get()
- .pipe(takeUntilDestroyed(this.destroyRef))
- .subscribe((user) => {
- this.user = user;
-
- if (
- !hasPermission(user?.permissions, permissions.createWatchlistItem) ||
- !hasScope(user?.scopes, scopes.watchlistCreate)
- ) {
- this.router.navigate(['.'], { relativeTo: this.route });
-
- return;
- }
-
- const dialogRef = this.dialog.open<
- GfCreateWatchlistItemDialogComponent,
- CreateWatchlistItemDialogParams
- >(GfCreateWatchlistItemDialogComponent, {
- data: {
- deviceType: this.deviceType(),
- locale: this.user?.settings?.locale ?? DEFAULT_LOCALE
- },
- width: this.deviceType() === 'mobile' ? '100vw' : '50rem'
- });
-
- dialogRef
- .afterClosed()
- .pipe(takeUntilDestroyed(this.destroyRef))
- .subscribe(({ dataSource, symbol } = {}) => {
- if (dataSource && symbol) {
- this.dataService
- .postWatchlistItem({ dataSource, symbol })
- .pipe(takeUntilDestroyed(this.destroyRef))
- .subscribe({
- next: () => this.loadWatchlistData()
- });
- }
-
- this.router.navigate(['.'], { relativeTo: this.route });
- });
- });
- }
}
diff --git a/apps/client/src/app/components/home-watchlist/home-watchlist.html b/apps/client/src/app/components/home-watchlist/home-watchlist.html
index fdd894de4..11b2d9c2f 100644
--- a/apps/client/src/app/components/home-watchlist/home-watchlist.html
+++ b/apps/client/src/app/components/home-watchlist/home-watchlist.html
@@ -22,5 +22,11 @@
@if (hasPermissionToCreateWatchlistItem) {
-
+
}
+
+
diff --git a/apps/client/src/app/components/home-watchlist/home-watchlist.service.ts b/apps/client/src/app/components/home-watchlist/home-watchlist.service.ts
new file mode 100644
index 000000000..47eafe605
--- /dev/null
+++ b/apps/client/src/app/components/home-watchlist/home-watchlist.service.ts
@@ -0,0 +1,15 @@
+import { Service } from '@angular/core';
+import { Subject } from 'rxjs';
+
+@Service({ autoProvided: false })
+export class HomeWatchlistService {
+ private readonly refreshSubject = new Subject();
+
+ public get refresh$() {
+ return this.refreshSubject.asObservable();
+ }
+
+ public triggerRefresh() {
+ this.refreshSubject.next();
+ }
+}
diff --git a/apps/client/src/app/components/home-watchlist/watchlist-dialog-host/watchlist-dialog-host.component.ts b/apps/client/src/app/components/home-watchlist/watchlist-dialog-host/watchlist-dialog-host.component.ts
new file mode 100644
index 000000000..9e1887e27
--- /dev/null
+++ b/apps/client/src/app/components/home-watchlist/watchlist-dialog-host/watchlist-dialog-host.component.ts
@@ -0,0 +1,114 @@
+import { UserService } from '@ghostfolio/client/services/user/user.service';
+import { DEFAULT_LOCALE } from '@ghostfolio/common/config';
+import { User } from '@ghostfolio/common/interfaces';
+import { hasPermission, permissions } from '@ghostfolio/common/permissions';
+import { internalRoutes } from '@ghostfolio/common/routes/routes';
+import { hasScope, scopes } from '@ghostfolio/common/scopes';
+import { DataService } from '@ghostfolio/ui/services';
+
+import {
+ ChangeDetectionStrategy,
+ Component,
+ DestroyRef,
+ inject,
+ OnDestroy,
+ OnInit
+} from '@angular/core';
+import { takeUntilDestroyed } from '@angular/core/rxjs-interop';
+import { MatDialog, MatDialogRef } from '@angular/material/dialog';
+import { Router } from '@angular/router';
+import { DeviceDetectorService } from 'ngx-device-detector';
+
+import { GfCreateWatchlistItemDialogComponent } from '../create-watchlist-item-dialog/create-watchlist-item-dialog.component';
+import { CreateWatchlistItemDialogParams } from '../create-watchlist-item-dialog/interfaces/interfaces';
+import { HomeWatchlistService } from '../home-watchlist.service';
+
+@Component({
+ changeDetection: ChangeDetectionStrategy.OnPush,
+ selector: 'gf-watchlist-dialog-host',
+ template: ''
+})
+export class GfWatchlistDialogHostComponent 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 homeWatchlistService = inject(HomeWatchlistService);
+ private readonly router = inject(Router);
+ private readonly userService = inject(UserService);
+
+ public ngOnInit() {
+ this.userService
+ .get()
+ .pipe(takeUntilDestroyed(this.destroyRef))
+ .subscribe((user) => {
+ if (
+ !hasPermission(user?.permissions, permissions.createWatchlistItem) ||
+ !hasScope(user?.scopes, scopes.watchlistCreate)
+ ) {
+ this.navigateBack();
+
+ return;
+ }
+
+ this.openDialog(user);
+ });
+ }
+
+ 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.home.subRoutes.watchlist.routerLink
+ );
+ }
+
+ private openDialog(user: User) {
+ const deviceType = this.deviceDetectorService.getDeviceInfo().deviceType;
+
+ const dialogRef = this.dialog.open<
+ GfCreateWatchlistItemDialogComponent,
+ CreateWatchlistItemDialogParams
+ >(GfCreateWatchlistItemDialogComponent, {
+ data: {
+ deviceType,
+ locale: user?.settings?.locale ?? DEFAULT_LOCALE
+ },
+ width: deviceType === 'mobile' ? '100vw' : '50rem'
+ });
+
+ this.dialogRef = dialogRef;
+
+ dialogRef
+ .afterClosed()
+ .pipe(takeUntilDestroyed(this.destroyRef))
+ .subscribe(({ dataSource, symbol } = {}) => {
+ if (!dataSource || !symbol) {
+ this.navigateBack();
+
+ return;
+ }
+
+ this.dataService
+ .postWatchlistItem({ dataSource, symbol })
+ .pipe(takeUntilDestroyed(this.destroyRef))
+ .subscribe({
+ error: () => {
+ this.navigateBack();
+ },
+ next: () => {
+ this.homeWatchlistService.triggerRefresh();
+
+ this.navigateBack();
+ }
+ });
+ });
+ }
+}
diff --git a/apps/client/src/app/pages/home/home-page.routes.ts b/apps/client/src/app/pages/home/home-page.routes.ts
index cc444c2b0..3e0ce7b26 100644
--- a/apps/client/src/app/pages/home/home-page.routes.ts
+++ b/apps/client/src/app/pages/home/home-page.routes.ts
@@ -2,6 +2,7 @@ import { GfHomeHoldingsComponent } from '@ghostfolio/client/components/home-hold
import { GfHomeOverviewComponent } from '@ghostfolio/client/components/home-overview/home-overview.component';
import { GfHomeSummaryComponent } from '@ghostfolio/client/components/home-summary/home-summary.component';
import { GfHomeWatchlistComponent } from '@ghostfolio/client/components/home-watchlist/home-watchlist.component';
+import { GfWatchlistDialogHostComponent } from '@ghostfolio/client/components/home-watchlist/watchlist-dialog-host/watchlist-dialog-host.component';
import { GfMarketsComponent } from '@ghostfolio/client/components/markets/markets.component';
import { AuthGuard } from '@ghostfolio/client/core/auth.guard';
import { internalRoutes } from '@ghostfolio/common/routes/routes';
@@ -34,6 +35,14 @@ export const routes: Routes = [
title: internalRoutes.home.subRoutes.markets.title
},
{
+ children: [
+ {
+ component: GfWatchlistDialogHostComponent,
+ path: internalRoutes.home.subRoutes.watchlist.subRoutes.create.path,
+ title:
+ internalRoutes.home.subRoutes.watchlist.subRoutes.create.title
+ }
+ ],
path: internalRoutes.home.subRoutes.watchlist.path,
component: GfHomeWatchlistComponent,
title: internalRoutes.home.subRoutes.watchlist.title
diff --git a/libs/common/src/lib/routes/routes.ts b/libs/common/src/lib/routes/routes.ts
index b45715607..ca3cdd366 100644
--- a/libs/common/src/lib/routes/routes.ts
+++ b/libs/common/src/lib/routes/routes.ts
@@ -156,6 +156,13 @@ export const internalRoutes = {
watchlist: {
path: 'watchlist',
routerLink: ['/home', 'watchlist'],
+ subRoutes: {
+ create: {
+ path: 'create',
+ routerLink: ['/home', 'watchlist', 'create'],
+ title: $localize`Add to Watchlist`
+ }
+ },
title: $localize`Watchlist`
}
},