From 1135a5b335c5c6617847d0500127a4d14e65a9d3 Mon Sep 17 00:00:00 2001 From: Thomas <4159106+dtslvr@users.noreply.github.com> Date: Thu, 8 Jul 2021 21:28:28 +0200 Subject: [PATCH] Fix rendering of currency and platform in dialogs and clean up observables (#202) --- CHANGELOG.md | 6 ++ apps/client/src/app/app.component.ts | 9 +- .../app/components/header/header.component.ts | 41 +++++---- .../performance-chart-dialog.component.ts | 10 ++ .../position-detail-dialog.component.ts | 15 ++- .../components/position/position.component.ts | 9 +- .../positions-table.component.ts | 15 ++- .../transactions-table.component.ts | 35 ++++--- .../app/pages/about/about-page.component.ts | 5 +- .../pages/account/account-page.component.ts | 2 + .../pages/accounts/accounts-page.component.ts | 91 +++++++++++-------- ...eate-or-update-account-dialog.component.ts | 25 +++-- .../app/pages/admin/admin-page.component.ts | 43 +++++---- .../src/app/pages/auth/auth-page.component.ts | 31 +++++-- .../src/app/pages/home/home-page.component.ts | 26 ++++-- .../pages/landing/landing-page.component.ts | 14 ++- .../pages/pricing/pricing-page.component.ts | 4 +- .../pages/register/register-page.component.ts | 16 ++-- .../tools/report/report-page.component.ts | 4 +- ...-or-update-transaction-dialog.component.ts | 19 ++-- .../transactions-page.component.ts | 88 ++++++++++-------- .../pages/webauthn/webauthn-page.component.ts | 47 ++++++---- .../src/app/pages/zen/zen-page.component.ts | 3 + 23 files changed, 362 insertions(+), 196 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 3f19b8fc..142bfc6c 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 + +### Fixed + +- Fixed rendering of currency and platform in dialogs (account and transaction) + ## 1.24.0 - 07.07.2021 ### Added diff --git a/apps/client/src/app/app.component.ts b/apps/client/src/app/app.component.ts index c326e2f0..33b276a9 100644 --- a/apps/client/src/app/app.component.ts +++ b/apps/client/src/app/app.component.ts @@ -52,9 +52,12 @@ export class AppComponent implements OnDestroy, OnInit { public ngOnInit() { this.deviceType = this.deviceService.getDeviceInfo().deviceType; - this.dataService.fetchInfo().subscribe((info) => { - this.info = info; - }); + this.dataService + .fetchInfo() + .pipe(takeUntil(this.unsubscribeSubject)) + .subscribe((info) => { + this.info = info; + }); this.router.events .pipe(filter((event) => event instanceof NavigationEnd)) diff --git a/apps/client/src/app/components/header/header.component.ts b/apps/client/src/app/components/header/header.component.ts index 3d8a9c47..a1e1d09b 100644 --- a/apps/client/src/app/components/header/header.component.ts +++ b/apps/client/src/app/components/header/header.component.ts @@ -51,6 +51,7 @@ export class HeaderComponent implements OnChanges { ) { this.impersonationStorageService .onChangeHasImpersonation() + .pipe(takeUntil(this.unsubscribeSubject)) .subscribe((id) => { this.impersonationId = id; }); @@ -98,23 +99,26 @@ export class HeaderComponent implements OnChanges { width: '30rem' }); - dialogRef.afterClosed().subscribe((data) => { - if (data?.accessToken) { - this.dataService - .loginAnonymous(data?.accessToken) - .pipe( - catchError(() => { - alert('Oops! Incorrect Security Token.'); + dialogRef + .afterClosed() + .pipe(takeUntil(this.unsubscribeSubject)) + .subscribe((data) => { + if (data?.accessToken) { + this.dataService + .loginAnonymous(data?.accessToken) + .pipe( + catchError(() => { + alert('Oops! Incorrect Security Token.'); - return EMPTY; - }), - takeUntil(this.unsubscribeSubject) - ) - .subscribe(({ authToken }) => { - this.setToken(authToken); - }); - } - }); + return EMPTY; + }), + takeUntil(this.unsubscribeSubject) + ) + .subscribe(({ authToken }) => { + this.setToken(authToken); + }); + } + }); } public setToken(aToken: string) { @@ -125,4 +129,9 @@ export class HeaderComponent implements OnChanges { this.router.navigate(['/']); } + + public ngOnDestroy() { + this.unsubscribeSubject.next(); + this.unsubscribeSubject.complete(); + } } diff --git a/apps/client/src/app/components/performance-chart-dialog/performance-chart-dialog.component.ts b/apps/client/src/app/components/performance-chart-dialog/performance-chart-dialog.component.ts index 4086669c..f8adcec1 100644 --- a/apps/client/src/app/components/performance-chart-dialog/performance-chart-dialog.component.ts +++ b/apps/client/src/app/components/performance-chart-dialog/performance-chart-dialog.component.ts @@ -7,6 +7,8 @@ import { import { MAT_DIALOG_DATA, MatDialogRef } from '@angular/material/dialog'; import { DataService } from '@ghostfolio/client/services/data.service'; import { isToday, parse } from 'date-fns'; +import { Subject } from 'rxjs'; +import { takeUntil } from 'rxjs/operators'; import { LineChartItem } from '../line-chart/interfaces/line-chart.interface'; import { PositionDetailDialogParams } from './interfaces/interfaces'; @@ -27,6 +29,8 @@ export class PerformanceChartDialog { public historicalDataItems: LineChartItem[]; public title: string; + private unsubscribeSubject = new Subject(); + public constructor( private changeDetectorRef: ChangeDetectorRef, private dataService: DataService, @@ -35,6 +39,7 @@ export class PerformanceChartDialog { ) { this.dataService .fetchPositionDetail(this.benchmarkSymbol) + .pipe(takeUntil(this.unsubscribeSubject)) .subscribe(({ currency, firstBuyDate, historicalData, marketPrice }) => { this.benchmarkDataItems = []; this.currency = currency; @@ -84,4 +89,9 @@ export class PerformanceChartDialog { public onClose(): void { this.dialogRef.close(); } + + public ngOnDestroy() { + this.unsubscribeSubject.next(); + this.unsubscribeSubject.complete(); + } } diff --git a/apps/client/src/app/components/position/position-detail-dialog/position-detail-dialog.component.ts b/apps/client/src/app/components/position/position-detail-dialog/position-detail-dialog.component.ts index 161d4539..e8ffbfad 100644 --- a/apps/client/src/app/components/position/position-detail-dialog/position-detail-dialog.component.ts +++ b/apps/client/src/app/components/position/position-detail-dialog/position-detail-dialog.component.ts @@ -2,11 +2,14 @@ import { ChangeDetectionStrategy, ChangeDetectorRef, Component, - Inject + Inject, + OnDestroy } from '@angular/core'; import { MAT_DIALOG_DATA, MatDialogRef } from '@angular/material/dialog'; import { DataService } from '@ghostfolio/client/services/data.service'; import { format, isSameMonth, isToday, parseISO } from 'date-fns'; +import { Subject } from 'rxjs'; +import { takeUntil } from 'rxjs/operators'; import { LineChartItem } from '../../line-chart/interfaces/line-chart.interface'; import { PositionDetailDialogParams } from './interfaces/interfaces'; @@ -18,7 +21,7 @@ import { PositionDetailDialogParams } from './interfaces/interfaces'; templateUrl: 'position-detail-dialog.html', styleUrls: ['./position-detail-dialog.component.scss'] }) -export class PositionDetailDialog { +export class PositionDetailDialog implements OnDestroy { public averagePrice: number; public benchmarkDataItems: LineChartItem[]; public currency: string; @@ -33,6 +36,8 @@ export class PositionDetailDialog { public quantity: number; public transactionCount: number; + private unsubscribeSubject = new Subject(); + public constructor( private changeDetectorRef: ChangeDetectorRef, private dataService: DataService, @@ -41,6 +46,7 @@ export class PositionDetailDialog { ) { this.dataService .fetchPositionDetail(data.symbol) + .pipe(takeUntil(this.unsubscribeSubject)) .subscribe( ({ averagePrice, @@ -135,4 +141,9 @@ export class PositionDetailDialog { public onClose(): void { this.dialogRef.close(); } + + public ngOnDestroy() { + this.unsubscribeSubject.next(); + this.unsubscribeSubject.complete(); + } } diff --git a/apps/client/src/app/components/position/position.component.ts b/apps/client/src/app/components/position/position.component.ts index 974101ce..4eba2cca 100644 --- a/apps/client/src/app/components/position/position.component.ts +++ b/apps/client/src/app/components/position/position.component.ts @@ -72,8 +72,11 @@ export class PositionComponent implements OnDestroy, OnInit { width: this.deviceType === 'mobile' ? '100vw' : '50rem' }); - dialogRef.afterClosed().subscribe(() => { - this.router.navigate(['.'], { relativeTo: this.route }); - }); + dialogRef + .afterClosed() + .pipe(takeUntil(this.unsubscribeSubject)) + .subscribe(() => { + this.router.navigate(['.'], { relativeTo: this.route }); + }); } } diff --git a/apps/client/src/app/components/positions-table/positions-table.component.ts b/apps/client/src/app/components/positions-table/positions-table.component.ts index e990366a..22694cef 100644 --- a/apps/client/src/app/components/positions-table/positions-table.component.ts +++ b/apps/client/src/app/components/positions-table/positions-table.component.ts @@ -4,6 +4,7 @@ import { EventEmitter, Input, OnChanges, + OnDestroy, OnInit, Output, ViewChild @@ -26,7 +27,7 @@ import { PositionDetailDialog } from '../position/position-detail-dialog/positio templateUrl: './positions-table.component.html', styleUrls: ['./positions-table.component.scss'] }) -export class PositionsTableComponent implements OnChanges, OnInit { +export class PositionsTableComponent implements OnChanges, OnDestroy, OnInit { @Input() baseCurrency: string; @Input() deviceType: string; @Input() locale: string; @@ -38,7 +39,8 @@ export class PositionsTableComponent implements OnChanges, OnInit { @ViewChild(MatPaginator) paginator: MatPaginator; @ViewChild(MatSort) sort: MatSort; - public dataSource: MatTableDataSource = new MatTableDataSource(); + public dataSource: MatTableDataSource = + new MatTableDataSource(); public displayedColumns = []; public isLoading = true; public pageSize = 7; @@ -133,9 +135,12 @@ export class PositionsTableComponent implements OnChanges, OnInit { width: this.deviceType === 'mobile' ? '100vw' : '50rem' }); - dialogRef.afterClosed().subscribe(() => { - this.router.navigate(['.'], { relativeTo: this.route }); - }); + dialogRef + .afterClosed() + .pipe(takeUntil(this.unsubscribeSubject)) + .subscribe(() => { + this.router.navigate(['.'], { relativeTo: this.route }); + }); } public ngOnDestroy() { diff --git a/apps/client/src/app/components/transactions-table/transactions-table.component.ts b/apps/client/src/app/components/transactions-table/transactions-table.component.ts index ca0b2f85..b65d30e6 100644 --- a/apps/client/src/app/components/transactions-table/transactions-table.component.ts +++ b/apps/client/src/app/components/transactions-table/transactions-table.component.ts @@ -89,18 +89,20 @@ export class TransactionsTableComponent } }); - this.searchControl.valueChanges.subscribe((keyword) => { - if (keyword) { - const filterValue = keyword.toLowerCase(); - this.filters$.next( - this.allFilters.filter( - (filter) => filter.toLowerCase().indexOf(filterValue) === 0 - ) - ); - } else { - this.filters$.next(this.allFilters); - } - }); + this.searchControl.valueChanges + .pipe(takeUntil(this.unsubscribeSubject)) + .subscribe((keyword) => { + if (keyword) { + const filterValue = keyword.toLowerCase(); + this.filters$.next( + this.allFilters.filter( + (filter) => filter.toLowerCase().indexOf(filterValue) === 0 + ) + ); + } else { + this.filters$.next(this.allFilters); + } + }); } public addKeyword({ input, value }: MatChipInputEvent): void { @@ -223,9 +225,12 @@ export class TransactionsTableComponent width: this.deviceType === 'mobile' ? '100vw' : '50rem' }); - dialogRef.afterClosed().subscribe(() => { - this.router.navigate(['.'], { relativeTo: this.route }); - }); + dialogRef + .afterClosed() + .pipe(takeUntil(this.unsubscribeSubject)) + .subscribe(() => { + this.router.navigate(['.'], { relativeTo: this.route }); + }); } public ngOnDestroy() { diff --git a/apps/client/src/app/pages/about/about-page.component.ts b/apps/client/src/app/pages/about/about-page.component.ts index 25f41a1e..06255063 100644 --- a/apps/client/src/app/pages/about/about-page.component.ts +++ b/apps/client/src/app/pages/about/about-page.component.ts @@ -1,4 +1,4 @@ -import { ChangeDetectorRef, Component, OnInit } from '@angular/core'; +import { ChangeDetectorRef, Component, OnDestroy, OnInit } from '@angular/core'; import { DataService } from '@ghostfolio/client/services/data.service'; import { UserService } from '@ghostfolio/client/services/user/user.service'; import { baseCurrency } from '@ghostfolio/common/config'; @@ -15,7 +15,7 @@ import { environment } from '../../../environments/environment'; templateUrl: './about-page.html', styleUrls: ['./about-page.scss'] }) -export class AboutPageComponent implements OnInit { +export class AboutPageComponent implements OnDestroy, OnInit { public baseCurrency = baseCurrency; public hasPermissionForStatistics: boolean; public isLoggedIn: boolean; @@ -41,6 +41,7 @@ export class AboutPageComponent implements OnInit { public ngOnInit() { this.dataService .fetchInfo() + .pipe(takeUntil(this.unsubscribeSubject)) .subscribe(({ globalPermissions, statistics }) => { this.hasPermissionForStatistics = hasPermission( globalPermissions, diff --git a/apps/client/src/app/pages/account/account-page.component.ts b/apps/client/src/app/pages/account/account-page.component.ts index 9ae85c5e..8dd69da0 100644 --- a/apps/client/src/app/pages/account/account-page.component.ts +++ b/apps/client/src/app/pages/account/account-page.component.ts @@ -166,6 +166,7 @@ export class AccountPageComponent implements OnDestroy, OnInit { this.webAuthnService .deregister() .pipe( + takeUntil(this.unsubscribeSubject), catchError(() => { this.update(); @@ -181,6 +182,7 @@ export class AccountPageComponent implements OnDestroy, OnInit { this.webAuthnService .register() .pipe( + takeUntil(this.unsubscribeSubject), catchError(() => { this.update(); 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 656b6776..4f1bffce 100644 --- a/apps/client/src/app/pages/accounts/accounts-page.component.ts +++ b/apps/client/src/app/pages/accounts/accounts-page.component.ts @@ -1,4 +1,4 @@ -import { ChangeDetectorRef, Component, OnInit } from '@angular/core'; +import { ChangeDetectorRef, Component, OnDestroy, OnInit } from '@angular/core'; import { MatDialog } from '@angular/material/dialog'; import { ActivatedRoute, Router } from '@angular/router'; import { CreateAccountDto } from '@ghostfolio/api/app/account/create-account.dto'; @@ -20,7 +20,7 @@ import { CreateOrUpdateAccountDialog } from './create-or-update-account-dialog/c templateUrl: './accounts-page.html', styleUrls: ['./accounts-page.scss'] }) -export class AccountsPageComponent implements OnInit { +export class AccountsPageComponent implements OnDestroy, OnInit { public accounts: AccountModel[]; public deviceType: string; public hasImpersonationId: boolean; @@ -71,6 +71,7 @@ export class AccountsPageComponent implements OnInit { this.impersonationStorageService .onChangeHasImpersonation() + .pipe(takeUntil(this.unsubscribeSubject)) .subscribe((aId) => { this.hasImpersonationId = !!aId; }); @@ -98,23 +99,29 @@ export class AccountsPageComponent implements OnInit { } public fetchAccounts() { - this.dataService.fetchAccounts().subscribe((response) => { - this.accounts = response; + this.dataService + .fetchAccounts() + .pipe(takeUntil(this.unsubscribeSubject)) + .subscribe((response) => { + this.accounts = response; - if (this.accounts?.length <= 0) { - this.router.navigate([], { queryParams: { createDialog: true } }); - } + if (this.accounts?.length <= 0) { + this.router.navigate([], { queryParams: { createDialog: true } }); + } - this.changeDetectorRef.markForCheck(); - }); + this.changeDetectorRef.markForCheck(); + }); } public onDeleteAccount(aId: string) { - this.dataService.deleteAccount(aId).subscribe({ - next: () => { - this.fetchAccounts(); - } - }); + this.dataService + .deleteAccount(aId) + .pipe(takeUntil(this.unsubscribeSubject)) + .subscribe({ + next: () => { + this.fetchAccounts(); + } + }); } public onUpdateAccount(aAccount: AccountModel) { @@ -146,19 +153,25 @@ export class AccountsPageComponent implements OnInit { width: this.deviceType === 'mobile' ? '100vw' : '50rem' }); - dialogRef.afterClosed().subscribe((data: any) => { - const account: UpdateAccountDto = data?.account; + dialogRef + .afterClosed() + .pipe(takeUntil(this.unsubscribeSubject)) + .subscribe((data: any) => { + const account: UpdateAccountDto = data?.account; - if (account) { - this.dataService.putAccount(account).subscribe({ - next: () => { - this.fetchAccounts(); - } - }); - } + if (account) { + this.dataService + .putAccount(account) + .pipe(takeUntil(this.unsubscribeSubject)) + .subscribe({ + next: () => { + this.fetchAccounts(); + } + }); + } - this.router.navigate(['.'], { relativeTo: this.route }); - }); + this.router.navigate(['.'], { relativeTo: this.route }); + }); } public ngOnDestroy() { @@ -181,18 +194,24 @@ export class AccountsPageComponent implements OnInit { width: this.deviceType === 'mobile' ? '100vw' : '50rem' }); - dialogRef.afterClosed().subscribe((data: any) => { - const account: CreateAccountDto = data?.account; + dialogRef + .afterClosed() + .pipe(takeUntil(this.unsubscribeSubject)) + .subscribe((data: any) => { + const account: CreateAccountDto = data?.account; - if (account) { - this.dataService.postAccount(account).subscribe({ - next: () => { - this.fetchAccounts(); - } - }); - } + if (account) { + this.dataService + .postAccount(account) + .pipe(takeUntil(this.unsubscribeSubject)) + .subscribe({ + next: () => { + this.fetchAccounts(); + } + }); + } - this.router.navigate(['.'], { relativeTo: this.route }); - }); + this.router.navigate(['.'], { relativeTo: this.route }); + }); } } diff --git a/apps/client/src/app/pages/accounts/create-or-update-account-dialog/create-or-update-account-dialog.component.ts b/apps/client/src/app/pages/accounts/create-or-update-account-dialog/create-or-update-account-dialog.component.ts index 1ab5accb..bd809dfc 100644 --- a/apps/client/src/app/pages/accounts/create-or-update-account-dialog/create-or-update-account-dialog.component.ts +++ b/apps/client/src/app/pages/accounts/create-or-update-account-dialog/create-or-update-account-dialog.component.ts @@ -1,7 +1,14 @@ -import { ChangeDetectionStrategy, Component, Inject } from '@angular/core'; +import { + ChangeDetectionStrategy, + ChangeDetectorRef, + Component, + Inject, + OnDestroy +} from '@angular/core'; import { MAT_DIALOG_DATA, MatDialogRef } from '@angular/material/dialog'; import { Currency } from '@prisma/client'; import { Subject } from 'rxjs'; +import { takeUntil } from 'rxjs/operators'; import { DataService } from '../../../services/data.service'; import { CreateOrUpdateAccountDialogParams } from './interfaces/interfaces'; @@ -13,23 +20,29 @@ import { CreateOrUpdateAccountDialogParams } from './interfaces/interfaces'; styleUrls: ['./create-or-update-account-dialog.scss'], templateUrl: 'create-or-update-account-dialog.html' }) -export class CreateOrUpdateAccountDialog { +export class CreateOrUpdateAccountDialog implements OnDestroy { public currencies: Currency[] = []; public platforms: { id: string; name: string }[]; private unsubscribeSubject = new Subject(); public constructor( + private changeDetectorRef: ChangeDetectorRef, private dataService: DataService, public dialogRef: MatDialogRef, @Inject(MAT_DIALOG_DATA) public data: CreateOrUpdateAccountDialogParams ) {} ngOnInit() { - this.dataService.fetchInfo().subscribe(({ currencies, platforms }) => { - this.currencies = currencies; - this.platforms = platforms; - }); + this.dataService + .fetchInfo() + .pipe(takeUntil(this.unsubscribeSubject)) + .subscribe(({ currencies, platforms }) => { + this.currencies = currencies; + this.platforms = platforms; + + this.changeDetectorRef.markForCheck(); + }); } public onCancel(): void { diff --git a/apps/client/src/app/pages/admin/admin-page.component.ts b/apps/client/src/app/pages/admin/admin-page.component.ts index cb3b2544..3eebc14c 100644 --- a/apps/client/src/app/pages/admin/admin-page.component.ts +++ b/apps/client/src/app/pages/admin/admin-page.component.ts @@ -1,4 +1,4 @@ -import { ChangeDetectorRef, Component, OnInit } from '@angular/core'; +import { ChangeDetectorRef, Component, OnDestroy, OnInit } from '@angular/core'; import { AdminService } from '@ghostfolio/client/services/admin.service'; import { CacheService } from '@ghostfolio/client/services/cache.service'; import { DataService } from '@ghostfolio/client/services/data.service'; @@ -19,7 +19,7 @@ import { takeUntil } from 'rxjs/operators'; templateUrl: './admin-page.html', styleUrls: ['./admin-page.scss'] }) -export class AdminPageComponent implements OnInit { +export class AdminPageComponent implements OnDestroy, OnInit { public dataGatheringInProgress: boolean; public defaultDateFormat = DEFAULT_DATE_FORMAT; public exchangeRates: { label1: string; label2: string; value: number }[]; @@ -58,11 +58,14 @@ export class AdminPageComponent implements OnInit { } public onFlushCache() { - this.cacheService.flush().subscribe(() => { - setTimeout(() => { - window.location.reload(); - }, 300); - }); + this.cacheService + .flush() + .pipe(takeUntil(this.unsubscribeSubject)) + .subscribe(() => { + setTimeout(() => { + window.location.reload(); + }, 300); + }); } public onGatherMax() { @@ -71,11 +74,14 @@ export class AdminPageComponent implements OnInit { ); if (confirmation === true) { - this.adminService.gatherMax().subscribe(() => { - setTimeout(() => { - window.location.reload(); - }, 300); - }); + this.adminService + .gatherMax() + .pipe(takeUntil(this.unsubscribeSubject)) + .subscribe(() => { + setTimeout(() => { + window.location.reload(); + }, 300); + }); } } @@ -98,11 +104,14 @@ export class AdminPageComponent implements OnInit { const confirmation = confirm('Do you really want to delete this user?'); if (confirmation) { - this.dataService.deleteUser(aId).subscribe({ - next: () => { - this.fetchAdminData(); - } - }); + this.dataService + .deleteUser(aId) + .pipe(takeUntil(this.unsubscribeSubject)) + .subscribe({ + next: () => { + this.fetchAdminData(); + } + }); } } diff --git a/apps/client/src/app/pages/auth/auth-page.component.ts b/apps/client/src/app/pages/auth/auth-page.component.ts index e1a1877a..11c89020 100644 --- a/apps/client/src/app/pages/auth/auth-page.component.ts +++ b/apps/client/src/app/pages/auth/auth-page.component.ts @@ -1,17 +1,21 @@ -import { Component, OnInit } from '@angular/core'; +import { Component, OnDestroy, OnInit } from '@angular/core'; import { ActivatedRoute, Router } from '@angular/router'; import { STAY_SIGNED_IN, SettingsStorageService } from '@ghostfolio/client/services/settings-storage.service'; import { TokenStorageService } from '@ghostfolio/client/services/token-storage.service'; +import { Subject } from 'rxjs'; +import { takeUntil } from 'rxjs/operators'; @Component({ selector: 'gf-auth-page', templateUrl: './auth-page.html', styleUrls: ['./auth-page.scss'] }) -export class AuthPageComponent implements OnInit { +export class AuthPageComponent implements OnDestroy, OnInit { + private unsubscribeSubject = new Subject(); + /** * @constructor */ @@ -26,14 +30,21 @@ export class AuthPageComponent implements OnInit { * Initializes the controller */ public ngOnInit() { - this.route.params.subscribe((params) => { - const jwt = params['jwt']; - this.tokenStorageService.saveToken( - jwt, - this.settingsStorageService.getSetting(STAY_SIGNED_IN) === 'true' - ); + this.route.params + .pipe(takeUntil(this.unsubscribeSubject)) + .subscribe((params) => { + const jwt = params['jwt']; + this.tokenStorageService.saveToken( + jwt, + this.settingsStorageService.getSetting(STAY_SIGNED_IN) === 'true' + ); - this.router.navigate(['/']); - }); + this.router.navigate(['/']); + }); + } + + public ngOnDestroy() { + this.unsubscribeSubject.next(); + this.unsubscribeSubject.complete(); } } diff --git a/apps/client/src/app/pages/home/home-page.component.ts b/apps/client/src/app/pages/home/home-page.component.ts index 966ecd86..22a1bd78 100644 --- a/apps/client/src/app/pages/home/home-page.component.ts +++ b/apps/client/src/app/pages/home/home-page.component.ts @@ -116,6 +116,7 @@ export class HomePageComponent implements OnDestroy, OnInit { this.impersonationStorageService .onChangeHasImpersonation() + .pipe(takeUntil(this.unsubscribeSubject)) .subscribe((aId) => { this.hasImpersonationId = !!aId; }); @@ -148,9 +149,12 @@ export class HomePageComponent implements OnDestroy, OnInit { width: '50rem' }); - dialogRef.afterClosed().subscribe(() => { - this.router.navigate(['.'], { relativeTo: this.route }); - }); + dialogRef + .afterClosed() + .pipe(takeUntil(this.unsubscribeSubject)) + .subscribe(() => { + this.router.navigate(['.'], { relativeTo: this.route }); + }); } private update() { @@ -161,6 +165,7 @@ export class HomePageComponent implements OnDestroy, OnInit { this.dataService .fetchChart({ range: this.dateRange }) + .pipe(takeUntil(this.unsubscribeSubject)) .subscribe((chartData) => { this.historicalDataItems = chartData.map((chartDataItem) => { return { @@ -174,6 +179,7 @@ export class HomePageComponent implements OnDestroy, OnInit { this.dataService .fetchPortfolioPerformance({ range: this.dateRange }) + .pipe(takeUntil(this.unsubscribeSubject)) .subscribe((response) => { this.performance = response; this.isLoadingPerformance = false; @@ -181,15 +187,19 @@ export class HomePageComponent implements OnDestroy, OnInit { this.changeDetectorRef.markForCheck(); }); - this.dataService.fetchPortfolioOverview().subscribe((response) => { - this.overview = response; - this.isLoadingOverview = false; + this.dataService + .fetchPortfolioOverview() + .pipe(takeUntil(this.unsubscribeSubject)) + .subscribe((response) => { + this.overview = response; + this.isLoadingOverview = false; - this.changeDetectorRef.markForCheck(); - }); + this.changeDetectorRef.markForCheck(); + }); this.dataService .fetchPortfolioPositions({ range: this.dateRange }) + .pipe(takeUntil(this.unsubscribeSubject)) .subscribe((response) => { this.positions = response; this.hasPositions = diff --git a/apps/client/src/app/pages/landing/landing-page.component.ts b/apps/client/src/app/pages/landing/landing-page.component.ts index 1ff3717a..29b05fb7 100644 --- a/apps/client/src/app/pages/landing/landing-page.component.ts +++ b/apps/client/src/app/pages/landing/landing-page.component.ts @@ -6,6 +6,7 @@ import { TokenStorageService } from '@ghostfolio/client/services/token-storage.s import { WebAuthnService } from '@ghostfolio/client/services/web-authn.service'; import { format } from 'date-fns'; import { Subject } from 'rxjs'; +import { takeUntil } from 'rxjs/operators'; @Component({ selector: 'gf-landing-page', @@ -33,13 +34,16 @@ export class LandingPageComponent implements OnDestroy, OnInit { * Initializes the controller */ public ngOnInit() { - this.dataService.fetchInfo().subscribe(({ demoAuthToken }) => { - this.demoAuthToken = demoAuthToken; + this.dataService + .fetchInfo() + .pipe(takeUntil(this.unsubscribeSubject)) + .subscribe(({ demoAuthToken }) => { + this.demoAuthToken = demoAuthToken; - this.initializeLineChart(); + this.initializeLineChart(); - this.changeDetectorRef.markForCheck(); - }); + this.changeDetectorRef.markForCheck(); + }); } public initializeLineChart() { diff --git a/apps/client/src/app/pages/pricing/pricing-page.component.ts b/apps/client/src/app/pages/pricing/pricing-page.component.ts index 5f17ce22..061f2c52 100644 --- a/apps/client/src/app/pages/pricing/pricing-page.component.ts +++ b/apps/client/src/app/pages/pricing/pricing-page.component.ts @@ -1,4 +1,4 @@ -import { ChangeDetectorRef, Component, OnInit } from '@angular/core'; +import { ChangeDetectorRef, Component, OnDestroy, OnInit } from '@angular/core'; import { DataService } from '@ghostfolio/client/services/data.service'; import { UserService } from '@ghostfolio/client/services/user/user.service'; import { baseCurrency } from '@ghostfolio/common/config'; @@ -11,7 +11,7 @@ import { takeUntil } from 'rxjs/operators'; templateUrl: './pricing-page.html', styleUrls: ['./pricing-page.scss'] }) -export class PricingPageComponent implements OnInit { +export class PricingPageComponent implements OnDestroy, OnInit { public baseCurrency = baseCurrency; public coupon: number; public isLoggedIn: boolean; diff --git a/apps/client/src/app/pages/register/register-page.component.ts b/apps/client/src/app/pages/register/register-page.component.ts index 16668b00..33758eba 100644 --- a/apps/client/src/app/pages/register/register-page.component.ts +++ b/apps/client/src/app/pages/register/register-page.component.ts @@ -43,6 +43,7 @@ export class RegisterPageComponent implements OnDestroy, OnInit { public ngOnInit() { this.dataService .fetchInfo() + .pipe(takeUntil(this.unsubscribeSubject)) .subscribe(({ demoAuthToken, globalPermissions }) => { this.demoAuthToken = demoAuthToken; this.hasPermissionForSocialLogin = hasPermission( @@ -76,13 +77,16 @@ export class RegisterPageComponent implements OnDestroy, OnInit { width: '30rem' }); - dialogRef.afterClosed().subscribe((data) => { - if (data?.authToken) { - this.tokenStorageService.saveToken(authToken, true); + dialogRef + .afterClosed() + .pipe(takeUntil(this.unsubscribeSubject)) + .subscribe((data) => { + if (data?.authToken) { + this.tokenStorageService.saveToken(authToken, true); - this.router.navigate(['/']); - } - }); + this.router.navigate(['/']); + } + }); } public ngOnDestroy() { diff --git a/apps/client/src/app/pages/tools/report/report-page.component.ts b/apps/client/src/app/pages/tools/report/report-page.component.ts index 2e4f062f..776a6076 100644 --- a/apps/client/src/app/pages/tools/report/report-page.component.ts +++ b/apps/client/src/app/pages/tools/report/report-page.component.ts @@ -1,4 +1,4 @@ -import { ChangeDetectorRef, Component, OnInit } from '@angular/core'; +import { ChangeDetectorRef, Component, OnDestroy, OnInit } from '@angular/core'; import { DataService } from '@ghostfolio/client/services/data.service'; import { PortfolioReportRule } from '@ghostfolio/common/interfaces'; import { Subject } from 'rxjs'; @@ -9,7 +9,7 @@ import { takeUntil } from 'rxjs/operators'; templateUrl: './report-page.html', styleUrls: ['./report-page.scss'] }) -export class ReportPageComponent implements OnInit { +export class ReportPageComponent implements OnDestroy, OnInit { public accountClusterRiskRules: PortfolioReportRule[]; public currencyClusterRiskRules: PortfolioReportRule[]; public feeRules: PortfolioReportRule[]; diff --git a/apps/client/src/app/pages/transactions/create-or-update-transaction-dialog/create-or-update-transaction-dialog.component.ts b/apps/client/src/app/pages/transactions/create-or-update-transaction-dialog/create-or-update-transaction-dialog.component.ts index d6fc7e8c..ae19f826 100644 --- a/apps/client/src/app/pages/transactions/create-or-update-transaction-dialog/create-or-update-transaction-dialog.component.ts +++ b/apps/client/src/app/pages/transactions/create-or-update-transaction-dialog/create-or-update-transaction-dialog.component.ts @@ -2,7 +2,8 @@ import { ChangeDetectionStrategy, ChangeDetectorRef, Component, - Inject + Inject, + OnDestroy } from '@angular/core'; import { FormControl, Validators } from '@angular/forms'; import { MatAutocompleteSelectedEvent } from '@angular/material/autocomplete'; @@ -28,7 +29,7 @@ import { CreateOrUpdateTransactionDialogParams } from './interfaces/interfaces'; styleUrls: ['./create-or-update-transaction-dialog.scss'], templateUrl: 'create-or-update-transaction-dialog.html' }) -export class CreateOrUpdateTransactionDialog { +export class CreateOrUpdateTransactionDialog implements OnDestroy { public currencies: Currency[] = []; public currentMarketPrice = null; public filteredLookupItems: Observable; @@ -49,10 +50,15 @@ export class CreateOrUpdateTransactionDialog { ) {} ngOnInit() { - this.dataService.fetchInfo().subscribe(({ currencies, platforms }) => { - this.currencies = currencies; - this.platforms = platforms; - }); + this.dataService + .fetchInfo() + .pipe(takeUntil(this.unsubscribeSubject)) + .subscribe(({ currencies, platforms }) => { + this.currencies = currencies; + this.platforms = platforms; + + this.changeDetectorRef.markForCheck(); + }); this.filteredLookupItems = this.searchSymbolCtrl.valueChanges.pipe( startWith(''), @@ -73,6 +79,7 @@ export class CreateOrUpdateTransactionDialog { .pipe(takeUntil(this.unsubscribeSubject)) .subscribe(({ marketPrice }) => { this.currentMarketPrice = marketPrice; + this.changeDetectorRef.markForCheck(); }); } diff --git a/apps/client/src/app/pages/transactions/transactions-page.component.ts b/apps/client/src/app/pages/transactions/transactions-page.component.ts index 79888ca5..9ee74c3d 100644 --- a/apps/client/src/app/pages/transactions/transactions-page.component.ts +++ b/apps/client/src/app/pages/transactions/transactions-page.component.ts @@ -1,4 +1,4 @@ -import { ChangeDetectorRef, Component, OnInit } from '@angular/core'; +import { ChangeDetectorRef, Component, OnDestroy, OnInit } from '@angular/core'; import { MatDialog } from '@angular/material/dialog'; import { ActivatedRoute, Router } from '@angular/router'; import { CreateOrderDto } from '@ghostfolio/api/app/order/create-order.dto'; @@ -20,7 +20,7 @@ import { CreateOrUpdateTransactionDialog } from './create-or-update-transaction- templateUrl: './transactions-page.html', styleUrls: ['./transactions-page.scss'] }) -export class TransactionsPageComponent implements OnInit { +export class TransactionsPageComponent implements OnDestroy, OnInit { public deviceType: string; public hasImpersonationId: boolean; public hasPermissionToCreateOrder: boolean; @@ -71,6 +71,7 @@ export class TransactionsPageComponent implements OnInit { this.impersonationStorageService .onChangeHasImpersonation() + .pipe(takeUntil(this.unsubscribeSubject)) .subscribe((aId) => { this.hasImpersonationId = !!aId; }); @@ -98,15 +99,18 @@ export class TransactionsPageComponent implements OnInit { } public fetchOrders() { - this.dataService.fetchOrders().subscribe((response) => { - this.transactions = response; + this.dataService + .fetchOrders() + .pipe(takeUntil(this.unsubscribeSubject)) + .subscribe((response) => { + this.transactions = response; - if (this.transactions?.length <= 0) { - this.router.navigate([], { queryParams: { createDialog: true } }); - } + if (this.transactions?.length <= 0) { + this.router.navigate([], { queryParams: { createDialog: true } }); + } - this.changeDetectorRef.markForCheck(); - }); + this.changeDetectorRef.markForCheck(); + }); } public onCloneTransaction(aTransaction: OrderModel) { @@ -114,11 +118,14 @@ export class TransactionsPageComponent implements OnInit { } public onDeleteTransaction(aId: string) { - this.dataService.deleteOrder(aId).subscribe({ - next: () => { - this.fetchOrders(); - } - }); + this.dataService + .deleteOrder(aId) + .pipe(takeUntil(this.unsubscribeSubject)) + .subscribe({ + next: () => { + this.fetchOrders(); + } + }); } public onUpdateTransaction(aTransaction: OrderModel) { @@ -159,19 +166,25 @@ export class TransactionsPageComponent implements OnInit { width: this.deviceType === 'mobile' ? '100vw' : '50rem' }); - dialogRef.afterClosed().subscribe((data: any) => { - const transaction: UpdateOrderDto = data?.transaction; + dialogRef + .afterClosed() + .pipe(takeUntil(this.unsubscribeSubject)) + .subscribe((data: any) => { + const transaction: UpdateOrderDto = data?.transaction; - if (transaction) { - this.dataService.putOrder(transaction).subscribe({ - next: () => { - this.fetchOrders(); - } - }); - } + if (transaction) { + this.dataService + .putOrder(transaction) + .pipe(takeUntil(this.unsubscribeSubject)) + .subscribe({ + next: () => { + this.fetchOrders(); + } + }); + } - this.router.navigate(['.'], { relativeTo: this.route }); - }); + this.router.navigate(['.'], { relativeTo: this.route }); + }); } public ngOnDestroy() { @@ -203,18 +216,21 @@ export class TransactionsPageComponent implements OnInit { width: this.deviceType === 'mobile' ? '100vw' : '50rem' }); - dialogRef.afterClosed().subscribe((data: any) => { - const transaction: CreateOrderDto = data?.transaction; + dialogRef + .afterClosed() + .pipe(takeUntil(this.unsubscribeSubject)) + .subscribe((data: any) => { + const transaction: CreateOrderDto = data?.transaction; - if (transaction) { - this.dataService.postOrder(transaction).subscribe({ - next: () => { - this.fetchOrders(); - } - }); - } + if (transaction) { + this.dataService.postOrder(transaction).subscribe({ + next: () => { + this.fetchOrders(); + } + }); + } - this.router.navigate(['.'], { relativeTo: this.route }); - }); + this.router.navigate(['.'], { relativeTo: this.route }); + }); } } diff --git a/apps/client/src/app/pages/webauthn/webauthn-page.component.ts b/apps/client/src/app/pages/webauthn/webauthn-page.component.ts index 431e4bc8..838db520 100644 --- a/apps/client/src/app/pages/webauthn/webauthn-page.component.ts +++ b/apps/client/src/app/pages/webauthn/webauthn-page.component.ts @@ -1,16 +1,20 @@ -import { ChangeDetectorRef, Component, OnInit } from '@angular/core'; +import { ChangeDetectorRef, Component, OnDestroy, OnInit } from '@angular/core'; import { Router } from '@angular/router'; import { TokenStorageService } from '@ghostfolio/client/services/token-storage.service'; import { WebAuthnService } from '@ghostfolio/client/services/web-authn.service'; +import { Subject } from 'rxjs'; +import { takeUntil } from 'rxjs/operators'; @Component({ selector: 'gf-webauthn-page', templateUrl: './webauthn-page.html', styleUrls: ['./webauthn-page.scss'] }) -export class WebauthnPageComponent implements OnInit { +export class WebauthnPageComponent implements OnDestroy, OnInit { public hasError = false; + private unsubscribeSubject = new Subject(); + constructor( private changeDetectorRef: ChangeDetectorRef, private router: Router, @@ -23,24 +27,35 @@ export class WebauthnPageComponent implements OnInit { } public deregisterDevice() { - this.webAuthnService.deregister().subscribe(() => { - this.router.navigate(['/']); - }); + this.webAuthnService + .deregister() + .pipe(takeUntil(this.unsubscribeSubject)) + .subscribe(() => { + this.router.navigate(['/']); + }); } public signIn() { this.hasError = false; - this.webAuthnService.login().subscribe( - ({ authToken }) => { - this.tokenStorageService.saveToken(authToken, false); - this.router.navigate(['/']); - }, - (error) => { - console.error(error); - this.hasError = true; - this.changeDetectorRef.markForCheck(); - } - ); + this.webAuthnService + .login() + .pipe(takeUntil(this.unsubscribeSubject)) + .subscribe( + ({ authToken }) => { + this.tokenStorageService.saveToken(authToken, false); + this.router.navigate(['/']); + }, + (error) => { + console.error(error); + this.hasError = true; + this.changeDetectorRef.markForCheck(); + } + ); + } + + public ngOnDestroy() { + this.unsubscribeSubject.next(); + this.unsubscribeSubject.complete(); } } diff --git a/apps/client/src/app/pages/zen/zen-page.component.ts b/apps/client/src/app/pages/zen/zen-page.component.ts index ccb71b06..bcf75f39 100644 --- a/apps/client/src/app/pages/zen/zen-page.component.ts +++ b/apps/client/src/app/pages/zen/zen-page.component.ts @@ -61,6 +61,7 @@ export class ZenPageComponent implements OnDestroy, OnInit { this.impersonationStorageService .onChangeHasImpersonation() + .pipe(takeUntil(this.unsubscribeSubject)) .subscribe((aId) => { this.hasImpersonationId = !!aId; }); @@ -78,6 +79,7 @@ export class ZenPageComponent implements OnDestroy, OnInit { this.dataService .fetchChart({ range: this.dateRange }) + .pipe(takeUntil(this.unsubscribeSubject)) .subscribe((chartData) => { this.historicalDataItems = chartData.map((chartDataItem) => { return { @@ -91,6 +93,7 @@ export class ZenPageComponent implements OnDestroy, OnInit { this.dataService .fetchPortfolioPerformance({ range: this.dateRange }) + .pipe(takeUntil(this.unsubscribeSubject)) .subscribe((response) => { this.performance = response; this.isLoadingPerformance = false;