From c9b5e24ceb4c690bce22db729523ff86653710d4 Mon Sep 17 00:00:00 2001 From: Thomas Kaul <4159106+dtslvr@users.noreply.github.com> Date: Sun, 6 Sep 2026 20:08:41 +0200 Subject: [PATCH 1/2] Migrate create dialog to dedicated route --- .../home-watchlist.component.ts | 66 ++-------- .../home-watchlist/home-watchlist.html | 8 +- .../home-watchlist/home-watchlist.service.ts | 15 +++ .../watchlist-dialog-host.component.ts | 114 ++++++++++++++++++ .../src/app/pages/home/home-page.routes.ts | 9 ++ libs/common/src/lib/routes/routes.ts | 7 ++ 6 files changed, 161 insertions(+), 58 deletions(-) create mode 100644 apps/client/src/app/components/home-watchlist/home-watchlist.service.ts create mode 100644 apps/client/src/app/components/home-watchlist/watchlist-dialog-host/watchlist-dialog-host.component.ts 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` } }, From 9ef09d2e54d3be8f1452387b0bbd0a7347f71b96 Mon Sep 17 00:00:00 2001 From: Thomas Kaul <4159106+dtslvr@users.noreply.github.com> Date: Sun, 6 Sep 2026 20:09:05 +0200 Subject: [PATCH 2/2] Update changelog --- CHANGELOG.md | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index ed9b8f191..49e456f91 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,12 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). +## Unreleased + +### Changed + +- Migrated the create dialog of the watchlist to a dedicated route + ## 3.68.0 - 2026-09-06 ### Added