From 8bca0f58303904f427f022d57fd787ad92603769 Mon Sep 17 00:00:00 2001 From: Kenrick Tandrian <60643640+KenTandrian@users.noreply.github.com> Date: Thu, 12 Mar 2026 01:47:24 +0700 Subject: [PATCH 1/6] Task/improve type safety in portfolio proportion chart component (#6491) * Improve type safety in portfolio proportion chart component --- .../portfolio-proportion-chart.component.ts | 94 ++++++++++--------- 1 file changed, 48 insertions(+), 46 deletions(-) diff --git a/libs/ui/src/lib/portfolio-proportion-chart/portfolio-proportion-chart.component.ts b/libs/ui/src/lib/portfolio-proportion-chart/portfolio-proportion-chart.component.ts index 7d0203e9c..2f5d9e4f7 100644 --- a/libs/ui/src/lib/portfolio-proportion-chart/portfolio-proportion-chart.component.ts +++ b/libs/ui/src/lib/portfolio-proportion-chart/portfolio-proportion-chart.component.ts @@ -13,12 +13,11 @@ import { ChangeDetectionStrategy, Component, ElementRef, - EventEmitter, Input, OnChanges, OnDestroy, - Output, - ViewChild + output, + viewChild } from '@angular/core'; import { DataSource } from '@prisma/client'; import { Big } from 'big.js'; @@ -81,15 +80,16 @@ export class GfPortfolioProportionChartComponent @Input() maxItems?: number; @Input() showLabels = false; - @Output() proportionChartClicked = new EventEmitter(); - - @ViewChild('chartCanvas') chartCanvas: ElementRef; - public chart: Chart<'doughnut'>; public isLoading = true; + protected readonly proportionChartClicked = output(); + private readonly OTHER_KEY = 'OTHER'; + private readonly chartCanvas = + viewChild.required>('chartCanvas'); + private colorMap: { [symbol: string]: string; } = {}; @@ -130,45 +130,45 @@ export class GfPortfolioProportionChartComponent }; if (this.keys.length > 0) { + const primaryKey = this.keys[0]; + const secondaryKey = this.keys[1]; + Object.keys(this.data).forEach((symbol) => { - if (this.data[symbol][this.keys[0]]?.toUpperCase()) { - if (chartData[this.data[symbol][this.keys[0]].toUpperCase()]) { - chartData[this.data[symbol][this.keys[0]].toUpperCase()].value = - chartData[ - this.data[symbol][this.keys[0]].toUpperCase() - ].value.plus(this.data[symbol].value || 0); - - if ( - chartData[this.data[symbol][this.keys[0]].toUpperCase()] - .subCategory[this.data[symbol][this.keys[1]]] - ) { - chartData[ - this.data[symbol][this.keys[0]].toUpperCase() - ].subCategory[this.data[symbol][this.keys[1]]].value = chartData[ - this.data[symbol][this.keys[0]].toUpperCase() - ].subCategory[this.data[symbol][this.keys[1]]].value.plus( - this.data[symbol].value || 0 - ); + const asset = this.data[symbol]; + const assetValue = asset.value || 0; + const primaryKeyValue = (asset[primaryKey] as string)?.toUpperCase(); + const secondaryKeyValue = asset[secondaryKey] as string; + + if (primaryKeyValue) { + if (chartData[primaryKeyValue]) { + chartData[primaryKeyValue].value = + chartData[primaryKeyValue].value.plus(assetValue); + + const targetSubCategory = + chartData[primaryKeyValue].subCategory?.[secondaryKeyValue]; + if (targetSubCategory) { + targetSubCategory.value = + targetSubCategory.value.plus(assetValue); } else { - chartData[ - this.data[symbol][this.keys[0]].toUpperCase() - ].subCategory[this.data[symbol][this.keys[1]] ?? UNKNOWN_KEY] = { - value: new Big(this.data[symbol].value || 0) - }; + if (chartData[primaryKeyValue].subCategory) { + chartData[primaryKeyValue].subCategory[ + secondaryKeyValue ?? UNKNOWN_KEY + ] = { + value: new Big(assetValue) + }; + } } } else { - chartData[this.data[symbol][this.keys[0]].toUpperCase()] = { - name: this.data[symbol][this.keys[0]], + chartData[primaryKeyValue] = { + name: asset[primaryKey] as string, subCategory: {}, - value: new Big(this.data[symbol].value || 0) + value: new Big(assetValue) }; - if (this.data[symbol][this.keys[1]]) { - chartData[ - this.data[symbol][this.keys[0]].toUpperCase() - ].subCategory = { - [this.data[symbol][this.keys[1]]]: { - value: new Big(this.data[symbol].value || 0) + if (secondaryKeyValue) { + chartData[primaryKeyValue].subCategory = { + [secondaryKeyValue]: { + value: new Big(assetValue) } }; } @@ -181,10 +181,10 @@ export class GfPortfolioProportionChartComponent } else { chartData[UNKNOWN_KEY] = { name: this.data[symbol].name, - subCategory: this.keys[1] - ? { [this.keys[1]]: { value: new Big(0) } } + subCategory: secondaryKey + ? { [secondaryKey]: { value: new Big(0) } } : undefined, - value: new Big(this.data[symbol].value || 0) + value: new Big(assetValue) }; } } @@ -278,13 +278,15 @@ export class GfPortfolioProportionChartComponent Object.keys(item.subCategory ?? {}).forEach((subCategory) => { if (item.name === UNKNOWN_KEY) { - backgroundColorSubCategory.push(item.color); + backgroundColorSubCategory.push(item.color ?? ''); } else { backgroundColorSubCategory.push( Color(item.color).lighten(lightnessRatio).hex() ); } - dataSubCategory.push(item.subCategory[subCategory].value.toNumber()); + dataSubCategory.push( + item.subCategory?.[subCategory].value.toNumber() ?? 0 + ); labelSubCategory.push(subCategory); lightnessRatio += 0.1; @@ -334,7 +336,7 @@ export class GfPortfolioProportionChartComponent labels }; - if (this.chartCanvas) { + if (this.chartCanvas()) { if (this.chart) { this.chart.data = data; this.chart.options.plugins ??= {}; @@ -343,7 +345,7 @@ export class GfPortfolioProportionChartComponent this.chart.update(); } else { - this.chart = new Chart<'doughnut'>(this.chartCanvas.nativeElement, { + this.chart = new Chart<'doughnut'>(this.chartCanvas().nativeElement, { data, options: { animation: false, From 8f68bd98c25fcec978d958b3bcba836bc12fec1d Mon Sep 17 00:00:00 2001 From: Erwin-N <111194281+Erwin-N@users.noreply.github.com> Date: Wed, 11 Mar 2026 20:01:25 +0100 Subject: [PATCH 2/6] Task/upgrade svgmap to version 2.19.2 (#6503) * Upgrade svgmap to version 2.19.2 * Update changelog --- CHANGELOG.md | 6 ++++++ apps/client/src/styles.scss | 2 +- package-lock.json | 8 ++++---- package.json | 2 +- 4 files changed, 12 insertions(+), 6 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 98bcdb20f..103a09fbb 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 + +- Upgraded `svgmap` from version `2.14.0` to `2.19.2` + ## 2.249.0 - 2026-03-10 ### Added diff --git a/apps/client/src/styles.scss b/apps/client/src/styles.scss index b7a031bfa..142d2ea53 100644 --- a/apps/client/src/styles.scss +++ b/apps/client/src/styles.scss @@ -2,7 +2,7 @@ @import './styles/table'; @import './styles/variables'; -@import 'svgmap/dist/svgMap'; +@import 'svgmap/style.min'; :root { --dark-background: rgb(25, 25, 25); diff --git a/package-lock.json b/package-lock.json index e4cf3382c..9ae63af6e 100644 --- a/package-lock.json +++ b/package-lock.json @@ -90,7 +90,7 @@ "reflect-metadata": "0.2.2", "rxjs": "7.8.1", "stripe": "20.3.0", - "svgmap": "2.14.0", + "svgmap": "2.19.2", "tablemark": "4.1.0", "twitter-api-v2": "1.29.0", "yahoo-finance2": "3.13.2", @@ -32339,9 +32339,9 @@ "license": "BSD-2-Clause" }, "node_modules/svgmap": { - "version": "2.14.0", - "resolved": "https://registry.npmjs.org/svgmap/-/svgmap-2.14.0.tgz", - "integrity": "sha512-+Vklx4DO1uv1SFq6wnJWl/dRjX4uRT9CcsIHuADxAcZ+h5X1OSyDVbNdIu837fx5TtYYuaGRhWuFCXIioN/1ww==", + "version": "2.19.2", + "resolved": "https://registry.npmjs.org/svgmap/-/svgmap-2.19.2.tgz", + "integrity": "sha512-mRqRcQiwwSTh9kTOPhjTmd3ywxA9aTfybBHGAoyuGn9CI9PnAQsuZ7H/2/VEIvgJhi1xM5IGBfk8i4/Ke4iTCQ==", "license": "MIT", "dependencies": { "svg-pan-zoom": "^3.6.2" diff --git a/package.json b/package.json index 03f4a527d..fd62729b9 100644 --- a/package.json +++ b/package.json @@ -135,7 +135,7 @@ "reflect-metadata": "0.2.2", "rxjs": "7.8.1", "stripe": "20.3.0", - "svgmap": "2.14.0", + "svgmap": "2.19.2", "tablemark": "4.1.0", "twitter-api-v2": "1.29.0", "yahoo-finance2": "3.13.2", From 4dabd8ae68adcffeae4c142ef62439f77d822868 Mon Sep 17 00:00:00 2001 From: Kenrick Tandrian <60643640+KenTandrian@users.noreply.github.com> Date: Thu, 12 Mar 2026 02:07:32 +0700 Subject: [PATCH 3/6] Task/improve type safety in symbol autocomplete (#6498) * Improve type safety in symbol autocomplete --- .../lib/interfaces/lookup-item.interface.ts | 2 +- .../symbol-autocomplete.component.ts | 61 +++++++++---------- 2 files changed, 29 insertions(+), 34 deletions(-) diff --git a/libs/common/src/lib/interfaces/lookup-item.interface.ts b/libs/common/src/lib/interfaces/lookup-item.interface.ts index fa91ed690..6cedeca09 100644 --- a/libs/common/src/lib/interfaces/lookup-item.interface.ts +++ b/libs/common/src/lib/interfaces/lookup-item.interface.ts @@ -7,7 +7,7 @@ export interface LookupItem { assetSubClass: AssetSubClass; currency: string; dataProviderInfo: DataProviderInfo; - dataSource: DataSource; + dataSource: DataSource | null; name: string; symbol: string; } diff --git a/libs/ui/src/lib/symbol-autocomplete/symbol-autocomplete.component.ts b/libs/ui/src/lib/symbol-autocomplete/symbol-autocomplete.component.ts index c74e8a077..4b1898b8a 100644 --- a/libs/ui/src/lib/symbol-autocomplete/symbol-autocomplete.component.ts +++ b/libs/ui/src/lib/symbol-autocomplete/symbol-autocomplete.component.ts @@ -8,15 +8,17 @@ import { ChangeDetectionStrategy, ChangeDetectorRef, Component, + DestroyRef, DoCheck, ElementRef, Input, OnChanges, - OnDestroy, OnInit, SimpleChanges, - ViewChild + inject, + viewChild } from '@angular/core'; +import { takeUntilDestroyed } from '@angular/core/rxjs-interop'; import { FormControl, FormsModule, @@ -35,13 +37,12 @@ import { import { MatInput, MatInputModule } from '@angular/material/input'; import { MatProgressSpinnerModule } from '@angular/material/progress-spinner'; import { isString } from 'lodash'; -import { Subject, tap } from 'rxjs'; +import { tap } from 'rxjs'; import { debounceTime, distinctUntilChanged, filter, - switchMap, - takeUntil + switchMap } from 'rxjs/operators'; import { translate } from '../i18n'; @@ -77,21 +78,21 @@ import { AbstractMatFormField } from '../shared/abstract-mat-form-field'; }) export class GfSymbolAutocompleteComponent extends AbstractMatFormField - implements DoCheck, OnChanges, OnDestroy, OnInit + implements DoCheck, OnChanges, OnInit { @Input() public defaultLookupItems: LookupItem[] = []; @Input() public isLoading = false; - @ViewChild('symbolAutocomplete') public symbolAutocomplete: MatAutocomplete; - @Input() private includeIndices = false; - @ViewChild(MatInput) private input: MatInput; - - public control = new FormControl(); + public readonly control = new FormControl(); public lookupItems: (LookupItem & { assetSubClassString: string })[] = []; - private unsubscribeSubject = new Subject(); + protected readonly symbolAutocomplete = + viewChild.required('symbolAutocomplete'); + + private readonly destroyRef = inject(DestroyRef); + private readonly input = viewChild.required(MatInput); public constructor( public readonly _elementRef: ElementRef, @@ -105,13 +106,22 @@ export class GfSymbolAutocompleteComponent this.controlType = 'symbol-autocomplete'; } + public get empty() { + return this.input().empty; + } + + public set value(value: LookupItem) { + this.control.setValue(value); + super.value = value; + } + public ngOnInit() { if (this.disabled) { this.control.disable(); } this.control.valueChanges - .pipe(takeUntil(this.unsubscribeSubject)) + .pipe(takeUntilDestroyed(this.destroyRef)) .subscribe(() => { if (super.value) { super.value.dataSource = null; @@ -136,7 +146,7 @@ export class GfSymbolAutocompleteComponent }), debounceTime(400), distinctUntilChanged(), - takeUntil(this.unsubscribeSubject), + takeUntilDestroyed(this.destroyRef), switchMap((query: string) => { return this.dataService.fetchSymbols({ query, @@ -168,12 +178,8 @@ export class GfSymbolAutocompleteComponent return aLookupItem?.symbol ?? ''; } - public get empty() { - return this.input?.empty; - } - public focus() { - this.input.focus(); + this.input().focus(); } public isValueInOptions(value: string) { @@ -185,7 +191,7 @@ export class GfSymbolAutocompleteComponent public ngDoCheck() { if (this.ngControl) { this.validateRequired(); - this.errorState = this.ngControl.invalid && this.ngControl.touched; + this.errorState = !!(this.ngControl.invalid && this.ngControl.touched); this.stateChanges.next(); } } @@ -197,18 +203,6 @@ export class GfSymbolAutocompleteComponent } as LookupItem; } - public set value(value: LookupItem) { - this.control.setValue(value); - super.value = value; - } - - public ngOnDestroy() { - super.ngOnDestroy(); - - this.unsubscribeSubject.next(); - this.unsubscribeSubject.complete(); - } - private showDefaultOptions() { this.lookupItems = this.defaultLookupItems.map((lookupItem) => { return { @@ -224,8 +218,9 @@ export class GfSymbolAutocompleteComponent const requiredCheck = super.required ? !super.value?.dataSource || !super.value?.symbol : false; + if (requiredCheck) { - this.ngControl.control.setErrors({ invalidData: true }); + this.ngControl.control?.setErrors({ invalidData: true }); } } } From bac3e2ebf36aedad595845bc204e518fe8821ef1 Mon Sep 17 00:00:00 2001 From: Erwin-N <111194281+Erwin-N@users.noreply.github.com> Date: Thu, 12 Mar 2026 17:25:22 +0100 Subject: [PATCH 4/6] Task/eliminate OnDestroy lifecycle hook from FIRE page (#6521) * Eliminate OnDestroy lifecycle hook --- .../portfolio/fire/fire-page.component.ts | 48 +++++++++---------- 1 file changed, 23 insertions(+), 25 deletions(-) diff --git a/apps/client/src/app/pages/portfolio/fire/fire-page.component.ts b/apps/client/src/app/pages/portfolio/fire/fire-page.component.ts index 27db6c76e..2f7568982 100644 --- a/apps/client/src/app/pages/portfolio/fire/fire-page.component.ts +++ b/apps/client/src/app/pages/portfolio/fire/fire-page.component.ts @@ -12,14 +12,18 @@ import { DataService } from '@ghostfolio/ui/services'; import { GfValueComponent } from '@ghostfolio/ui/value'; import { CommonModule, NgStyle } from '@angular/common'; -import { ChangeDetectorRef, Component, OnDestroy, OnInit } from '@angular/core'; +import { + ChangeDetectorRef, + Component, + DestroyRef, + OnInit +} from '@angular/core'; +import { takeUntilDestroyed } from '@angular/core/rxjs-interop'; import { FormsModule, ReactiveFormsModule } from '@angular/forms'; import { FormControl } from '@angular/forms'; import { Big } from 'big.js'; import { DeviceDetectorService } from 'ngx-device-detector'; import { NgxSkeletonLoaderModule } from 'ngx-skeleton-loader'; -import { Subject } from 'rxjs'; -import { takeUntil } from 'rxjs/operators'; @Component({ imports: [ @@ -36,7 +40,7 @@ import { takeUntil } from 'rxjs/operators'; styleUrls: ['./fire-page.scss'], templateUrl: './fire-page.html' }) -export class GfFirePageComponent implements OnDestroy, OnInit { +export class GfFirePageComponent implements OnInit { public deviceType: string; public fireWealth: FireWealth; public hasImpersonationId: boolean; @@ -52,11 +56,10 @@ export class GfFirePageComponent implements OnDestroy, OnInit { public withdrawalRatePerYear: Big; public withdrawalRatePerYearProjected: Big; - private unsubscribeSubject = new Subject(); - public constructor( private changeDetectorRef: ChangeDetectorRef, private dataService: DataService, + private destroyRef: DestroyRef, private deviceService: DeviceDetectorService, private impersonationStorageService: ImpersonationStorageService, private userService: UserService @@ -68,7 +71,7 @@ export class GfFirePageComponent implements OnDestroy, OnInit { this.dataService .fetchPortfolioDetails() - .pipe(takeUntil(this.unsubscribeSubject)) + .pipe(takeUntilDestroyed(this.destroyRef)) .subscribe(({ summary }) => { this.fireWealth = { today: { @@ -92,19 +95,19 @@ export class GfFirePageComponent implements OnDestroy, OnInit { this.impersonationStorageService .onChangeHasImpersonation() - .pipe(takeUntil(this.unsubscribeSubject)) + .pipe(takeUntilDestroyed(this.destroyRef)) .subscribe((impersonationId) => { this.hasImpersonationId = !!impersonationId; }); this.safeWithdrawalRateControl.valueChanges - .pipe(takeUntil(this.unsubscribeSubject)) + .pipe(takeUntilDestroyed(this.destroyRef)) .subscribe((value) => { this.onSafeWithdrawalRateChange(Number(value)); }); this.userService.stateChanged - .pipe(takeUntil(this.unsubscribeSubject)) + .pipe(takeUntilDestroyed(this.destroyRef)) .subscribe((state) => { if (state?.user) { this.user = state.user; @@ -132,11 +135,11 @@ export class GfFirePageComponent implements OnDestroy, OnInit { public onAnnualInterestRateChange(annualInterestRate: number) { this.dataService .putUserSetting({ annualInterestRate }) - .pipe(takeUntil(this.unsubscribeSubject)) + .pipe(takeUntilDestroyed(this.destroyRef)) .subscribe(() => { this.userService .get(true) - .pipe(takeUntil(this.unsubscribeSubject)) + .pipe(takeUntilDestroyed(this.destroyRef)) .subscribe((user) => { this.user = user; @@ -163,11 +166,11 @@ export class GfFirePageComponent implements OnDestroy, OnInit { retirementDate: retirementDate.toISOString(), projectedTotalAmount: null }) - .pipe(takeUntil(this.unsubscribeSubject)) + .pipe(takeUntilDestroyed(this.destroyRef)) .subscribe(() => { this.userService .get(true) - .pipe(takeUntil(this.unsubscribeSubject)) + .pipe(takeUntilDestroyed(this.destroyRef)) .subscribe((user) => { this.user = user; @@ -179,11 +182,11 @@ export class GfFirePageComponent implements OnDestroy, OnInit { public onSafeWithdrawalRateChange(safeWithdrawalRate: number) { this.dataService .putUserSetting({ safeWithdrawalRate }) - .pipe(takeUntil(this.unsubscribeSubject)) + .pipe(takeUntilDestroyed(this.destroyRef)) .subscribe(() => { this.userService .get(true) - .pipe(takeUntil(this.unsubscribeSubject)) + .pipe(takeUntilDestroyed(this.destroyRef)) .subscribe((user) => { this.user = user; @@ -198,11 +201,11 @@ export class GfFirePageComponent implements OnDestroy, OnInit { public onSavingsRateChange(savingsRate: number) { this.dataService .putUserSetting({ savingsRate }) - .pipe(takeUntil(this.unsubscribeSubject)) + .pipe(takeUntilDestroyed(this.destroyRef)) .subscribe(() => { this.userService .get(true) - .pipe(takeUntil(this.unsubscribeSubject)) + .pipe(takeUntilDestroyed(this.destroyRef)) .subscribe((user) => { this.user = user; @@ -217,11 +220,11 @@ export class GfFirePageComponent implements OnDestroy, OnInit { projectedTotalAmount, retirementDate: null }) - .pipe(takeUntil(this.unsubscribeSubject)) + .pipe(takeUntilDestroyed(this.destroyRef)) .subscribe(() => { this.userService .get(true) - .pipe(takeUntil(this.unsubscribeSubject)) + .pipe(takeUntilDestroyed(this.destroyRef)) .subscribe((user) => { this.user = user; @@ -230,11 +233,6 @@ export class GfFirePageComponent implements OnDestroy, OnInit { }); } - public ngOnDestroy() { - this.unsubscribeSubject.next(); - this.unsubscribeSubject.complete(); - } - private calculateWithdrawalRates() { if (this.fireWealth && this.user?.settings?.safeWithdrawalRate) { this.withdrawalRatePerYear = new Big( From 55d717ca9397b5e5d3ee5c360b550eedd3dcd5ad Mon Sep 17 00:00:00 2001 From: Erwin-N <111194281+Erwin-N@users.noreply.github.com> Date: Thu, 12 Mar 2026 20:03:37 +0100 Subject: [PATCH 5/6] Task/eliminate OnDestroy lifecycle hook from X-ray page (#6522) * Eliminate OnDestroy lifecycle hook --- .../portfolio/x-ray/x-ray-page.component.ts | 22 +++++++------------ 1 file changed, 8 insertions(+), 14 deletions(-) diff --git a/apps/client/src/app/pages/portfolio/x-ray/x-ray-page.component.ts b/apps/client/src/app/pages/portfolio/x-ray/x-ray-page.component.ts index 70b748b10..e97fd4876 100644 --- a/apps/client/src/app/pages/portfolio/x-ray/x-ray-page.component.ts +++ b/apps/client/src/app/pages/portfolio/x-ray/x-ray-page.component.ts @@ -12,7 +12,8 @@ import { GfPremiumIndicatorComponent } from '@ghostfolio/ui/premium-indicator'; import { DataService } from '@ghostfolio/ui/services'; import { NgClass } from '@angular/common'; -import { ChangeDetectorRef, Component } from '@angular/core'; +import { ChangeDetectorRef, Component, DestroyRef } from '@angular/core'; +import { takeUntilDestroyed } from '@angular/core/rxjs-interop'; import { IonIcon } from '@ionic/angular/standalone'; import { addIcons } from 'ionicons'; import { @@ -21,7 +22,6 @@ import { warningOutline } from 'ionicons/icons'; import { NgxSkeletonLoaderModule } from 'ngx-skeleton-loader'; -import { Subject, takeUntil } from 'rxjs'; @Component({ imports: [ @@ -48,11 +48,10 @@ export class GfXRayPageComponent { public statistics: PortfolioReportResponse['xRay']['statistics']; public user: User; - private unsubscribeSubject = new Subject(); - public constructor( private changeDetectorRef: ChangeDetectorRef, private dataService: DataService, + private destroyRef: DestroyRef, private impersonationStorageService: ImpersonationStorageService, private userService: UserService ) { @@ -62,13 +61,13 @@ export class GfXRayPageComponent { public ngOnInit() { this.impersonationStorageService .onChangeHasImpersonation() - .pipe(takeUntil(this.unsubscribeSubject)) + .pipe(takeUntilDestroyed(this.destroyRef)) .subscribe((impersonationId) => { this.hasImpersonationId = !!impersonationId; }); this.userService.stateChanged - .pipe(takeUntil(this.unsubscribeSubject)) + .pipe(takeUntilDestroyed(this.destroyRef)) .subscribe((state) => { if (state?.user) { this.user = state.user; @@ -91,28 +90,23 @@ export class GfXRayPageComponent { public onRulesUpdated(event: UpdateUserSettingDto) { this.dataService .putUserSetting(event) - .pipe(takeUntil(this.unsubscribeSubject)) + .pipe(takeUntilDestroyed(this.destroyRef)) .subscribe(() => { this.userService .get(true) - .pipe(takeUntil(this.unsubscribeSubject)) + .pipe(takeUntilDestroyed(this.destroyRef)) .subscribe(); this.initializePortfolioReport(); }); } - public ngOnDestroy() { - this.unsubscribeSubject.next(); - this.unsubscribeSubject.complete(); - } - private initializePortfolioReport() { this.isLoading = true; this.dataService .fetchPortfolioReport() - .pipe(takeUntil(this.unsubscribeSubject)) + .pipe(takeUntilDestroyed(this.destroyRef)) .subscribe(({ xRay: { categories, statistics } }) => { this.categories = categories; this.inactiveRules = this.mergeInactiveRules(categories); From cfa974385adaadd5c0b94ebde95bce341519d02e Mon Sep 17 00:00:00 2001 From: Erwin-N <111194281+Erwin-N@users.noreply.github.com> Date: Fri, 13 Mar 2026 12:08:54 +0100 Subject: [PATCH 6/6] Task/eliminate OnDestroy lifecycle hook from accounts page (#6527) * Eliminate OnDestroy lifecycle hook --- .../pages/accounts/accounts-page.component.ts | 52 +++++++++---------- 1 file changed, 26 insertions(+), 26 deletions(-) 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 a0eef4eba..fdc78a8c4 100644 --- a/apps/client/src/app/pages/accounts/accounts-page.component.ts +++ b/apps/client/src/app/pages/accounts/accounts-page.component.ts @@ -13,7 +13,13 @@ import { GfAccountsTableComponent } from '@ghostfolio/ui/accounts-table'; import { NotificationService } from '@ghostfolio/ui/notifications'; import { DataService } from '@ghostfolio/ui/services'; -import { ChangeDetectorRef, Component, OnDestroy, OnInit } from '@angular/core'; +import { + ChangeDetectorRef, + Component, + DestroyRef, + OnInit +} from '@angular/core'; +import { takeUntilDestroyed } from '@angular/core/rxjs-interop'; import { MatButtonModule } from '@angular/material/button'; import { MatDialog } from '@angular/material/dialog'; import { ActivatedRoute, Router, RouterModule } from '@angular/router'; @@ -21,8 +27,8 @@ import { Account as AccountModel } from '@prisma/client'; import { addIcons } from 'ionicons'; import { addOutline } from 'ionicons/icons'; import { DeviceDetectorService } from 'ngx-device-detector'; -import { EMPTY, Subject, Subscription } from 'rxjs'; -import { catchError, takeUntil } from 'rxjs/operators'; +import { EMPTY, Subscription } from 'rxjs'; +import { catchError } from 'rxjs/operators'; import { GfCreateOrUpdateAccountDialogComponent } from './create-or-update-account-dialog/create-or-update-account-dialog.component'; import { CreateOrUpdateAccountDialogParams } from './create-or-update-account-dialog/interfaces/interfaces'; @@ -36,7 +42,7 @@ import { GfTransferBalanceDialogComponent } from './transfer-balance/transfer-ba styleUrls: ['./accounts-page.scss'], templateUrl: './accounts-page.html' }) -export class GfAccountsPageComponent implements OnDestroy, OnInit { +export class GfAccountsPageComponent implements OnInit { public accounts: AccountModel[]; public activitiesCount = 0; public deviceType: string; @@ -48,11 +54,10 @@ export class GfAccountsPageComponent implements OnDestroy, OnInit { public totalValueInBaseCurrency = 0; public user: User; - private unsubscribeSubject = new Subject(); - public constructor( private changeDetectorRef: ChangeDetectorRef, private dataService: DataService, + private destroyRef: DestroyRef, private deviceService: DeviceDetectorService, private dialog: MatDialog, private impersonationStorageService: ImpersonationStorageService, @@ -62,7 +67,7 @@ export class GfAccountsPageComponent implements OnDestroy, OnInit { private userService: UserService ) { this.route.queryParams - .pipe(takeUntil(this.unsubscribeSubject)) + .pipe(takeUntilDestroyed(this.destroyRef)) .subscribe((params) => { if (params['accountId'] && params['accountDetailDialog']) { this.openAccountDetailDialog(params['accountId']); @@ -94,13 +99,13 @@ export class GfAccountsPageComponent implements OnDestroy, OnInit { this.impersonationStorageService .onChangeHasImpersonation() - .pipe(takeUntil(this.unsubscribeSubject)) + .pipe(takeUntilDestroyed(this.destroyRef)) .subscribe((impersonationId) => { this.hasImpersonationId = !!impersonationId; }); this.userService.stateChanged - .pipe(takeUntil(this.unsubscribeSubject)) + .pipe(takeUntilDestroyed(this.destroyRef)) .subscribe((state) => { if (state?.user) { this.user = state.user; @@ -124,7 +129,7 @@ export class GfAccountsPageComponent implements OnDestroy, OnInit { public fetchAccounts() { this.dataService .fetchAccounts() - .pipe(takeUntil(this.unsubscribeSubject)) + .pipe(takeUntilDestroyed(this.destroyRef)) .subscribe( ({ accounts, @@ -151,11 +156,11 @@ export class GfAccountsPageComponent implements OnDestroy, OnInit { this.dataService .deleteAccount(aId) - .pipe(takeUntil(this.unsubscribeSubject)) + .pipe(takeUntilDestroyed(this.destroyRef)) .subscribe(() => { this.userService .get(true) - .pipe(takeUntil(this.unsubscribeSubject)) + .pipe(takeUntilDestroyed(this.destroyRef)) .subscribe(); this.fetchAccounts(); @@ -204,18 +209,18 @@ export class GfAccountsPageComponent implements OnDestroy, OnInit { dialogRef .afterClosed() - .pipe(takeUntil(this.unsubscribeSubject)) + .pipe(takeUntilDestroyed(this.destroyRef)) .subscribe((account: UpdateAccountDto | null) => { if (account) { this.reset(); this.dataService .putAccount(account) - .pipe(takeUntil(this.unsubscribeSubject)) + .pipe(takeUntilDestroyed(this.destroyRef)) .subscribe(() => { this.userService .get(true) - .pipe(takeUntil(this.unsubscribeSubject)) + .pipe(takeUntilDestroyed(this.destroyRef)) .subscribe(); this.fetchAccounts(); @@ -228,11 +233,6 @@ export class GfAccountsPageComponent implements OnDestroy, OnInit { }); } - public ngOnDestroy() { - this.unsubscribeSubject.next(); - this.unsubscribeSubject.complete(); - } - private openAccountDetailDialog(aAccountId: string) { const dialogRef = this.dialog.open< GfAccountDetailDialogComponent, @@ -254,7 +254,7 @@ export class GfAccountsPageComponent implements OnDestroy, OnInit { dialogRef .afterClosed() - .pipe(takeUntil(this.unsubscribeSubject)) + .pipe(takeUntilDestroyed(this.destroyRef)) .subscribe(() => { this.fetchAccounts(); @@ -284,18 +284,18 @@ export class GfAccountsPageComponent implements OnDestroy, OnInit { dialogRef .afterClosed() - .pipe(takeUntil(this.unsubscribeSubject)) + .pipe(takeUntilDestroyed(this.destroyRef)) .subscribe((account: CreateAccountDto | null) => { if (account) { this.reset(); this.dataService .postAccount(account) - .pipe(takeUntil(this.unsubscribeSubject)) + .pipe(takeUntilDestroyed(this.destroyRef)) .subscribe(() => { this.userService .get(true) - .pipe(takeUntil(this.unsubscribeSubject)) + .pipe(takeUntilDestroyed(this.destroyRef)) .subscribe(); this.fetchAccounts(); @@ -321,7 +321,7 @@ export class GfAccountsPageComponent implements OnDestroy, OnInit { dialogRef .afterClosed() - .pipe(takeUntil(this.unsubscribeSubject)) + .pipe(takeUntilDestroyed(this.destroyRef)) .subscribe((data: any) => { if (data) { this.reset(); @@ -343,7 +343,7 @@ export class GfAccountsPageComponent implements OnDestroy, OnInit { return EMPTY; }), - takeUntil(this.unsubscribeSubject) + takeUntilDestroyed(this.destroyRef) ) .subscribe(() => { this.fetchAccounts();