Browse Source

Merge branch 'main' into task/switch-to-use-asset-profile-data-from-portfolio-position

pull/6499/head
Thomas Kaul 3 weeks ago
committed by GitHub
parent
commit
4f3c950e2b
No known key found for this signature in database GPG Key ID: B5690EEEBB952194
  1. 2
      CHANGELOG.md
  2. 7
      apps/api/src/app/endpoints/assets/assets.controller.ts
  3. 6
      apps/api/src/services/i18n/i18n.service.ts
  4. 7
      apps/client/src/app/app.component.ts
  5. 6
      apps/client/src/app/components/admin-jobs/admin-jobs.component.ts
  6. 5
      apps/client/src/app/components/admin-users/admin-users.component.ts
  7. 27
      apps/client/src/app/components/home-holdings/home-holdings.component.ts
  8. 21
      apps/client/src/app/components/home-market/home-market.component.ts
  9. 23
      apps/client/src/app/components/home-overview/home-overview.component.ts
  10. 25
      apps/client/src/app/components/home-summary/home-summary.component.ts
  11. 5
      apps/client/src/app/components/user-account-access/user-account-access.component.ts
  12. 5
      apps/client/src/app/components/user-account-settings/user-account-settings.component.ts
  13. 2
      apps/client/src/app/core/auth.guard.ts
  14. 6
      apps/client/src/app/core/http-response.interceptor.ts
  15. 17
      apps/client/src/app/pages/about/about-page.component.ts
  16. 52
      apps/client/src/app/pages/accounts/accounts-page.component.ts
  17. 37
      apps/client/src/app/pages/portfolio/analysis/analysis-page.component.ts
  18. 48
      apps/client/src/app/pages/portfolio/fire/fire-page.component.ts
  19. 22
      apps/client/src/app/pages/portfolio/x-ray/x-ray-page.component.ts
  20. 6
      apps/client/src/app/pages/register/register-page.component.ts
  21. 26
      apps/client/src/app/services/token-storage.service.ts
  22. 36
      apps/client/src/app/services/user/user.service.ts
  23. 64
      apps/client/src/locales/messages.pl.xlf
  24. 2
      libs/ui/src/lib/benchmark/benchmark-detail-dialog/interfaces/interfaces.ts
  25. 16
      libs/ui/src/lib/benchmark/benchmark.component.html
  26. 129
      libs/ui/src/lib/benchmark/benchmark.component.ts
  27. 4
      libs/ui/src/lib/holdings-table/holdings-table.component.html
  28. 19
      libs/ui/src/lib/holdings-table/holdings-table.component.ts
  29. 7
      libs/ui/src/lib/toggle/toggle.component.html
  30. 32
      libs/ui/src/lib/toggle/toggle.component.ts

2
CHANGELOG.md

@ -11,6 +11,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- Switched to using asset profile data from the endpoint `GET api/v1/portfolio/holdings` - Switched to using asset profile data from the endpoint `GET api/v1/portfolio/holdings`
- Switched to using asset profile data from the holdings of the public page - Switched to using asset profile data from the holdings of the public page
- Consolidated the sign-out logic within the user service to unify cookie, state and token clearance
- Improved the language localization for Polish (`pl`)
- Upgraded `svgmap` from version `2.14.0` to `2.19.2` - Upgraded `svgmap` from version `2.14.0` to `2.19.2`
## 2.249.0 - 2026-03-10 ## 2.249.0 - 2026-03-10

7
apps/api/src/app/endpoints/assets/assets.controller.ts

@ -4,6 +4,7 @@ import { interpolate } from '@ghostfolio/common/helper';
import { import {
Controller, Controller,
Get, Get,
OnModuleInit,
Param, Param,
Res, Res,
Version, Version,
@ -14,12 +15,14 @@ import { readFileSync } from 'node:fs';
import { join } from 'node:path'; import { join } from 'node:path';
@Controller('assets') @Controller('assets')
export class AssetsController { export class AssetsController implements OnModuleInit {
private webManifest = ''; private webManifest = '';
public constructor( public constructor(
public readonly configurationService: ConfigurationService public readonly configurationService: ConfigurationService
) { ) {}
public onModuleInit() {
try { try {
this.webManifest = readFileSync( this.webManifest = readFileSync(
join(__dirname, 'assets', 'site.webmanifest'), join(__dirname, 'assets', 'site.webmanifest'),

6
apps/api/src/services/i18n/i18n.service.ts

@ -1,16 +1,16 @@
import { DEFAULT_LANGUAGE_CODE } from '@ghostfolio/common/config'; import { DEFAULT_LANGUAGE_CODE } from '@ghostfolio/common/config';
import { Injectable, Logger } from '@nestjs/common'; import { Injectable, Logger, OnModuleInit } from '@nestjs/common';
import * as cheerio from 'cheerio'; import * as cheerio from 'cheerio';
import { readFileSync, readdirSync } from 'node:fs'; import { readFileSync, readdirSync } from 'node:fs';
import { join } from 'node:path'; import { join } from 'node:path';
@Injectable() @Injectable()
export class I18nService { export class I18nService implements OnModuleInit {
private localesPath = join(__dirname, 'assets', 'locales'); private localesPath = join(__dirname, 'assets', 'locales');
private translations: { [locale: string]: cheerio.CheerioAPI } = {}; private translations: { [locale: string]: cheerio.CheerioAPI } = {};
public constructor() { public onModuleInit() {
this.loadFiles(); this.loadFiles();
} }

7
apps/client/src/app/app.component.ts

@ -38,7 +38,6 @@ import { GfHeaderComponent } from './components/header/header.component';
import { GfHoldingDetailDialogComponent } from './components/holding-detail-dialog/holding-detail-dialog.component'; import { GfHoldingDetailDialogComponent } from './components/holding-detail-dialog/holding-detail-dialog.component';
import { HoldingDetailDialogParams } from './components/holding-detail-dialog/interfaces/interfaces'; import { HoldingDetailDialogParams } from './components/holding-detail-dialog/interfaces/interfaces';
import { ImpersonationStorageService } from './services/impersonation-storage.service'; import { ImpersonationStorageService } from './services/impersonation-storage.service';
import { TokenStorageService } from './services/token-storage.service';
import { UserService } from './services/user/user.service'; import { UserService } from './services/user/user.service';
@Component({ @Component({
@ -82,7 +81,6 @@ export class GfAppComponent implements OnDestroy, OnInit {
private route: ActivatedRoute, private route: ActivatedRoute,
private router: Router, private router: Router,
private title: Title, private title: Title,
private tokenStorageService: TokenStorageService,
private userService: UserService private userService: UserService
) { ) {
this.initializeTheme(); this.initializeTheme();
@ -236,12 +234,11 @@ export class GfAppComponent implements OnDestroy, OnInit {
} }
public onCreateAccount() { public onCreateAccount() {
this.tokenStorageService.signOut(); this.userService.signOut();
} }
public onSignOut() { public onSignOut() {
this.tokenStorageService.signOut(); this.userService.signOut();
this.userService.remove();
document.location.href = `/${document.documentElement.lang}`; document.location.href = `/${document.documentElement.lang}`;
} }

6
apps/client/src/app/components/admin-jobs/admin-jobs.component.ts

@ -194,7 +194,11 @@ export class GfAdminJobsComponent implements OnInit {
public onOpenBullBoard() { public onOpenBullBoard() {
const token = this.tokenStorageService.getToken(); const token = this.tokenStorageService.getToken();
document.cookie = `${BULL_BOARD_COOKIE_NAME}=${token}; path=${BULL_BOARD_ROUTE}; SameSite=Strict`; document.cookie = [
`${BULL_BOARD_COOKIE_NAME}=${encodeURIComponent(token)}`,
'path=/',
'SameSite=Strict'
].join('; ');
window.open(BULL_BOARD_ROUTE, '_blank'); window.open(BULL_BOARD_ROUTE, '_blank');
} }

5
apps/client/src/app/components/admin-users/admin-users.component.ts

@ -1,7 +1,6 @@
import { UserDetailDialogParams } from '@ghostfolio/client/components/user-detail-dialog/interfaces/interfaces'; import { UserDetailDialogParams } from '@ghostfolio/client/components/user-detail-dialog/interfaces/interfaces';
import { GfUserDetailDialogComponent } from '@ghostfolio/client/components/user-detail-dialog/user-detail-dialog.component'; import { GfUserDetailDialogComponent } from '@ghostfolio/client/components/user-detail-dialog/user-detail-dialog.component';
import { ImpersonationStorageService } from '@ghostfolio/client/services/impersonation-storage.service'; import { ImpersonationStorageService } from '@ghostfolio/client/services/impersonation-storage.service';
import { TokenStorageService } from '@ghostfolio/client/services/token-storage.service';
import { UserService } from '@ghostfolio/client/services/user/user.service'; import { UserService } from '@ghostfolio/client/services/user/user.service';
import { DEFAULT_PAGE_SIZE } from '@ghostfolio/common/config'; import { DEFAULT_PAGE_SIZE } from '@ghostfolio/common/config';
import { ConfirmationDialogType } from '@ghostfolio/common/enums'; import { ConfirmationDialogType } from '@ghostfolio/common/enums';
@ -106,7 +105,6 @@ export class GfAdminUsersComponent implements OnDestroy, OnInit {
private notificationService: NotificationService, private notificationService: NotificationService,
private route: ActivatedRoute, private route: ActivatedRoute,
private router: Router, private router: Router,
private tokenStorageService: TokenStorageService,
private userService: UserService private userService: UserService
) { ) {
this.deviceType = this.deviceService.getDeviceInfo().deviceType; this.deviceType = this.deviceService.getDeviceInfo().deviceType;
@ -229,8 +227,7 @@ export class GfAdminUsersComponent implements OnDestroy, OnInit {
this.notificationService.alert({ this.notificationService.alert({
discardFn: () => { discardFn: () => {
if (aUserId === this.user.id) { if (aUserId === this.user.id) {
this.tokenStorageService.signOut(); this.userService.signOut();
this.userService.remove();
document.location.href = `/${document.documentElement.lang}`; document.location.href = `/${document.documentElement.lang}`;
} }

27
apps/client/src/app/components/home-holdings/home-holdings.component.ts

@ -19,9 +19,10 @@ import {
ChangeDetectorRef, ChangeDetectorRef,
Component, Component,
CUSTOM_ELEMENTS_SCHEMA, CUSTOM_ELEMENTS_SCHEMA,
OnDestroy, DestroyRef,
OnInit OnInit
} from '@angular/core'; } from '@angular/core';
import { takeUntilDestroyed } from '@angular/core/rxjs-interop';
import { FormControl, FormsModule, ReactiveFormsModule } from '@angular/forms'; import { FormControl, FormsModule, ReactiveFormsModule } from '@angular/forms';
import { MatButtonModule } from '@angular/material/button'; import { MatButtonModule } from '@angular/material/button';
import { MatButtonToggleModule } from '@angular/material/button-toggle'; import { MatButtonToggleModule } from '@angular/material/button-toggle';
@ -30,8 +31,6 @@ import { IonIcon } from '@ionic/angular/standalone';
import { addIcons } from 'ionicons'; import { addIcons } from 'ionicons';
import { gridOutline, reorderFourOutline } from 'ionicons/icons'; import { gridOutline, reorderFourOutline } from 'ionicons/icons';
import { DeviceDetectorService } from 'ngx-device-detector'; import { DeviceDetectorService } from 'ngx-device-detector';
import { Subject } from 'rxjs';
import { takeUntil } from 'rxjs/operators';
@Component({ @Component({
imports: [ imports: [
@ -51,7 +50,7 @@ import { takeUntil } from 'rxjs/operators';
styleUrls: ['./home-holdings.scss'], styleUrls: ['./home-holdings.scss'],
templateUrl: './home-holdings.html' templateUrl: './home-holdings.html'
}) })
export class GfHomeHoldingsComponent implements OnDestroy, OnInit { export class GfHomeHoldingsComponent implements OnInit {
public static DEFAULT_HOLDINGS_VIEW_MODE: HoldingsViewMode = 'TABLE'; public static DEFAULT_HOLDINGS_VIEW_MODE: HoldingsViewMode = 'TABLE';
public deviceType: string; public deviceType: string;
@ -71,11 +70,10 @@ export class GfHomeHoldingsComponent implements OnDestroy, OnInit {
GfHomeHoldingsComponent.DEFAULT_HOLDINGS_VIEW_MODE GfHomeHoldingsComponent.DEFAULT_HOLDINGS_VIEW_MODE
); );
private unsubscribeSubject = new Subject<void>();
public constructor( public constructor(
private changeDetectorRef: ChangeDetectorRef, private changeDetectorRef: ChangeDetectorRef,
private dataService: DataService, private dataService: DataService,
private destroyRef: DestroyRef,
private deviceService: DeviceDetectorService, private deviceService: DeviceDetectorService,
private impersonationStorageService: ImpersonationStorageService, private impersonationStorageService: ImpersonationStorageService,
private router: Router, private router: Router,
@ -89,13 +87,13 @@ export class GfHomeHoldingsComponent implements OnDestroy, OnInit {
this.impersonationStorageService this.impersonationStorageService
.onChangeHasImpersonation() .onChangeHasImpersonation()
.pipe(takeUntil(this.unsubscribeSubject)) .pipe(takeUntilDestroyed(this.destroyRef))
.subscribe((impersonationId) => { .subscribe((impersonationId) => {
this.hasImpersonationId = !!impersonationId; this.hasImpersonationId = !!impersonationId;
}); });
this.userService.stateChanged this.userService.stateChanged
.pipe(takeUntil(this.unsubscribeSubject)) .pipe(takeUntilDestroyed(this.destroyRef))
.subscribe((state) => { .subscribe((state) => {
if (state?.user) { if (state?.user) {
this.user = state.user; this.user = state.user;
@ -117,15 +115,15 @@ export class GfHomeHoldingsComponent implements OnDestroy, OnInit {
}); });
this.viewModeFormControl.valueChanges this.viewModeFormControl.valueChanges
.pipe(takeUntil(this.unsubscribeSubject)) .pipe(takeUntilDestroyed(this.destroyRef))
.subscribe((holdingsViewMode) => { .subscribe((holdingsViewMode) => {
this.dataService this.dataService
.putUserSetting({ holdingsViewMode }) .putUserSetting({ holdingsViewMode })
.pipe(takeUntil(this.unsubscribeSubject)) .pipe(takeUntilDestroyed(this.destroyRef))
.subscribe(() => { .subscribe(() => {
this.userService this.userService
.get(true) .get(true)
.pipe(takeUntil(this.unsubscribeSubject)) .pipe(takeUntilDestroyed(this.destroyRef))
.subscribe((user) => { .subscribe((user) => {
this.user = user; this.user = user;
@ -149,11 +147,6 @@ export class GfHomeHoldingsComponent implements OnDestroy, OnInit {
} }
} }
public ngOnDestroy() {
this.unsubscribeSubject.next();
this.unsubscribeSubject.complete();
}
private fetchHoldings() { private fetchHoldings() {
const filters = this.userService.getFilters(); const filters = this.userService.getFilters();
@ -193,7 +186,7 @@ export class GfHomeHoldingsComponent implements OnDestroy, OnInit {
this.holdings = undefined; this.holdings = undefined;
this.fetchHoldings() this.fetchHoldings()
.pipe(takeUntil(this.unsubscribeSubject)) .pipe(takeUntilDestroyed(this.destroyRef))
.subscribe(({ holdings }) => { .subscribe(({ holdings }) => {
this.holdings = holdings; this.holdings = holdings;

21
apps/client/src/app/components/home-market/home-market.component.ts

@ -17,12 +17,11 @@ import {
ChangeDetectorRef, ChangeDetectorRef,
Component, Component,
CUSTOM_ELEMENTS_SCHEMA, CUSTOM_ELEMENTS_SCHEMA,
OnDestroy, DestroyRef,
OnInit OnInit
} from '@angular/core'; } from '@angular/core';
import { takeUntilDestroyed } from '@angular/core/rxjs-interop';
import { DeviceDetectorService } from 'ngx-device-detector'; import { DeviceDetectorService } from 'ngx-device-detector';
import { Subject } from 'rxjs';
import { takeUntil } from 'rxjs/operators';
@Component({ @Component({
imports: [ imports: [
@ -35,7 +34,7 @@ import { takeUntil } from 'rxjs/operators';
styleUrls: ['./home-market.scss'], styleUrls: ['./home-market.scss'],
templateUrl: './home-market.html' templateUrl: './home-market.html'
}) })
export class GfHomeMarketComponent implements OnDestroy, OnInit { export class GfHomeMarketComponent implements OnInit {
public benchmarks: Benchmark[]; public benchmarks: Benchmark[];
public deviceType: string; public deviceType: string;
public fearAndGreedIndex: number; public fearAndGreedIndex: number;
@ -47,11 +46,10 @@ export class GfHomeMarketComponent implements OnDestroy, OnInit {
public readonly numberOfDays = 365; public readonly numberOfDays = 365;
public user: User; public user: User;
private unsubscribeSubject = new Subject<void>();
public constructor( public constructor(
private changeDetectorRef: ChangeDetectorRef, private changeDetectorRef: ChangeDetectorRef,
private dataService: DataService, private dataService: DataService,
private destroyRef: DestroyRef,
private deviceService: DeviceDetectorService, private deviceService: DeviceDetectorService,
private userService: UserService private userService: UserService
) { ) {
@ -59,7 +57,7 @@ export class GfHomeMarketComponent implements OnDestroy, OnInit {
this.info = this.dataService.fetchInfo(); this.info = this.dataService.fetchInfo();
this.userService.stateChanged this.userService.stateChanged
.pipe(takeUntil(this.unsubscribeSubject)) .pipe(takeUntilDestroyed(this.destroyRef))
.subscribe((state) => { .subscribe((state) => {
if (state?.user) { if (state?.user) {
this.user = state.user; this.user = state.user;
@ -82,7 +80,7 @@ export class GfHomeMarketComponent implements OnDestroy, OnInit {
includeHistoricalData: this.numberOfDays, includeHistoricalData: this.numberOfDays,
symbol: ghostfolioFearAndGreedIndexSymbol symbol: ghostfolioFearAndGreedIndexSymbol
}) })
.pipe(takeUntil(this.unsubscribeSubject)) .pipe(takeUntilDestroyed(this.destroyRef))
.subscribe(({ historicalData, marketPrice }) => { .subscribe(({ historicalData, marketPrice }) => {
this.fearAndGreedIndex = marketPrice; this.fearAndGreedIndex = marketPrice;
this.historicalDataItems = [ this.historicalDataItems = [
@ -99,16 +97,11 @@ export class GfHomeMarketComponent implements OnDestroy, OnInit {
this.dataService this.dataService
.fetchBenchmarks() .fetchBenchmarks()
.pipe(takeUntil(this.unsubscribeSubject)) .pipe(takeUntilDestroyed(this.destroyRef))
.subscribe(({ benchmarks }) => { .subscribe(({ benchmarks }) => {
this.benchmarks = benchmarks; this.benchmarks = benchmarks;
this.changeDetectorRef.markForCheck(); this.changeDetectorRef.markForCheck();
}); });
} }
public ngOnDestroy() {
this.unsubscribeSubject.next();
this.unsubscribeSubject.complete();
}
} }

23
apps/client/src/app/components/home-overview/home-overview.component.ts

@ -19,14 +19,13 @@ import {
ChangeDetectorRef, ChangeDetectorRef,
Component, Component,
CUSTOM_ELEMENTS_SCHEMA, CUSTOM_ELEMENTS_SCHEMA,
OnDestroy, DestroyRef,
OnInit OnInit
} from '@angular/core'; } from '@angular/core';
import { takeUntilDestroyed } from '@angular/core/rxjs-interop';
import { MatButtonModule } from '@angular/material/button'; import { MatButtonModule } from '@angular/material/button';
import { RouterModule } from '@angular/router'; import { RouterModule } from '@angular/router';
import { DeviceDetectorService } from 'ngx-device-detector'; import { DeviceDetectorService } from 'ngx-device-detector';
import { Subject } from 'rxjs';
import { takeUntil } from 'rxjs/operators';
@Component({ @Component({
imports: [ imports: [
@ -41,7 +40,7 @@ import { takeUntil } from 'rxjs/operators';
styleUrls: ['./home-overview.scss'], styleUrls: ['./home-overview.scss'],
templateUrl: './home-overview.html' templateUrl: './home-overview.html'
}) })
export class GfHomeOverviewComponent implements OnDestroy, OnInit { export class GfHomeOverviewComponent implements OnInit {
public deviceType: string; public deviceType: string;
public errors: AssetProfileIdentifier[]; public errors: AssetProfileIdentifier[];
public hasError: boolean; public hasError: boolean;
@ -62,18 +61,17 @@ export class GfHomeOverviewComponent implements OnDestroy, OnInit {
public unit: string; public unit: string;
public user: User; public user: User;
private unsubscribeSubject = new Subject<void>();
public constructor( public constructor(
private changeDetectorRef: ChangeDetectorRef, private changeDetectorRef: ChangeDetectorRef,
private dataService: DataService, private dataService: DataService,
private destroyRef: DestroyRef,
private deviceService: DeviceDetectorService, private deviceService: DeviceDetectorService,
private impersonationStorageService: ImpersonationStorageService, private impersonationStorageService: ImpersonationStorageService,
private layoutService: LayoutService, private layoutService: LayoutService,
private userService: UserService private userService: UserService
) { ) {
this.userService.stateChanged this.userService.stateChanged
.pipe(takeUntil(this.unsubscribeSubject)) .pipe(takeUntilDestroyed(this.destroyRef))
.subscribe((state) => { .subscribe((state) => {
if (state?.user) { if (state?.user) {
this.user = state.user; this.user = state.user;
@ -99,7 +97,7 @@ export class GfHomeOverviewComponent implements OnDestroy, OnInit {
this.impersonationStorageService this.impersonationStorageService
.onChangeHasImpersonation() .onChangeHasImpersonation()
.pipe(takeUntil(this.unsubscribeSubject)) .pipe(takeUntilDestroyed(this.destroyRef))
.subscribe((impersonationId) => { .subscribe((impersonationId) => {
this.hasImpersonationId = !!impersonationId; this.hasImpersonationId = !!impersonationId;
@ -107,17 +105,12 @@ export class GfHomeOverviewComponent implements OnDestroy, OnInit {
}); });
this.layoutService.shouldReloadContent$ this.layoutService.shouldReloadContent$
.pipe(takeUntil(this.unsubscribeSubject)) .pipe(takeUntilDestroyed(this.destroyRef))
.subscribe(() => { .subscribe(() => {
this.update(); this.update();
}); });
} }
public ngOnDestroy() {
this.unsubscribeSubject.next();
this.unsubscribeSubject.complete();
}
private update() { private update() {
this.historicalDataItems = null; this.historicalDataItems = null;
this.isLoadingPerformance = true; this.isLoadingPerformance = true;
@ -126,7 +119,7 @@ export class GfHomeOverviewComponent implements OnDestroy, OnInit {
.fetchPortfolioPerformance({ .fetchPortfolioPerformance({
range: this.user?.settings?.dateRange range: this.user?.settings?.dateRange
}) })
.pipe(takeUntil(this.unsubscribeSubject)) .pipe(takeUntilDestroyed(this.destroyRef))
.subscribe(({ chart, errors, performance }) => { .subscribe(({ chart, errors, performance }) => {
this.errors = errors; this.errors = errors;
this.performance = performance; this.performance = performance;

25
apps/client/src/app/components/home-summary/home-summary.component.ts

@ -13,14 +13,13 @@ import {
ChangeDetectorRef, ChangeDetectorRef,
Component, Component,
CUSTOM_ELEMENTS_SCHEMA, CUSTOM_ELEMENTS_SCHEMA,
OnDestroy, DestroyRef,
OnInit OnInit
} from '@angular/core'; } from '@angular/core';
import { takeUntilDestroyed } from '@angular/core/rxjs-interop';
import { MatCardModule } from '@angular/material/card'; import { MatCardModule } from '@angular/material/card';
import { MatSnackBarRef, TextOnlySnackBar } from '@angular/material/snack-bar'; import { MatSnackBarRef, TextOnlySnackBar } from '@angular/material/snack-bar';
import { DeviceDetectorService } from 'ngx-device-detector'; import { DeviceDetectorService } from 'ngx-device-detector';
import { Subject } from 'rxjs';
import { takeUntil } from 'rxjs/operators';
@Component({ @Component({
imports: [GfPortfolioSummaryComponent, MatCardModule], imports: [GfPortfolioSummaryComponent, MatCardModule],
@ -29,7 +28,7 @@ import { takeUntil } from 'rxjs/operators';
styleUrls: ['./home-summary.scss'], styleUrls: ['./home-summary.scss'],
templateUrl: './home-summary.html' templateUrl: './home-summary.html'
}) })
export class GfHomeSummaryComponent implements OnDestroy, OnInit { export class GfHomeSummaryComponent implements OnInit {
public deviceType: string; public deviceType: string;
public hasImpersonationId: boolean; public hasImpersonationId: boolean;
public hasPermissionForSubscription: boolean; public hasPermissionForSubscription: boolean;
@ -40,11 +39,10 @@ export class GfHomeSummaryComponent implements OnDestroy, OnInit {
public summary: PortfolioSummary; public summary: PortfolioSummary;
public user: User; public user: User;
private unsubscribeSubject = new Subject<void>();
public constructor( public constructor(
private changeDetectorRef: ChangeDetectorRef, private changeDetectorRef: ChangeDetectorRef,
private dataService: DataService, private dataService: DataService,
private destroyRef: DestroyRef,
private deviceService: DeviceDetectorService, private deviceService: DeviceDetectorService,
private impersonationStorageService: ImpersonationStorageService, private impersonationStorageService: ImpersonationStorageService,
private userService: UserService private userService: UserService
@ -57,7 +55,7 @@ export class GfHomeSummaryComponent implements OnDestroy, OnInit {
); );
this.userService.stateChanged this.userService.stateChanged
.pipe(takeUntil(this.unsubscribeSubject)) .pipe(takeUntilDestroyed(this.destroyRef))
.subscribe((state) => { .subscribe((state) => {
if (state?.user) { if (state?.user) {
this.user = state.user; this.user = state.user;
@ -77,7 +75,7 @@ export class GfHomeSummaryComponent implements OnDestroy, OnInit {
this.impersonationStorageService this.impersonationStorageService
.onChangeHasImpersonation() .onChangeHasImpersonation()
.pipe(takeUntil(this.unsubscribeSubject)) .pipe(takeUntilDestroyed(this.destroyRef))
.subscribe((impersonationId) => { .subscribe((impersonationId) => {
this.hasImpersonationId = !!impersonationId; this.hasImpersonationId = !!impersonationId;
}); });
@ -86,11 +84,11 @@ export class GfHomeSummaryComponent implements OnDestroy, OnInit {
public onChangeEmergencyFund(emergencyFund: number) { public onChangeEmergencyFund(emergencyFund: number) {
this.dataService this.dataService
.putUserSetting({ emergencyFund }) .putUserSetting({ emergencyFund })
.pipe(takeUntil(this.unsubscribeSubject)) .pipe(takeUntilDestroyed(this.destroyRef))
.subscribe(() => { .subscribe(() => {
this.userService this.userService
.get(true) .get(true)
.pipe(takeUntil(this.unsubscribeSubject)) .pipe(takeUntilDestroyed(this.destroyRef))
.subscribe((user) => { .subscribe((user) => {
this.user = user; this.user = user;
@ -99,17 +97,12 @@ export class GfHomeSummaryComponent implements OnDestroy, OnInit {
}); });
} }
public ngOnDestroy() {
this.unsubscribeSubject.next();
this.unsubscribeSubject.complete();
}
private update() { private update() {
this.isLoading = true; this.isLoading = true;
this.dataService this.dataService
.fetchPortfolioDetails() .fetchPortfolioDetails()
.pipe(takeUntil(this.unsubscribeSubject)) .pipe(takeUntilDestroyed(this.destroyRef))
.subscribe(({ summary }) => { .subscribe(({ summary }) => {
this.summary = summary; this.summary = summary;
this.isLoading = false; this.isLoading = false;

5
apps/client/src/app/components/user-account-access/user-account-access.component.ts

@ -1,5 +1,4 @@
import { GfAccessTableComponent } from '@ghostfolio/client/components/access-table/access-table.component'; import { GfAccessTableComponent } from '@ghostfolio/client/components/access-table/access-table.component';
import { TokenStorageService } from '@ghostfolio/client/services/token-storage.service';
import { UserService } from '@ghostfolio/client/services/user/user.service'; import { UserService } from '@ghostfolio/client/services/user/user.service';
import { CreateAccessDto } from '@ghostfolio/common/dtos'; import { CreateAccessDto } from '@ghostfolio/common/dtos';
import { ConfirmationDialogType } from '@ghostfolio/common/enums'; import { ConfirmationDialogType } from '@ghostfolio/common/enums';
@ -76,7 +75,6 @@ export class GfUserAccountAccessComponent implements OnDestroy, OnInit {
private notificationService: NotificationService, private notificationService: NotificationService,
private route: ActivatedRoute, private route: ActivatedRoute,
private router: Router, private router: Router,
private tokenStorageService: TokenStorageService,
private userService: UserService private userService: UserService
) { ) {
const { globalPermissions } = this.dataService.fetchInfo(); const { globalPermissions } = this.dataService.fetchInfo();
@ -161,8 +159,7 @@ export class GfUserAccountAccessComponent implements OnDestroy, OnInit {
.subscribe(({ accessToken }) => { .subscribe(({ accessToken }) => {
this.notificationService.alert({ this.notificationService.alert({
discardFn: () => { discardFn: () => {
this.tokenStorageService.signOut(); this.userService.signOut();
this.userService.remove();
document.location.href = `/${document.documentElement.lang}`; document.location.href = `/${document.documentElement.lang}`;
}, },

5
apps/client/src/app/components/user-account-settings/user-account-settings.component.ts

@ -3,7 +3,6 @@ import {
KEY_TOKEN, KEY_TOKEN,
SettingsStorageService SettingsStorageService
} from '@ghostfolio/client/services/settings-storage.service'; } from '@ghostfolio/client/services/settings-storage.service';
import { TokenStorageService } from '@ghostfolio/client/services/token-storage.service';
import { UserService } from '@ghostfolio/client/services/user/user.service'; import { UserService } from '@ghostfolio/client/services/user/user.service';
import { WebAuthnService } from '@ghostfolio/client/services/web-authn.service'; import { WebAuthnService } from '@ghostfolio/client/services/web-authn.service';
import { ConfirmationDialogType } from '@ghostfolio/common/enums'; import { ConfirmationDialogType } from '@ghostfolio/common/enums';
@ -108,7 +107,6 @@ export class GfUserAccountSettingsComponent implements OnDestroy, OnInit {
private notificationService: NotificationService, private notificationService: NotificationService,
private settingsStorageService: SettingsStorageService, private settingsStorageService: SettingsStorageService,
private snackBar: MatSnackBar, private snackBar: MatSnackBar,
private tokenStorageService: TokenStorageService,
private userService: UserService, private userService: UserService,
public webAuthnService: WebAuthnService public webAuthnService: WebAuthnService
) { ) {
@ -198,8 +196,7 @@ export class GfUserAccountSettingsComponent implements OnDestroy, OnInit {
takeUntil(this.unsubscribeSubject) takeUntil(this.unsubscribeSubject)
) )
.subscribe(() => { .subscribe(() => {
this.tokenStorageService.signOut(); this.userService.signOut();
this.userService.remove();
document.location.href = `/${document.documentElement.lang}`; document.location.href = `/${document.documentElement.lang}`;
}); });

2
apps/client/src/app/core/auth.guard.ts

@ -68,7 +68,7 @@ export class AuthGuard {
this.dataService this.dataService
.putUserSetting({ language: document.documentElement.lang }) .putUserSetting({ language: document.documentElement.lang })
.subscribe(() => { .subscribe(() => {
this.userService.remove(); this.userService.reset();
setTimeout(() => { setTimeout(() => {
window.location.reload(); window.location.reload();

6
apps/client/src/app/core/http-response.interceptor.ts

@ -1,4 +1,4 @@
import { TokenStorageService } from '@ghostfolio/client/services/token-storage.service'; import { UserService } from '@ghostfolio/client/services/user/user.service';
import { WebAuthnService } from '@ghostfolio/client/services/web-authn.service'; import { WebAuthnService } from '@ghostfolio/client/services/web-authn.service';
import { InfoItem } from '@ghostfolio/common/interfaces'; import { InfoItem } from '@ghostfolio/common/interfaces';
import { internalRoutes, publicRoutes } from '@ghostfolio/common/routes/routes'; import { internalRoutes, publicRoutes } from '@ghostfolio/common/routes/routes';
@ -32,8 +32,8 @@ export class HttpResponseInterceptor implements HttpInterceptor {
public constructor( public constructor(
private dataService: DataService, private dataService: DataService,
private router: Router, private router: Router,
private tokenStorageService: TokenStorageService,
private snackBar: MatSnackBar, private snackBar: MatSnackBar,
private userService: UserService,
private webAuthnService: WebAuthnService private webAuthnService: WebAuthnService
) { ) {
this.info = this.dataService.fetchInfo(); this.info = this.dataService.fetchInfo();
@ -115,7 +115,7 @@ export class HttpResponseInterceptor implements HttpInterceptor {
if (this.webAuthnService.isEnabled()) { if (this.webAuthnService.isEnabled()) {
this.router.navigate(internalRoutes.webauthn.routerLink); this.router.navigate(internalRoutes.webauthn.routerLink);
} else { } else {
this.tokenStorageService.signOut(); this.userService.signOut();
} }
} }
} }

17
apps/client/src/app/pages/about/about-page.component.ts

@ -8,9 +8,10 @@ import {
ChangeDetectorRef, ChangeDetectorRef,
Component, Component,
CUSTOM_ELEMENTS_SCHEMA, CUSTOM_ELEMENTS_SCHEMA,
OnDestroy, DestroyRef,
OnInit OnInit
} from '@angular/core'; } from '@angular/core';
import { takeUntilDestroyed } from '@angular/core/rxjs-interop';
import { MatTabsModule } from '@angular/material/tabs'; import { MatTabsModule } from '@angular/material/tabs';
import { RouterModule } from '@angular/router'; import { RouterModule } from '@angular/router';
import { IonIcon } from '@ionic/angular/standalone'; import { IonIcon } from '@ionic/angular/standalone';
@ -24,8 +25,6 @@ import {
sparklesOutline sparklesOutline
} from 'ionicons/icons'; } from 'ionicons/icons';
import { DeviceDetectorService } from 'ngx-device-detector'; import { DeviceDetectorService } from 'ngx-device-detector';
import { Subject } from 'rxjs';
import { takeUntil } from 'rxjs/operators';
@Component({ @Component({
host: { class: 'page has-tabs' }, host: { class: 'page has-tabs' },
@ -35,17 +34,16 @@ import { takeUntil } from 'rxjs/operators';
styleUrls: ['./about-page.scss'], styleUrls: ['./about-page.scss'],
templateUrl: './about-page.html' templateUrl: './about-page.html'
}) })
export class AboutPageComponent implements OnDestroy, OnInit { export class AboutPageComponent implements OnInit {
public deviceType: string; public deviceType: string;
public hasPermissionForSubscription: boolean; public hasPermissionForSubscription: boolean;
public tabs: TabConfiguration[] = []; public tabs: TabConfiguration[] = [];
public user: User; public user: User;
private unsubscribeSubject = new Subject<void>();
public constructor( public constructor(
private changeDetectorRef: ChangeDetectorRef, private changeDetectorRef: ChangeDetectorRef,
private dataService: DataService, private dataService: DataService,
private destroyRef: DestroyRef,
private deviceService: DeviceDetectorService, private deviceService: DeviceDetectorService,
private userService: UserService private userService: UserService
) { ) {
@ -57,7 +55,7 @@ export class AboutPageComponent implements OnDestroy, OnInit {
); );
this.userService.stateChanged this.userService.stateChanged
.pipe(takeUntil(this.unsubscribeSubject)) .pipe(takeUntilDestroyed(this.destroyRef))
.subscribe((state) => { .subscribe((state) => {
this.tabs = [ this.tabs = [
{ {
@ -118,9 +116,4 @@ export class AboutPageComponent implements OnDestroy, OnInit {
public ngOnInit() { public ngOnInit() {
this.deviceType = this.deviceService.getDeviceInfo().deviceType; this.deviceType = this.deviceService.getDeviceInfo().deviceType;
} }
public ngOnDestroy() {
this.unsubscribeSubject.next();
this.unsubscribeSubject.complete();
}
} }

52
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 { NotificationService } from '@ghostfolio/ui/notifications';
import { DataService } from '@ghostfolio/ui/services'; 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 { MatButtonModule } from '@angular/material/button';
import { MatDialog } from '@angular/material/dialog'; import { MatDialog } from '@angular/material/dialog';
import { ActivatedRoute, Router, RouterModule } from '@angular/router'; import { ActivatedRoute, Router, RouterModule } from '@angular/router';
@ -21,8 +27,8 @@ import { Account as AccountModel } from '@prisma/client';
import { addIcons } from 'ionicons'; import { addIcons } from 'ionicons';
import { addOutline } from 'ionicons/icons'; import { addOutline } from 'ionicons/icons';
import { DeviceDetectorService } from 'ngx-device-detector'; import { DeviceDetectorService } from 'ngx-device-detector';
import { EMPTY, Subject, Subscription } from 'rxjs'; import { EMPTY, Subscription } from 'rxjs';
import { catchError, takeUntil } from 'rxjs/operators'; import { catchError } from 'rxjs/operators';
import { GfCreateOrUpdateAccountDialogComponent } from './create-or-update-account-dialog/create-or-update-account-dialog.component'; import { GfCreateOrUpdateAccountDialogComponent } from './create-or-update-account-dialog/create-or-update-account-dialog.component';
import { CreateOrUpdateAccountDialogParams } from './create-or-update-account-dialog/interfaces/interfaces'; 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'], styleUrls: ['./accounts-page.scss'],
templateUrl: './accounts-page.html' templateUrl: './accounts-page.html'
}) })
export class GfAccountsPageComponent implements OnDestroy, OnInit { export class GfAccountsPageComponent implements OnInit {
public accounts: AccountModel[]; public accounts: AccountModel[];
public activitiesCount = 0; public activitiesCount = 0;
public deviceType: string; public deviceType: string;
@ -48,11 +54,10 @@ export class GfAccountsPageComponent implements OnDestroy, OnInit {
public totalValueInBaseCurrency = 0; public totalValueInBaseCurrency = 0;
public user: User; public user: User;
private unsubscribeSubject = new Subject<void>();
public constructor( public constructor(
private changeDetectorRef: ChangeDetectorRef, private changeDetectorRef: ChangeDetectorRef,
private dataService: DataService, private dataService: DataService,
private destroyRef: DestroyRef,
private deviceService: DeviceDetectorService, private deviceService: DeviceDetectorService,
private dialog: MatDialog, private dialog: MatDialog,
private impersonationStorageService: ImpersonationStorageService, private impersonationStorageService: ImpersonationStorageService,
@ -62,7 +67,7 @@ export class GfAccountsPageComponent implements OnDestroy, OnInit {
private userService: UserService private userService: UserService
) { ) {
this.route.queryParams this.route.queryParams
.pipe(takeUntil(this.unsubscribeSubject)) .pipe(takeUntilDestroyed(this.destroyRef))
.subscribe((params) => { .subscribe((params) => {
if (params['accountId'] && params['accountDetailDialog']) { if (params['accountId'] && params['accountDetailDialog']) {
this.openAccountDetailDialog(params['accountId']); this.openAccountDetailDialog(params['accountId']);
@ -94,13 +99,13 @@ export class GfAccountsPageComponent implements OnDestroy, OnInit {
this.impersonationStorageService this.impersonationStorageService
.onChangeHasImpersonation() .onChangeHasImpersonation()
.pipe(takeUntil(this.unsubscribeSubject)) .pipe(takeUntilDestroyed(this.destroyRef))
.subscribe((impersonationId) => { .subscribe((impersonationId) => {
this.hasImpersonationId = !!impersonationId; this.hasImpersonationId = !!impersonationId;
}); });
this.userService.stateChanged this.userService.stateChanged
.pipe(takeUntil(this.unsubscribeSubject)) .pipe(takeUntilDestroyed(this.destroyRef))
.subscribe((state) => { .subscribe((state) => {
if (state?.user) { if (state?.user) {
this.user = state.user; this.user = state.user;
@ -124,7 +129,7 @@ export class GfAccountsPageComponent implements OnDestroy, OnInit {
public fetchAccounts() { public fetchAccounts() {
this.dataService this.dataService
.fetchAccounts() .fetchAccounts()
.pipe(takeUntil(this.unsubscribeSubject)) .pipe(takeUntilDestroyed(this.destroyRef))
.subscribe( .subscribe(
({ ({
accounts, accounts,
@ -151,11 +156,11 @@ export class GfAccountsPageComponent implements OnDestroy, OnInit {
this.dataService this.dataService
.deleteAccount(aId) .deleteAccount(aId)
.pipe(takeUntil(this.unsubscribeSubject)) .pipe(takeUntilDestroyed(this.destroyRef))
.subscribe(() => { .subscribe(() => {
this.userService this.userService
.get(true) .get(true)
.pipe(takeUntil(this.unsubscribeSubject)) .pipe(takeUntilDestroyed(this.destroyRef))
.subscribe(); .subscribe();
this.fetchAccounts(); this.fetchAccounts();
@ -204,18 +209,18 @@ export class GfAccountsPageComponent implements OnDestroy, OnInit {
dialogRef dialogRef
.afterClosed() .afterClosed()
.pipe(takeUntil(this.unsubscribeSubject)) .pipe(takeUntilDestroyed(this.destroyRef))
.subscribe((account: UpdateAccountDto | null) => { .subscribe((account: UpdateAccountDto | null) => {
if (account) { if (account) {
this.reset(); this.reset();
this.dataService this.dataService
.putAccount(account) .putAccount(account)
.pipe(takeUntil(this.unsubscribeSubject)) .pipe(takeUntilDestroyed(this.destroyRef))
.subscribe(() => { .subscribe(() => {
this.userService this.userService
.get(true) .get(true)
.pipe(takeUntil(this.unsubscribeSubject)) .pipe(takeUntilDestroyed(this.destroyRef))
.subscribe(); .subscribe();
this.fetchAccounts(); this.fetchAccounts();
@ -228,11 +233,6 @@ export class GfAccountsPageComponent implements OnDestroy, OnInit {
}); });
} }
public ngOnDestroy() {
this.unsubscribeSubject.next();
this.unsubscribeSubject.complete();
}
private openAccountDetailDialog(aAccountId: string) { private openAccountDetailDialog(aAccountId: string) {
const dialogRef = this.dialog.open< const dialogRef = this.dialog.open<
GfAccountDetailDialogComponent, GfAccountDetailDialogComponent,
@ -254,7 +254,7 @@ export class GfAccountsPageComponent implements OnDestroy, OnInit {
dialogRef dialogRef
.afterClosed() .afterClosed()
.pipe(takeUntil(this.unsubscribeSubject)) .pipe(takeUntilDestroyed(this.destroyRef))
.subscribe(() => { .subscribe(() => {
this.fetchAccounts(); this.fetchAccounts();
@ -284,18 +284,18 @@ export class GfAccountsPageComponent implements OnDestroy, OnInit {
dialogRef dialogRef
.afterClosed() .afterClosed()
.pipe(takeUntil(this.unsubscribeSubject)) .pipe(takeUntilDestroyed(this.destroyRef))
.subscribe((account: CreateAccountDto | null) => { .subscribe((account: CreateAccountDto | null) => {
if (account) { if (account) {
this.reset(); this.reset();
this.dataService this.dataService
.postAccount(account) .postAccount(account)
.pipe(takeUntil(this.unsubscribeSubject)) .pipe(takeUntilDestroyed(this.destroyRef))
.subscribe(() => { .subscribe(() => {
this.userService this.userService
.get(true) .get(true)
.pipe(takeUntil(this.unsubscribeSubject)) .pipe(takeUntilDestroyed(this.destroyRef))
.subscribe(); .subscribe();
this.fetchAccounts(); this.fetchAccounts();
@ -321,7 +321,7 @@ export class GfAccountsPageComponent implements OnDestroy, OnInit {
dialogRef dialogRef
.afterClosed() .afterClosed()
.pipe(takeUntil(this.unsubscribeSubject)) .pipe(takeUntilDestroyed(this.destroyRef))
.subscribe((data: any) => { .subscribe((data: any) => {
if (data) { if (data) {
this.reset(); this.reset();
@ -343,7 +343,7 @@ export class GfAccountsPageComponent implements OnDestroy, OnInit {
return EMPTY; return EMPTY;
}), }),
takeUntil(this.unsubscribeSubject) takeUntilDestroyed(this.destroyRef)
) )
.subscribe(() => { .subscribe(() => {
this.fetchAccounts(); this.fetchAccounts();

37
apps/client/src/app/pages/portfolio/analysis/analysis-page.component.ts

@ -24,10 +24,11 @@ import { Clipboard } from '@angular/cdk/clipboard';
import { import {
ChangeDetectorRef, ChangeDetectorRef,
Component, Component,
OnDestroy, DestroyRef,
OnInit, OnInit,
ViewChild ViewChild
} from '@angular/core'; } from '@angular/core';
import { takeUntilDestroyed } from '@angular/core/rxjs-interop';
import { MatButtonModule } from '@angular/material/button'; import { MatButtonModule } from '@angular/material/button';
import { MatCardModule } from '@angular/material/card'; import { MatCardModule } from '@angular/material/card';
import { MatMenuModule, MatMenuTrigger } from '@angular/material/menu'; import { MatMenuModule, MatMenuTrigger } from '@angular/material/menu';
@ -42,8 +43,6 @@ import { isNumber, sortBy } from 'lodash';
import ms from 'ms'; import ms from 'ms';
import { DeviceDetectorService } from 'ngx-device-detector'; import { DeviceDetectorService } from 'ngx-device-detector';
import { NgxSkeletonLoaderModule } from 'ngx-skeleton-loader'; import { NgxSkeletonLoaderModule } from 'ngx-skeleton-loader';
import { Subject } from 'rxjs';
import { takeUntil } from 'rxjs/operators';
@Component({ @Component({
imports: [ imports: [
@ -64,7 +63,7 @@ import { takeUntil } from 'rxjs/operators';
styleUrls: ['./analysis-page.scss'], styleUrls: ['./analysis-page.scss'],
templateUrl: './analysis-page.html' templateUrl: './analysis-page.html'
}) })
export class GfAnalysisPageComponent implements OnDestroy, OnInit { export class GfAnalysisPageComponent implements OnInit {
@ViewChild(MatMenuTrigger) actionsMenuButton!: MatMenuTrigger; @ViewChild(MatMenuTrigger) actionsMenuButton!: MatMenuTrigger;
public benchmark: Partial<SymbolProfile>; public benchmark: Partial<SymbolProfile>;
@ -102,12 +101,11 @@ export class GfAnalysisPageComponent implements OnDestroy, OnInit {
public unitLongestStreak: string; public unitLongestStreak: string;
public user: User; public user: User;
private unsubscribeSubject = new Subject<void>();
public constructor( public constructor(
private changeDetectorRef: ChangeDetectorRef, private changeDetectorRef: ChangeDetectorRef,
private clipboard: Clipboard, private clipboard: Clipboard,
private dataService: DataService, private dataService: DataService,
private destroyRef: DestroyRef,
private deviceService: DeviceDetectorService, private deviceService: DeviceDetectorService,
private impersonationStorageService: ImpersonationStorageService, private impersonationStorageService: ImpersonationStorageService,
private snackBar: MatSnackBar, private snackBar: MatSnackBar,
@ -135,13 +133,13 @@ export class GfAnalysisPageComponent implements OnDestroy, OnInit {
this.impersonationStorageService this.impersonationStorageService
.onChangeHasImpersonation() .onChangeHasImpersonation()
.pipe(takeUntil(this.unsubscribeSubject)) .pipe(takeUntilDestroyed(this.destroyRef))
.subscribe((impersonationId) => { .subscribe((impersonationId) => {
this.hasImpersonationId = !!impersonationId; this.hasImpersonationId = !!impersonationId;
}); });
this.userService.stateChanged this.userService.stateChanged
.pipe(takeUntil(this.unsubscribeSubject)) .pipe(takeUntilDestroyed(this.destroyRef))
.subscribe((state) => { .subscribe((state) => {
if (state?.user) { if (state?.user) {
this.user = state.user; this.user = state.user;
@ -163,11 +161,11 @@ export class GfAnalysisPageComponent implements OnDestroy, OnInit {
public onChangeBenchmark(symbolProfileId: string) { public onChangeBenchmark(symbolProfileId: string) {
this.dataService this.dataService
.putUserSetting({ benchmark: symbolProfileId }) .putUserSetting({ benchmark: symbolProfileId })
.pipe(takeUntil(this.unsubscribeSubject)) .pipe(takeUntilDestroyed(this.destroyRef))
.subscribe(() => { .subscribe(() => {
this.userService this.userService
.get(true) .get(true)
.pipe(takeUntil(this.unsubscribeSubject)) .pipe(takeUntilDestroyed(this.destroyRef))
.subscribe((user) => { .subscribe((user) => {
this.user = user; this.user = user;
@ -193,7 +191,7 @@ export class GfAnalysisPageComponent implements OnDestroy, OnInit {
mode, mode,
filters: this.userService.getFilters() filters: this.userService.getFilters()
}) })
.pipe(takeUntil(this.unsubscribeSubject)) .pipe(takeUntilDestroyed(this.destroyRef))
.subscribe(({ prompt }) => { .subscribe(({ prompt }) => {
this.clipboard.copy(prompt); this.clipboard.copy(prompt);
@ -207,7 +205,7 @@ export class GfAnalysisPageComponent implements OnDestroy, OnInit {
snackBarRef snackBarRef
.onAction() .onAction()
.pipe(takeUntil(this.unsubscribeSubject)) .pipe(takeUntilDestroyed(this.destroyRef))
.subscribe(() => { .subscribe(() => {
window.open('https://duck.ai', '_blank'); window.open('https://duck.ai', '_blank');
}); });
@ -222,11 +220,6 @@ export class GfAnalysisPageComponent implements OnDestroy, OnInit {
}); });
} }
public ngOnDestroy() {
this.unsubscribeSubject.next();
this.unsubscribeSubject.complete();
}
private fetchDividendsAndInvestments() { private fetchDividendsAndInvestments() {
this.isLoadingDividendTimelineChart = true; this.isLoadingDividendTimelineChart = true;
this.isLoadingInvestmentTimelineChart = true; this.isLoadingInvestmentTimelineChart = true;
@ -237,7 +230,7 @@ export class GfAnalysisPageComponent implements OnDestroy, OnInit {
groupBy: this.mode, groupBy: this.mode,
range: this.user?.settings?.dateRange range: this.user?.settings?.dateRange
}) })
.pipe(takeUntil(this.unsubscribeSubject)) .pipe(takeUntilDestroyed(this.destroyRef))
.subscribe(({ dividends }) => { .subscribe(({ dividends }) => {
this.dividendsByGroup = dividends; this.dividendsByGroup = dividends;
@ -252,7 +245,7 @@ export class GfAnalysisPageComponent implements OnDestroy, OnInit {
groupBy: this.mode, groupBy: this.mode,
range: this.user?.settings?.dateRange range: this.user?.settings?.dateRange
}) })
.pipe(takeUntil(this.unsubscribeSubject)) .pipe(takeUntilDestroyed(this.destroyRef))
.subscribe(({ investments, streaks }) => { .subscribe(({ investments, streaks }) => {
this.investmentsByGroup = investments; this.investmentsByGroup = investments;
this.streaks = streaks; this.streaks = streaks;
@ -287,7 +280,7 @@ export class GfAnalysisPageComponent implements OnDestroy, OnInit {
filters: this.userService.getFilters(), filters: this.userService.getFilters(),
range: this.user?.settings?.dateRange range: this.user?.settings?.dateRange
}) })
.pipe(takeUntil(this.unsubscribeSubject)) .pipe(takeUntilDestroyed(this.destroyRef))
.subscribe(({ chart, firstOrderDate, performance }) => { .subscribe(({ chart, firstOrderDate, performance }) => {
this.firstOrderDate = firstOrderDate ?? new Date(); this.firstOrderDate = firstOrderDate ?? new Date();
@ -346,7 +339,7 @@ export class GfAnalysisPageComponent implements OnDestroy, OnInit {
filters: this.userService.getFilters(), filters: this.userService.getFilters(),
range: this.user?.settings?.dateRange range: this.user?.settings?.dateRange
}) })
.pipe(takeUntil(this.unsubscribeSubject)) .pipe(takeUntilDestroyed(this.destroyRef))
.subscribe(({ holdings }) => { .subscribe(({ holdings }) => {
const holdingsSorted = sortBy( const holdingsSorted = sortBy(
holdings.filter(({ netPerformancePercentWithCurrencyEffect }) => { holdings.filter(({ netPerformancePercentWithCurrencyEffect }) => {
@ -397,7 +390,7 @@ export class GfAnalysisPageComponent implements OnDestroy, OnInit {
range: this.user?.settings?.dateRange, range: this.user?.settings?.dateRange,
startDate: this.firstOrderDate startDate: this.firstOrderDate
}) })
.pipe(takeUntil(this.unsubscribeSubject)) .pipe(takeUntilDestroyed(this.destroyRef))
.subscribe(({ marketData }) => { .subscribe(({ marketData }) => {
this.benchmarkDataItems = marketData.map(({ date, value }) => { this.benchmarkDataItems = marketData.map(({ date, value }) => {
return { return {

48
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 { GfValueComponent } from '@ghostfolio/ui/value';
import { CommonModule, NgStyle } from '@angular/common'; 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 { FormsModule, ReactiveFormsModule } from '@angular/forms';
import { FormControl } from '@angular/forms'; import { FormControl } from '@angular/forms';
import { Big } from 'big.js'; import { Big } from 'big.js';
import { DeviceDetectorService } from 'ngx-device-detector'; import { DeviceDetectorService } from 'ngx-device-detector';
import { NgxSkeletonLoaderModule } from 'ngx-skeleton-loader'; import { NgxSkeletonLoaderModule } from 'ngx-skeleton-loader';
import { Subject } from 'rxjs';
import { takeUntil } from 'rxjs/operators';
@Component({ @Component({
imports: [ imports: [
@ -36,7 +40,7 @@ import { takeUntil } from 'rxjs/operators';
styleUrls: ['./fire-page.scss'], styleUrls: ['./fire-page.scss'],
templateUrl: './fire-page.html' templateUrl: './fire-page.html'
}) })
export class GfFirePageComponent implements OnDestroy, OnInit { export class GfFirePageComponent implements OnInit {
public deviceType: string; public deviceType: string;
public fireWealth: FireWealth; public fireWealth: FireWealth;
public hasImpersonationId: boolean; public hasImpersonationId: boolean;
@ -52,11 +56,10 @@ export class GfFirePageComponent implements OnDestroy, OnInit {
public withdrawalRatePerYear: Big; public withdrawalRatePerYear: Big;
public withdrawalRatePerYearProjected: Big; public withdrawalRatePerYearProjected: Big;
private unsubscribeSubject = new Subject<void>();
public constructor( public constructor(
private changeDetectorRef: ChangeDetectorRef, private changeDetectorRef: ChangeDetectorRef,
private dataService: DataService, private dataService: DataService,
private destroyRef: DestroyRef,
private deviceService: DeviceDetectorService, private deviceService: DeviceDetectorService,
private impersonationStorageService: ImpersonationStorageService, private impersonationStorageService: ImpersonationStorageService,
private userService: UserService private userService: UserService
@ -68,7 +71,7 @@ export class GfFirePageComponent implements OnDestroy, OnInit {
this.dataService this.dataService
.fetchPortfolioDetails() .fetchPortfolioDetails()
.pipe(takeUntil(this.unsubscribeSubject)) .pipe(takeUntilDestroyed(this.destroyRef))
.subscribe(({ summary }) => { .subscribe(({ summary }) => {
this.fireWealth = { this.fireWealth = {
today: { today: {
@ -92,19 +95,19 @@ export class GfFirePageComponent implements OnDestroy, OnInit {
this.impersonationStorageService this.impersonationStorageService
.onChangeHasImpersonation() .onChangeHasImpersonation()
.pipe(takeUntil(this.unsubscribeSubject)) .pipe(takeUntilDestroyed(this.destroyRef))
.subscribe((impersonationId) => { .subscribe((impersonationId) => {
this.hasImpersonationId = !!impersonationId; this.hasImpersonationId = !!impersonationId;
}); });
this.safeWithdrawalRateControl.valueChanges this.safeWithdrawalRateControl.valueChanges
.pipe(takeUntil(this.unsubscribeSubject)) .pipe(takeUntilDestroyed(this.destroyRef))
.subscribe((value) => { .subscribe((value) => {
this.onSafeWithdrawalRateChange(Number(value)); this.onSafeWithdrawalRateChange(Number(value));
}); });
this.userService.stateChanged this.userService.stateChanged
.pipe(takeUntil(this.unsubscribeSubject)) .pipe(takeUntilDestroyed(this.destroyRef))
.subscribe((state) => { .subscribe((state) => {
if (state?.user) { if (state?.user) {
this.user = state.user; this.user = state.user;
@ -132,11 +135,11 @@ export class GfFirePageComponent implements OnDestroy, OnInit {
public onAnnualInterestRateChange(annualInterestRate: number) { public onAnnualInterestRateChange(annualInterestRate: number) {
this.dataService this.dataService
.putUserSetting({ annualInterestRate }) .putUserSetting({ annualInterestRate })
.pipe(takeUntil(this.unsubscribeSubject)) .pipe(takeUntilDestroyed(this.destroyRef))
.subscribe(() => { .subscribe(() => {
this.userService this.userService
.get(true) .get(true)
.pipe(takeUntil(this.unsubscribeSubject)) .pipe(takeUntilDestroyed(this.destroyRef))
.subscribe((user) => { .subscribe((user) => {
this.user = user; this.user = user;
@ -163,11 +166,11 @@ export class GfFirePageComponent implements OnDestroy, OnInit {
retirementDate: retirementDate.toISOString(), retirementDate: retirementDate.toISOString(),
projectedTotalAmount: null projectedTotalAmount: null
}) })
.pipe(takeUntil(this.unsubscribeSubject)) .pipe(takeUntilDestroyed(this.destroyRef))
.subscribe(() => { .subscribe(() => {
this.userService this.userService
.get(true) .get(true)
.pipe(takeUntil(this.unsubscribeSubject)) .pipe(takeUntilDestroyed(this.destroyRef))
.subscribe((user) => { .subscribe((user) => {
this.user = user; this.user = user;
@ -179,11 +182,11 @@ export class GfFirePageComponent implements OnDestroy, OnInit {
public onSafeWithdrawalRateChange(safeWithdrawalRate: number) { public onSafeWithdrawalRateChange(safeWithdrawalRate: number) {
this.dataService this.dataService
.putUserSetting({ safeWithdrawalRate }) .putUserSetting({ safeWithdrawalRate })
.pipe(takeUntil(this.unsubscribeSubject)) .pipe(takeUntilDestroyed(this.destroyRef))
.subscribe(() => { .subscribe(() => {
this.userService this.userService
.get(true) .get(true)
.pipe(takeUntil(this.unsubscribeSubject)) .pipe(takeUntilDestroyed(this.destroyRef))
.subscribe((user) => { .subscribe((user) => {
this.user = user; this.user = user;
@ -198,11 +201,11 @@ export class GfFirePageComponent implements OnDestroy, OnInit {
public onSavingsRateChange(savingsRate: number) { public onSavingsRateChange(savingsRate: number) {
this.dataService this.dataService
.putUserSetting({ savingsRate }) .putUserSetting({ savingsRate })
.pipe(takeUntil(this.unsubscribeSubject)) .pipe(takeUntilDestroyed(this.destroyRef))
.subscribe(() => { .subscribe(() => {
this.userService this.userService
.get(true) .get(true)
.pipe(takeUntil(this.unsubscribeSubject)) .pipe(takeUntilDestroyed(this.destroyRef))
.subscribe((user) => { .subscribe((user) => {
this.user = user; this.user = user;
@ -217,11 +220,11 @@ export class GfFirePageComponent implements OnDestroy, OnInit {
projectedTotalAmount, projectedTotalAmount,
retirementDate: null retirementDate: null
}) })
.pipe(takeUntil(this.unsubscribeSubject)) .pipe(takeUntilDestroyed(this.destroyRef))
.subscribe(() => { .subscribe(() => {
this.userService this.userService
.get(true) .get(true)
.pipe(takeUntil(this.unsubscribeSubject)) .pipe(takeUntilDestroyed(this.destroyRef))
.subscribe((user) => { .subscribe((user) => {
this.user = user; this.user = user;
@ -230,11 +233,6 @@ export class GfFirePageComponent implements OnDestroy, OnInit {
}); });
} }
public ngOnDestroy() {
this.unsubscribeSubject.next();
this.unsubscribeSubject.complete();
}
private calculateWithdrawalRates() { private calculateWithdrawalRates() {
if (this.fireWealth && this.user?.settings?.safeWithdrawalRate) { if (this.fireWealth && this.user?.settings?.safeWithdrawalRate) {
this.withdrawalRatePerYear = new Big( this.withdrawalRatePerYear = new Big(

22
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 { DataService } from '@ghostfolio/ui/services';
import { NgClass } from '@angular/common'; 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 { IonIcon } from '@ionic/angular/standalone';
import { addIcons } from 'ionicons'; import { addIcons } from 'ionicons';
import { import {
@ -21,7 +22,6 @@ import {
warningOutline warningOutline
} from 'ionicons/icons'; } from 'ionicons/icons';
import { NgxSkeletonLoaderModule } from 'ngx-skeleton-loader'; import { NgxSkeletonLoaderModule } from 'ngx-skeleton-loader';
import { Subject, takeUntil } from 'rxjs';
@Component({ @Component({
imports: [ imports: [
@ -48,11 +48,10 @@ export class GfXRayPageComponent {
public statistics: PortfolioReportResponse['xRay']['statistics']; public statistics: PortfolioReportResponse['xRay']['statistics'];
public user: User; public user: User;
private unsubscribeSubject = new Subject<void>();
public constructor( public constructor(
private changeDetectorRef: ChangeDetectorRef, private changeDetectorRef: ChangeDetectorRef,
private dataService: DataService, private dataService: DataService,
private destroyRef: DestroyRef,
private impersonationStorageService: ImpersonationStorageService, private impersonationStorageService: ImpersonationStorageService,
private userService: UserService private userService: UserService
) { ) {
@ -62,13 +61,13 @@ export class GfXRayPageComponent {
public ngOnInit() { public ngOnInit() {
this.impersonationStorageService this.impersonationStorageService
.onChangeHasImpersonation() .onChangeHasImpersonation()
.pipe(takeUntil(this.unsubscribeSubject)) .pipe(takeUntilDestroyed(this.destroyRef))
.subscribe((impersonationId) => { .subscribe((impersonationId) => {
this.hasImpersonationId = !!impersonationId; this.hasImpersonationId = !!impersonationId;
}); });
this.userService.stateChanged this.userService.stateChanged
.pipe(takeUntil(this.unsubscribeSubject)) .pipe(takeUntilDestroyed(this.destroyRef))
.subscribe((state) => { .subscribe((state) => {
if (state?.user) { if (state?.user) {
this.user = state.user; this.user = state.user;
@ -91,28 +90,23 @@ export class GfXRayPageComponent {
public onRulesUpdated(event: UpdateUserSettingDto) { public onRulesUpdated(event: UpdateUserSettingDto) {
this.dataService this.dataService
.putUserSetting(event) .putUserSetting(event)
.pipe(takeUntil(this.unsubscribeSubject)) .pipe(takeUntilDestroyed(this.destroyRef))
.subscribe(() => { .subscribe(() => {
this.userService this.userService
.get(true) .get(true)
.pipe(takeUntil(this.unsubscribeSubject)) .pipe(takeUntilDestroyed(this.destroyRef))
.subscribe(); .subscribe();
this.initializePortfolioReport(); this.initializePortfolioReport();
}); });
} }
public ngOnDestroy() {
this.unsubscribeSubject.next();
this.unsubscribeSubject.complete();
}
private initializePortfolioReport() { private initializePortfolioReport() {
this.isLoading = true; this.isLoading = true;
this.dataService this.dataService
.fetchPortfolioReport() .fetchPortfolioReport()
.pipe(takeUntil(this.unsubscribeSubject)) .pipe(takeUntilDestroyed(this.destroyRef))
.subscribe(({ xRay: { categories, statistics } }) => { .subscribe(({ xRay: { categories, statistics } }) => {
this.categories = categories; this.categories = categories;
this.inactiveRules = this.mergeInactiveRules(categories); this.inactiveRules = this.mergeInactiveRules(categories);

6
apps/client/src/app/pages/register/register-page.component.ts

@ -1,4 +1,5 @@
import { TokenStorageService } from '@ghostfolio/client/services/token-storage.service'; import { TokenStorageService } from '@ghostfolio/client/services/token-storage.service';
import { UserService } from '@ghostfolio/client/services/user/user.service';
import { InfoItem, LineChartItem } from '@ghostfolio/common/interfaces'; import { InfoItem, LineChartItem } from '@ghostfolio/common/interfaces';
import { hasPermission, permissions } from '@ghostfolio/common/permissions'; import { hasPermission, permissions } from '@ghostfolio/common/permissions';
import { GfLogoComponent } from '@ghostfolio/ui/logo'; import { GfLogoComponent } from '@ghostfolio/ui/logo';
@ -42,11 +43,12 @@ export class GfRegisterPageComponent implements OnInit {
private deviceService: DeviceDetectorService, private deviceService: DeviceDetectorService,
private dialog: MatDialog, private dialog: MatDialog,
private router: Router, private router: Router,
private tokenStorageService: TokenStorageService private tokenStorageService: TokenStorageService,
private userService: UserService
) { ) {
this.info = this.dataService.fetchInfo(); this.info = this.dataService.fetchInfo();
this.tokenStorageService.signOut(); this.userService.signOut();
} }
public ngOnInit() { public ngOnInit() {

26
apps/client/src/app/services/token-storage.service.ts

@ -1,19 +1,11 @@
import { WebAuthnService } from '@ghostfolio/client/services/web-authn.service';
import { Injectable } from '@angular/core'; import { Injectable } from '@angular/core';
import { KEY_TOKEN } from './settings-storage.service'; import { KEY_TOKEN } from './settings-storage.service';
import { UserService } from './user/user.service';
@Injectable({ @Injectable({
providedIn: 'root' providedIn: 'root'
}) })
export class TokenStorageService { export class TokenStorageService {
public constructor(
private userService: UserService,
private webAuthnService: WebAuthnService
) {}
public getToken(): string { public getToken(): string {
return ( return (
window.sessionStorage.getItem(KEY_TOKEN) || window.sessionStorage.getItem(KEY_TOKEN) ||
@ -25,23 +17,7 @@ export class TokenStorageService {
if (staySignedIn) { if (staySignedIn) {
window.localStorage.setItem(KEY_TOKEN, token); window.localStorage.setItem(KEY_TOKEN, token);
} }
window.sessionStorage.setItem(KEY_TOKEN, token);
}
public signOut() {
const utmSource = window.localStorage.getItem('utm_source');
if (this.webAuthnService.isEnabled()) { window.sessionStorage.setItem(KEY_TOKEN, token);
this.webAuthnService.deregister().subscribe();
}
window.localStorage.clear();
window.sessionStorage.clear();
this.userService.remove();
if (utmSource) {
window.localStorage.setItem('utm_source', utmSource);
}
} }
} }

36
apps/client/src/app/services/user/user.service.ts

@ -1,3 +1,4 @@
import { WebAuthnService } from '@ghostfolio/client/services/web-authn.service';
import { Filter, User } from '@ghostfolio/common/interfaces'; import { Filter, User } from '@ghostfolio/common/interfaces';
import { hasPermission, permissions } from '@ghostfolio/common/permissions'; import { hasPermission, permissions } from '@ghostfolio/common/permissions';
@ -26,7 +27,8 @@ export class UserService extends ObservableStore<UserStoreState> {
public constructor( public constructor(
private deviceService: DeviceDetectorService, private deviceService: DeviceDetectorService,
private dialog: MatDialog, private dialog: MatDialog,
private http: HttpClient private http: HttpClient,
private webAuthnService: WebAuthnService
) { ) {
super({ trackStateHistory: true }); super({ trackStateHistory: true });
@ -93,10 +95,40 @@ export class UserService extends ObservableStore<UserStoreState> {
return this.getFilters().length > 0; return this.getFilters().length > 0;
} }
public remove() { public reset() {
this.setState({ user: null }, UserStoreActions.RemoveUser); this.setState({ user: null }, UserStoreActions.RemoveUser);
} }
public signOut() {
const utmSource = window.localStorage.getItem('utm_source');
if (this.webAuthnService.isEnabled()) {
this.webAuthnService.deregister().subscribe();
}
window.localStorage.clear();
window.sessionStorage.clear();
void this.clearAllCookies();
this.reset();
if (utmSource) {
window.localStorage.setItem('utm_source', utmSource);
}
}
private async clearAllCookies() {
if (!('cookieStore' in window)) {
console.warn('Cookie Store API not available in this browser');
return;
}
const cookies = await cookieStore.getAll();
await Promise.all(cookies.map(({ name }) => cookieStore.delete(name)));
}
private fetchUser(): Observable<User> { private fetchUser(): Observable<User> {
return this.http.get<any>('/api/v1/user').pipe( return this.http.get<any>('/api/v1/user').pipe(
map((user) => { map((user) => {

64
apps/client/src/locales/messages.pl.xlf

@ -660,7 +660,7 @@
</trans-unit> </trans-unit>
<trans-unit id="5611965261696422586" datatype="html"> <trans-unit id="5611965261696422586" datatype="html">
<source>and is driven by the efforts of its <x id="START_LINK" ctype="x-a" equiv-text="&lt;a href=&quot;https://github.com/ghostfolio/ghostfolio/graphs/contributors&quot; title=&quot;Contributors to Ghostfolio&quot; &gt;"/>contributors<x id="CLOSE_LINK" ctype="x-a" equiv-text="&lt;/a &gt;"/></source> <source>and is driven by the efforts of its <x id="START_LINK" ctype="x-a" equiv-text="&lt;a href=&quot;https://github.com/ghostfolio/ghostfolio/graphs/contributors&quot; title=&quot;Contributors to Ghostfolio&quot; &gt;"/>contributors<x id="CLOSE_LINK" ctype="x-a" equiv-text="&lt;/a &gt;"/></source>
<target state="new">and is driven by the efforts of its <x id="START_LINK" ctype="x-a" equiv-text="&lt;a href=&quot;https://github.com/ghostfolio/ghostfolio/graphs/contributors&quot; title=&quot;Contributors to Ghostfolio&quot; &gt;"/>contributors<x id="CLOSE_LINK" ctype="x-a" equiv-text="&lt;/a &gt;"/></target> <target state="translated">i jest rozwijany dzięki pracy jego <x id="START_LINK" ctype="x-a" equiv-text="&lt;a href=&quot;https://github.com/ghostfolio/ghostfolio/graphs/contributors&quot; title=&quot;Contributors to Ghostfolio&quot; &gt;"/>współtwórców<x id="CLOSE_LINK" ctype="x-a" equiv-text="&lt;/a &gt;"/></target>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">apps/client/src/app/pages/about/overview/about-overview-page.html</context> <context context-type="sourcefile">apps/client/src/app/pages/about/overview/about-overview-page.html</context>
<context context-type="linenumber">49</context> <context context-type="linenumber">49</context>
@ -1580,7 +1580,7 @@
</trans-unit> </trans-unit>
<trans-unit id="5289957034780335504" datatype="html"> <trans-unit id="5289957034780335504" datatype="html">
<source>The source code is fully available as <x id="START_LINK" ctype="x-a" equiv-text="&lt;a href=&quot;https://github.com/ghostfolio/ghostfolio&quot; title=&quot;Find Ghostfolio on GitHub&quot; &gt;"/>open source software<x id="CLOSE_LINK" ctype="x-a" equiv-text="&lt;/a &gt;"/> (OSS) under the <x id="START_LINK_1" equiv-text="&lt;a href=&quot;https://www.gnu.org/licenses/agpl-3.0.html&quot; title=&quot;GNU Affero General Public License&quot; &gt;"/>AGPL-3.0 license<x id="CLOSE_LINK" ctype="x-a" equiv-text="&lt;/a &gt;"/></source> <source>The source code is fully available as <x id="START_LINK" ctype="x-a" equiv-text="&lt;a href=&quot;https://github.com/ghostfolio/ghostfolio&quot; title=&quot;Find Ghostfolio on GitHub&quot; &gt;"/>open source software<x id="CLOSE_LINK" ctype="x-a" equiv-text="&lt;/a &gt;"/> (OSS) under the <x id="START_LINK_1" equiv-text="&lt;a href=&quot;https://www.gnu.org/licenses/agpl-3.0.html&quot; title=&quot;GNU Affero General Public License&quot; &gt;"/>AGPL-3.0 license<x id="CLOSE_LINK" ctype="x-a" equiv-text="&lt;/a &gt;"/></source>
<target state="new">The source code is fully available as <x id="START_LINK" ctype="x-a" equiv-text="&lt;a href=&quot;https://github.com/ghostfolio/ghostfolio&quot; title=&quot;Find Ghostfolio on GitHub&quot; &gt;"/>open source software<x id="CLOSE_LINK" ctype="x-a" equiv-text="&lt;/a &gt;"/> (OSS) under the <x id="START_LINK_1" equiv-text="&lt;a href=&quot;https://www.gnu.org/licenses/agpl-3.0.html&quot; title=&quot;GNU Affero General Public License&quot; &gt;"/>AGPL-3.0 license<x id="CLOSE_LINK" ctype="x-a" equiv-text="&lt;/a &gt;"/></target> <target state="translated">Kod źródłowy jest w pełni dostępny jako <x id="START_LINK" ctype="x-a" equiv-text="&lt;a href=&quot;https://github.com/ghostfolio/ghostfolio&quot; title=&quot;Find Ghostfolio on GitHub&quot; &gt;"/>oprogramowanie open source<x id="CLOSE_LINK" ctype="x-a" equiv-text="&lt;/a &gt;"/> (OSS) na licencji <x id="START_LINK_1" equiv-text="&lt;a href=&quot;https://www.gnu.org/licenses/agpl-3.0.html&quot; title=&quot;GNU Affero General Public License&quot; &gt;"/>AGPL-3.0 license<x id="CLOSE_LINK" ctype="x-a" equiv-text="&lt;/a &gt;"/></target>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">apps/client/src/app/pages/about/overview/about-overview-page.html</context> <context context-type="sourcefile">apps/client/src/app/pages/about/overview/about-overview-page.html</context>
<context context-type="linenumber">16</context> <context context-type="linenumber">16</context>
@ -2148,7 +2148,7 @@
</trans-unit> </trans-unit>
<trans-unit id="366169681580494481" datatype="html"> <trans-unit id="366169681580494481" datatype="html">
<source>Performance with currency effect</source> <source>Performance with currency effect</source>
<target state="new">Performance with currency effect</target> <target state="translated">Wynik z efektem walutowym</target>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">apps/client/src/app/pages/portfolio/analysis/analysis-page.html</context> <context context-type="sourcefile">apps/client/src/app/pages/portfolio/analysis/analysis-page.html</context>
<context context-type="linenumber">135</context> <context context-type="linenumber">135</context>
@ -2376,7 +2376,7 @@
</trans-unit> </trans-unit>
<trans-unit id="3004519800638083911" datatype="html"> <trans-unit id="3004519800638083911" datatype="html">
<source>this is projected to increase to</source> <source>this is projected to increase to</source>
<target state="new">this is projected to increase to</target> <target state="translated">prognozuje się wzrost tej kwoty do</target>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">apps/client/src/app/pages/portfolio/fire/fire-page.html</context> <context context-type="sourcefile">apps/client/src/app/pages/portfolio/fire/fire-page.html</context>
<context context-type="linenumber">147</context> <context context-type="linenumber">147</context>
@ -3564,7 +3564,7 @@
</trans-unit> </trans-unit>
<trans-unit id="70768492340592330" datatype="html"> <trans-unit id="70768492340592330" datatype="html">
<source>and a safe withdrawal rate (SWR) of</source> <source>and a safe withdrawal rate (SWR) of</source>
<target state="new">and a safe withdrawal rate (SWR) of</target> <target state="translated">oraz bezpiecznej stopy wypłaty (SWR) na poziomie</target>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">apps/client/src/app/pages/portfolio/fire/fire-page.html</context> <context context-type="sourcefile">apps/client/src/app/pages/portfolio/fire/fire-page.html</context>
<context context-type="linenumber">108</context> <context context-type="linenumber">108</context>
@ -4124,7 +4124,7 @@
</trans-unit> </trans-unit>
<trans-unit id="5211792611718918888" datatype="html"> <trans-unit id="5211792611718918888" datatype="html">
<source>annual interest rate</source> <source>annual interest rate</source>
<target state="new">annual interest rate</target> <target state="translated">rocznej stopy zwrotu</target>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">apps/client/src/app/pages/portfolio/fire/fire-page.html</context> <context context-type="sourcefile">apps/client/src/app/pages/portfolio/fire/fire-page.html</context>
<context context-type="linenumber">185</context> <context context-type="linenumber">185</context>
@ -4464,7 +4464,7 @@
</trans-unit> </trans-unit>
<trans-unit id="2003818202621229370" datatype="html"> <trans-unit id="2003818202621229370" datatype="html">
<source>Sustainable retirement income</source> <source>Sustainable retirement income</source>
<target state="new">Sustainable retirement income</target> <target state="translated">Zrównoważony dochód na emeryturze</target>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">apps/client/src/app/pages/portfolio/fire/fire-page.html</context> <context context-type="sourcefile">apps/client/src/app/pages/portfolio/fire/fire-page.html</context>
<context context-type="linenumber">41</context> <context context-type="linenumber">41</context>
@ -4597,7 +4597,7 @@
</trans-unit> </trans-unit>
<trans-unit id="4905798562247431262" datatype="html"> <trans-unit id="4905798562247431262" datatype="html">
<source>per month</source> <source>per month</source>
<target state="new">per month</target> <target state="translated">miesięcznie</target>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">apps/client/src/app/pages/portfolio/fire/fire-page.html</context> <context context-type="sourcefile">apps/client/src/app/pages/portfolio/fire/fire-page.html</context>
<context context-type="linenumber">94</context> <context context-type="linenumber">94</context>
@ -5397,7 +5397,7 @@
</trans-unit> </trans-unit>
<trans-unit id="1468015720862673946" datatype="html"> <trans-unit id="1468015720862673946" datatype="html">
<source>View Details</source> <source>View Details</source>
<target state="new">View Details</target> <target state="translated">Zobacz szczegóły</target>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">apps/client/src/app/components/admin-users/admin-users.html</context> <context context-type="sourcefile">apps/client/src/app/components/admin-users/admin-users.html</context>
<context context-type="linenumber">225</context> <context context-type="linenumber">225</context>
@ -5425,7 +5425,7 @@
</trans-unit> </trans-unit>
<trans-unit id="2149165958319691680" datatype="html"> <trans-unit id="2149165958319691680" datatype="html">
<source>Buy</source> <source>Buy</source>
<target state="translated">Zakup</target> <target state="translated">Kupno</target>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">apps/client/src/app/components/portfolio-summary/portfolio-summary.component.html</context> <context context-type="sourcefile">apps/client/src/app/components/portfolio-summary/portfolio-summary.component.html</context>
<context context-type="linenumber">31</context> <context context-type="linenumber">31</context>
@ -5469,7 +5469,7 @@
</trans-unit> </trans-unit>
<trans-unit id="4881880242577556" datatype="html"> <trans-unit id="4881880242577556" datatype="html">
<source>Sell</source> <source>Sell</source>
<target state="translated">Sprzedaj</target> <target state="translated">Sprzedaż</target>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">apps/client/src/app/components/portfolio-summary/portfolio-summary.component.html</context> <context context-type="sourcefile">apps/client/src/app/components/portfolio-summary/portfolio-summary.component.html</context>
<context context-type="linenumber">44</context> <context context-type="linenumber">44</context>
@ -5625,7 +5625,7 @@
</trans-unit> </trans-unit>
<trans-unit id="3227075298129844075" datatype="html"> <trans-unit id="3227075298129844075" datatype="html">
<source>If you retire today, you would be able to withdraw</source> <source>If you retire today, you would be able to withdraw</source>
<target state="new">If you retire today, you would be able to withdraw</target> <target state="translated">Gdybyś przeszedł na emeryturę dziś, mógłbyś wypłacać</target>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">apps/client/src/app/pages/portfolio/fire/fire-page.html</context> <context context-type="sourcefile">apps/client/src/app/pages/portfolio/fire/fire-page.html</context>
<context context-type="linenumber">68</context> <context context-type="linenumber">68</context>
@ -6710,7 +6710,7 @@
</trans-unit> </trans-unit>
<trans-unit id="1355312194390410495" datatype="html"> <trans-unit id="1355312194390410495" datatype="html">
<source>, based on your total assets of</source> <source>, based on your total assets of</source>
<target state="new">, based on your total assets of</target> <target state="translated">, na podstawie całkowitej wartości aktywów wynoszącej</target>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">apps/client/src/app/pages/portfolio/fire/fire-page.html</context> <context context-type="sourcefile">apps/client/src/app/pages/portfolio/fire/fire-page.html</context>
<context context-type="linenumber">96</context> <context context-type="linenumber">96</context>
@ -6974,7 +6974,7 @@
</trans-unit> </trans-unit>
<trans-unit id="2878377610946588870" datatype="html"> <trans-unit id="2878377610946588870" datatype="html">
<source>, assuming a</source> <source>, assuming a</source>
<target state="new">, assuming a</target> <target state="translated">, przyjmując</target>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">apps/client/src/app/pages/portfolio/fire/fire-page.html</context> <context context-type="sourcefile">apps/client/src/app/pages/portfolio/fire/fire-page.html</context>
<context context-type="linenumber">174</context> <context context-type="linenumber">174</context>
@ -7062,7 +7062,7 @@
</trans-unit> </trans-unit>
<trans-unit id="3528767106831563012" datatype="html"> <trans-unit id="3528767106831563012" datatype="html">
<source>Ghostfolio is a lightweight wealth management application for individuals to keep track of stocks, ETFs or cryptocurrencies and make solid, data-driven investment decisions.</source> <source>Ghostfolio is a lightweight wealth management application for individuals to keep track of stocks, ETFs or cryptocurrencies and make solid, data-driven investment decisions.</source>
<target state="new">Ghostfolio is a lightweight wealth management application for individuals to keep track of stocks, ETFs or cryptocurrencies and make solid, data-driven investment decisions.</target> <target state="translated">Ghostfolio to aplikacja do zarządzania majątkiem, przeznaczona dla osób prywatnych do śledzenia akcji, ETF‑ów i kryptowalut oraz podejmowania solidnych, opartych na danych decyzji inwestycyjnych.</target>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">apps/client/src/app/pages/about/overview/about-overview-page.html</context> <context context-type="sourcefile">apps/client/src/app/pages/about/overview/about-overview-page.html</context>
<context context-type="linenumber">10</context> <context context-type="linenumber">10</context>
@ -7392,7 +7392,7 @@
</trans-unit> </trans-unit>
<trans-unit id="7825231215382064101" datatype="html"> <trans-unit id="7825231215382064101" datatype="html">
<source>Change with currency effect</source> <source>Change with currency effect</source>
<target state="new">Change with currency effect</target> <target state="translated">Zmiana z efektem walutowym</target>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">apps/client/src/app/pages/portfolio/analysis/analysis-page.html</context> <context context-type="sourcefile">apps/client/src/app/pages/portfolio/analysis/analysis-page.html</context>
<context context-type="linenumber">116</context> <context context-type="linenumber">116</context>
@ -7532,7 +7532,7 @@
</trans-unit> </trans-unit>
<trans-unit id="1325095699053123251" datatype="html"> <trans-unit id="1325095699053123251" datatype="html">
<source>The project has been initiated by</source> <source>The project has been initiated by</source>
<target state="new">The project has been initiated by</target> <target state="translated">Projekt został zainicjowany przez</target>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">apps/client/src/app/pages/about/overview/about-overview-page.html</context> <context context-type="sourcefile">apps/client/src/app/pages/about/overview/about-overview-page.html</context>
<context context-type="linenumber">40</context> <context context-type="linenumber">40</context>
@ -7556,7 +7556,7 @@
</trans-unit> </trans-unit>
<trans-unit id="5004550577313573215" datatype="html"> <trans-unit id="5004550577313573215" datatype="html">
<source>Total amount</source> <source>Total amount</source>
<target state="new">Total amount</target> <target state="translated">Wartość portfela</target>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">apps/client/src/app/pages/portfolio/analysis/analysis-page.html</context> <context context-type="sourcefile">apps/client/src/app/pages/portfolio/analysis/analysis-page.html</context>
<context context-type="linenumber">95</context> <context context-type="linenumber">95</context>
@ -7898,7 +7898,7 @@
</trans-unit> </trans-unit>
<trans-unit id="rule.feeRatioTotalInvestmentVolume" datatype="html"> <trans-unit id="rule.feeRatioTotalInvestmentVolume" datatype="html">
<source>Fee Ratio</source> <source>Fee Ratio</source>
<target state="new">Fee Ratio</target> <target state="translated">Wskaźnik opłat</target>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">apps/client/src/app/pages/i18n/i18n-page.html</context> <context context-type="sourcefile">apps/client/src/app/pages/i18n/i18n-page.html</context>
<context context-type="linenumber">152</context> <context context-type="linenumber">152</context>
@ -8333,7 +8333,7 @@
</trans-unit> </trans-unit>
<trans-unit id="rule.accountClusterRisk.category" datatype="html"> <trans-unit id="rule.accountClusterRisk.category" datatype="html">
<source>Account Cluster Risks</source> <source>Account Cluster Risks</source>
<target state="new">Account Cluster Risks</target> <target state="translated">Ryzyka skupienia w obrębie rachunków</target>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">apps/client/src/app/pages/i18n/i18n-page.html</context> <context context-type="sourcefile">apps/client/src/app/pages/i18n/i18n-page.html</context>
<context context-type="linenumber">14</context> <context context-type="linenumber">14</context>
@ -8341,7 +8341,7 @@
</trans-unit> </trans-unit>
<trans-unit id="rule.assetClassClusterRisk.category" datatype="html"> <trans-unit id="rule.assetClassClusterRisk.category" datatype="html">
<source>Asset Class Cluster Risks</source> <source>Asset Class Cluster Risks</source>
<target state="new">Asset Class Cluster Risks</target> <target state="translated">Ryzyka skupienia w obrębie klas aktywów</target>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">apps/client/src/app/pages/i18n/i18n-page.html</context> <context context-type="sourcefile">apps/client/src/app/pages/i18n/i18n-page.html</context>
<context context-type="linenumber">39</context> <context context-type="linenumber">39</context>
@ -8349,7 +8349,7 @@
</trans-unit> </trans-unit>
<trans-unit id="rule.currencyClusterRisk.category" datatype="html"> <trans-unit id="rule.currencyClusterRisk.category" datatype="html">
<source>Currency Cluster Risks</source> <source>Currency Cluster Risks</source>
<target state="new">Currency Cluster Risks</target> <target state="translated">Ryzyka koncentracji walutowej</target>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">apps/client/src/app/pages/i18n/i18n-page.html</context> <context context-type="sourcefile">apps/client/src/app/pages/i18n/i18n-page.html</context>
<context context-type="linenumber">83</context> <context context-type="linenumber">83</context>
@ -8357,7 +8357,7 @@
</trans-unit> </trans-unit>
<trans-unit id="rule.economicMarketClusterRisk.category" datatype="html"> <trans-unit id="rule.economicMarketClusterRisk.category" datatype="html">
<source>Economic Market Cluster Risks</source> <source>Economic Market Cluster Risks</source>
<target state="new">Economic Market Cluster Risks</target> <target state="translated">Ryzyka skupienia w obrębie segmentów rynku gospodarczego</target>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">apps/client/src/app/pages/i18n/i18n-page.html</context> <context context-type="sourcefile">apps/client/src/app/pages/i18n/i18n-page.html</context>
<context context-type="linenumber">106</context> <context context-type="linenumber">106</context>
@ -8373,7 +8373,7 @@
</trans-unit> </trans-unit>
<trans-unit id="rule.fees.category" datatype="html"> <trans-unit id="rule.fees.category" datatype="html">
<source>Fees</source> <source>Fees</source>
<target state="new">Fees</target> <target state="translated">Opłaty</target>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">apps/client/src/app/pages/i18n/i18n-page.html</context> <context context-type="sourcefile">apps/client/src/app/pages/i18n/i18n-page.html</context>
<context context-type="linenumber">161</context> <context context-type="linenumber">161</context>
@ -8381,7 +8381,7 @@
</trans-unit> </trans-unit>
<trans-unit id="rule.liquidity.category" datatype="html"> <trans-unit id="rule.liquidity.category" datatype="html">
<source>Liquidity</source> <source>Liquidity</source>
<target state="new">Liquidity</target> <target state="translated">Płynność</target>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">apps/client/src/app/pages/i18n/i18n-page.html</context> <context context-type="sourcefile">apps/client/src/app/pages/i18n/i18n-page.html</context>
<context context-type="linenumber">70</context> <context context-type="linenumber">70</context>
@ -8389,7 +8389,7 @@
</trans-unit> </trans-unit>
<trans-unit id="rule.liquidityBuyingPower" datatype="html"> <trans-unit id="rule.liquidityBuyingPower" datatype="html">
<source>Buying Power</source> <source>Buying Power</source>
<target state="new">Buying Power</target> <target state="translated">Siła nabywcza</target>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">apps/client/src/app/pages/i18n/i18n-page.html</context> <context context-type="sourcefile">apps/client/src/app/pages/i18n/i18n-page.html</context>
<context context-type="linenumber">71</context> <context context-type="linenumber">71</context>
@ -8413,7 +8413,7 @@
</trans-unit> </trans-unit>
<trans-unit id="rule.liquidityBuyingPower.true" datatype="html"> <trans-unit id="rule.liquidityBuyingPower.true" datatype="html">
<source>Your buying power exceeds ${thresholdMin} ${baseCurrency}</source> <source>Your buying power exceeds ${thresholdMin} ${baseCurrency}</source>
<target state="new">Your buying power exceeds ${thresholdMin} ${baseCurrency}</target> <target state="translated">Twoja siła nabywcza przekracza ${thresholdMin} ${baseCurrency}</target>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">apps/client/src/app/pages/i18n/i18n-page.html</context> <context context-type="sourcefile">apps/client/src/app/pages/i18n/i18n-page.html</context>
<context context-type="linenumber">80</context> <context context-type="linenumber">80</context>
@ -8421,7 +8421,7 @@
</trans-unit> </trans-unit>
<trans-unit id="rule.regionalMarketClusterRisk.category" datatype="html"> <trans-unit id="rule.regionalMarketClusterRisk.category" datatype="html">
<source>Regional Market Cluster Risks</source> <source>Regional Market Cluster Risks</source>
<target state="new">Regional Market Cluster Risks</target> <target state="translated">Ryzyka skupienia w obrębie regionów rynkowych</target>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">apps/client/src/app/pages/i18n/i18n-page.html</context> <context context-type="sourcefile">apps/client/src/app/pages/i18n/i18n-page.html</context>
<context context-type="linenumber">163</context> <context context-type="linenumber">163</context>
@ -8437,7 +8437,7 @@
</trans-unit> </trans-unit>
<trans-unit id="rule.economicMarketClusterRiskDevelopedMarkets" datatype="html"> <trans-unit id="rule.economicMarketClusterRiskDevelopedMarkets" datatype="html">
<source>Developed Markets</source> <source>Developed Markets</source>
<target state="new">Developed Markets</target> <target state="translated">Rynki rozwinięte</target>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">apps/client/src/app/pages/i18n/i18n-page.html</context> <context context-type="sourcefile">apps/client/src/app/pages/i18n/i18n-page.html</context>
<context context-type="linenumber">109</context> <context context-type="linenumber">109</context>
@ -8469,7 +8469,7 @@
</trans-unit> </trans-unit>
<trans-unit id="rule.economicMarketClusterRiskEmergingMarkets" datatype="html"> <trans-unit id="rule.economicMarketClusterRiskEmergingMarkets" datatype="html">
<source>Emerging Markets</source> <source>Emerging Markets</source>
<target state="new">Emerging Markets</target> <target state="translated">Rynki wschodzące</target>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">apps/client/src/app/pages/i18n/i18n-page.html</context> <context context-type="sourcefile">apps/client/src/app/pages/i18n/i18n-page.html</context>
<context context-type="linenumber">127</context> <context context-type="linenumber">127</context>
@ -8581,7 +8581,7 @@
</trans-unit> </trans-unit>
<trans-unit id="rule.regionalMarketClusterRiskEurope" datatype="html"> <trans-unit id="rule.regionalMarketClusterRiskEurope" datatype="html">
<source>Europe</source> <source>Europe</source>
<target state="new">Europe</target> <target state="translated">Europa</target>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">apps/client/src/app/pages/i18n/i18n-page.html</context> <context context-type="sourcefile">apps/client/src/app/pages/i18n/i18n-page.html</context>
<context context-type="linenumber">195</context> <context context-type="linenumber">195</context>
@ -8613,7 +8613,7 @@
</trans-unit> </trans-unit>
<trans-unit id="rule.regionalMarketClusterRiskJapan" datatype="html"> <trans-unit id="rule.regionalMarketClusterRiskJapan" datatype="html">
<source>Japan</source> <source>Japan</source>
<target state="new">Japan</target> <target state="translated">Japonia</target>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">apps/client/src/app/pages/i18n/i18n-page.html</context> <context context-type="sourcefile">apps/client/src/app/pages/i18n/i18n-page.html</context>
<context context-type="linenumber">209</context> <context context-type="linenumber">209</context>

2
libs/ui/src/lib/benchmark/benchmark-detail-dialog/interfaces/interfaces.ts

@ -3,7 +3,7 @@ import { ColorScheme } from '@ghostfolio/common/types';
import { DataSource } from '@prisma/client'; import { DataSource } from '@prisma/client';
export interface BenchmarkDetailDialogParams { export interface BenchmarkDetailDialogParams {
colorScheme: ColorScheme; colorScheme?: ColorScheme;
dataSource: DataSource; dataSource: DataSource;
deviceType: string; deviceType: string;
locale: string; locale: string;

16
libs/ui/src/lib/benchmark/benchmark.component.html

@ -15,7 +15,7 @@
<div class="text-truncate"> <div class="text-truncate">
{{ element?.name }} {{ element?.name }}
</div> </div>
@if (showSymbol) { @if (showSymbol()) {
<div> <div>
<small class="text-muted">{{ element?.symbol }}</small> <small class="text-muted">{{ element?.symbol }}</small>
</div> </div>
@ -98,7 +98,7 @@
@if (element?.performances?.allTimeHigh?.date) { @if (element?.performances?.allTimeHigh?.date) {
<gf-value <gf-value
[isDate]="true" [isDate]="true"
[locale]="locale" [locale]="locale()"
[value]="element?.performances?.allTimeHigh?.date" [value]="element?.performances?.allTimeHigh?.date"
/> />
} }
@ -123,7 +123,7 @@
<gf-value <gf-value
class="d-inline-block justify-content-end" class="d-inline-block justify-content-end"
[isPercent]="true" [isPercent]="true"
[locale]="locale" [locale]="locale()"
[ngClass]="{ [ngClass]="{
'text-danger': 'text-danger':
element?.performances?.allTimeHigh?.performancePercent < 0, element?.performances?.allTimeHigh?.performancePercent < 0,
@ -150,7 +150,7 @@
<ng-container matColumnDef="actions" stickyEnd> <ng-container matColumnDef="actions" stickyEnd>
<th *matHeaderCellDef class="px-1 text-center" mat-header-cell></th> <th *matHeaderCellDef class="px-1 text-center" mat-header-cell></th>
<td *matCellDef="let element" class="px-1 text-center" mat-cell> <td *matCellDef="let element" class="px-1 text-center" mat-cell>
@if (hasPermissionToDeleteItem) { @if (hasPermissionToDeleteItem()) {
<button <button
class="mx-1 no-min-width px-2" class="mx-1 no-min-width px-2"
mat-button mat-button
@ -163,7 +163,7 @@
<mat-menu #benchmarkMenu="matMenu" xPosition="before"> <mat-menu #benchmarkMenu="matMenu" xPosition="before">
<button <button
mat-menu-item mat-menu-item
[disabled]="!hasPermissionToDeleteItem" [disabled]="!hasPermissionToDeleteItem()"
(click)=" (click)="
onDeleteItem({ onDeleteItem({
dataSource: element.dataSource, dataSource: element.dataSource,
@ -180,9 +180,9 @@
</td> </td>
</ng-container> </ng-container>
<tr *matHeaderRowDef="displayedColumns" mat-header-row></tr> <tr *matHeaderRowDef="displayedColumns()" mat-header-row></tr>
<tr <tr
*matRowDef="let row; columns: displayedColumns" *matRowDef="let row; columns: displayedColumns()"
class="cursor-pointer" class="cursor-pointer"
mat-row mat-row
(click)=" (click)="
@ -204,7 +204,7 @@
width: '100%' width: '100%'
}" }"
/> />
} @else if (benchmarks?.length === 0) { } @else if (benchmarks()?.length === 0) {
<div class="p-3 text-center text-muted"> <div class="p-3 text-center text-muted">
<small i18n>No data available</small> <small i18n>No data available</small>
</div> </div>

129
libs/ui/src/lib/benchmark/benchmark.component.ts

@ -16,13 +16,15 @@ import {
CUSTOM_ELEMENTS_SCHEMA, CUSTOM_ELEMENTS_SCHEMA,
ChangeDetectionStrategy, ChangeDetectionStrategy,
Component, Component,
EventEmitter, DestroyRef,
Input, computed,
OnChanges, effect,
OnDestroy, inject,
Output, input,
ViewChild output,
viewChild
} from '@angular/core'; } from '@angular/core';
import { takeUntilDestroyed } from '@angular/core/rxjs-interop';
import { MatButtonModule } from '@angular/material/button'; import { MatButtonModule } from '@angular/material/button';
import { MatDialog } from '@angular/material/dialog'; import { MatDialog } from '@angular/material/dialog';
import { MatMenuModule } from '@angular/material/menu'; import { MatMenuModule } from '@angular/material/menu';
@ -34,7 +36,6 @@ import { addIcons } from 'ionicons';
import { ellipsisHorizontal, trashOutline } from 'ionicons/icons'; import { ellipsisHorizontal, trashOutline } from 'ionicons/icons';
import { isNumber } from 'lodash'; import { isNumber } from 'lodash';
import { NgxSkeletonLoaderModule } from 'ngx-skeleton-loader'; import { NgxSkeletonLoaderModule } from 'ngx-skeleton-loader';
import { Subject, takeUntil } from 'rxjs';
import { translate } from '../i18n'; import { translate } from '../i18n';
import { GfTrendIndicatorComponent } from '../trend-indicator/trend-indicator.component'; import { GfTrendIndicatorComponent } from '../trend-indicator/trend-indicator.component';
@ -61,41 +62,58 @@ import { BenchmarkDetailDialogParams } from './benchmark-detail-dialog/interface
styleUrls: ['./benchmark.component.scss'], styleUrls: ['./benchmark.component.scss'],
templateUrl: './benchmark.component.html' templateUrl: './benchmark.component.html'
}) })
export class GfBenchmarkComponent implements OnChanges, OnDestroy { export class GfBenchmarkComponent {
@Input() benchmarks: Benchmark[]; public readonly benchmarks = input.required<Benchmark[]>();
@Input() deviceType: string; public readonly deviceType = input.required<string>();
@Input() hasPermissionToDeleteItem: boolean; public readonly hasPermissionToDeleteItem = input<boolean>();
@Input() locale = getLocale(); public readonly locale = input(getLocale());
@Input() showSymbol = true; public readonly showSymbol = input(true);
@Input() user: User; public readonly user = input<User>();
@Output() itemDeleted = new EventEmitter<AssetProfileIdentifier>(); public readonly itemDeleted = output<AssetProfileIdentifier>();
@ViewChild(MatSort) sort: MatSort; protected readonly sort = viewChild(MatSort);
public dataSource = new MatTableDataSource<Benchmark>([]); protected readonly dataSource = new MatTableDataSource<Benchmark>([]);
public displayedColumns = [ protected readonly displayedColumns = computed(() => {
return [
'name', 'name',
...(this.user()?.settings?.isExperimentalFeatures
? ['trend50d', 'trend200d']
: []),
'date', 'date',
'change', 'change',
'marketCondition', 'marketCondition',
'actions' 'actions'
]; ];
public isLoading = true; });
public isNumber = isNumber; protected isLoading = true;
public resolveMarketCondition = resolveMarketCondition; protected readonly isNumber = isNumber;
public translate = translate; protected readonly resolveMarketCondition = resolveMarketCondition;
protected readonly translate = translate;
private unsubscribeSubject = new Subject<void>();
private readonly destroyRef = inject(DestroyRef);
public constructor( private readonly dialog = inject(MatDialog);
private dialog: MatDialog, private readonly notificationService = inject(NotificationService);
private notificationService: NotificationService, private readonly route = inject(ActivatedRoute);
private route: ActivatedRoute, private readonly router = inject(Router);
private router: Router
) { public constructor() {
effect(() => {
const benchmarks = this.benchmarks();
if (benchmarks) {
this.dataSource.data = benchmarks;
this.dataSource.sortingDataAccessor = getLowercase;
this.dataSource.sort = this.sort() ?? null;
this.isLoading = false;
}
});
this.route.queryParams this.route.queryParams
.pipe(takeUntil(this.unsubscribeSubject)) .pipe(takeUntilDestroyed(this.destroyRef))
.subscribe((params) => { .subscribe((params) => {
if ( if (
params['benchmarkDetailDialog'] && params['benchmarkDetailDialog'] &&
@ -112,30 +130,7 @@ export class GfBenchmarkComponent implements OnChanges, OnDestroy {
addIcons({ ellipsisHorizontal, trashOutline }); addIcons({ ellipsisHorizontal, trashOutline });
} }
public ngOnChanges() { protected onDeleteItem({ dataSource, symbol }: AssetProfileIdentifier) {
if (this.benchmarks) {
this.dataSource.data = this.benchmarks;
this.dataSource.sortingDataAccessor = getLowercase;
this.dataSource.sort = this.sort;
this.isLoading = false;
}
if (this.user?.settings?.isExperimentalFeatures) {
this.displayedColumns = [
'name',
'trend50d',
'trend200d',
'date',
'change',
'marketCondition',
'actions'
];
}
}
public onDeleteItem({ dataSource, symbol }: AssetProfileIdentifier) {
this.notificationService.confirm({ this.notificationService.confirm({
confirmFn: () => { confirmFn: () => {
this.itemDeleted.emit({ dataSource, symbol }); this.itemDeleted.emit({ dataSource, symbol });
@ -145,17 +140,15 @@ export class GfBenchmarkComponent implements OnChanges, OnDestroy {
}); });
} }
public onOpenBenchmarkDialog({ dataSource, symbol }: AssetProfileIdentifier) { protected onOpenBenchmarkDialog({
dataSource,
symbol
}: AssetProfileIdentifier) {
this.router.navigate([], { this.router.navigate([], {
queryParams: { dataSource, symbol, benchmarkDetailDialog: true } queryParams: { dataSource, symbol, benchmarkDetailDialog: true }
}); });
} }
public ngOnDestroy() {
this.unsubscribeSubject.next();
this.unsubscribeSubject.complete();
}
private openBenchmarkDetailDialog({ private openBenchmarkDetailDialog({
dataSource, dataSource,
symbol symbol
@ -167,17 +160,17 @@ export class GfBenchmarkComponent implements OnChanges, OnDestroy {
data: { data: {
dataSource, dataSource,
symbol, symbol,
colorScheme: this.user?.settings?.colorScheme, colorScheme: this.user()?.settings?.colorScheme,
deviceType: this.deviceType, deviceType: this.deviceType(),
locale: this.locale locale: this.locale()
}, },
height: this.deviceType === 'mobile' ? '98vh' : undefined, height: this.deviceType() === 'mobile' ? '98vh' : undefined,
width: this.deviceType === 'mobile' ? '100vw' : '50rem' width: this.deviceType() === 'mobile' ? '100vw' : '50rem'
}); });
dialogRef dialogRef
.afterClosed() .afterClosed()
.pipe(takeUntil(this.unsubscribeSubject)) .pipe(takeUntilDestroyed(this.destroyRef))
.subscribe(() => { .subscribe(() => {
this.router.navigate(['.'], { relativeTo: this.route }); this.router.navigate(['.'], { relativeTo: this.route });
}); });

4
libs/ui/src/lib/holdings-table/holdings-table.component.html

@ -193,7 +193,7 @@
</table> </table>
</div> </div>
<mat-paginator class="d-none" [pageSize]="pageSize" /> <mat-paginator class="d-none" [pageSize]="pageSize()" />
@if (isLoading()) { @if (isLoading()) {
<ngx-skeleton-loader <ngx-skeleton-loader
@ -206,7 +206,7 @@
/> />
} }
@if (dataSource.data.length > pageSize && !isLoading()) { @if (dataSource.data.length > pageSize() && !isLoading()) {
<div class="my-3 text-center"> <div class="my-3 text-center">
<button mat-stroked-button (click)="onShowAllHoldings()"> <button mat-stroked-button (click)="onShowAllHoldings()">
<ng-container i18n>Show all</ng-container> <ng-container i18n>Show all</ng-container>

19
libs/ui/src/lib/holdings-table/holdings-table.component.ts

@ -9,12 +9,11 @@ import {
CUSTOM_ELEMENTS_SCHEMA, CUSTOM_ELEMENTS_SCHEMA,
ChangeDetectionStrategy, ChangeDetectionStrategy,
Component, Component,
EventEmitter,
Input,
Output,
computed, computed,
effect, effect,
input, input,
model,
output,
viewChild viewChild
} from '@angular/core'; } from '@angular/core';
import { MatButtonModule } from '@angular/material/button'; import { MatButtonModule } from '@angular/material/button';
@ -47,17 +46,17 @@ import { GfValueComponent } from '../value/value.component';
templateUrl: './holdings-table.component.html' templateUrl: './holdings-table.component.html'
}) })
export class GfHoldingsTableComponent { export class GfHoldingsTableComponent {
@Input() pageSize = Number.MAX_SAFE_INTEGER;
@Output() holdingClicked = new EventEmitter<AssetProfileIdentifier>();
public readonly hasPermissionToOpenDetails = input(true); public readonly hasPermissionToOpenDetails = input(true);
public readonly hasPermissionToShowQuantities = input(true); public readonly hasPermissionToShowQuantities = input(true);
public readonly hasPermissionToShowValues = input(true); public readonly hasPermissionToShowValues = input(true);
public readonly holdings = input.required<PortfolioPosition[]>(); public readonly holdings = input.required<PortfolioPosition[]>();
public readonly locale = input(getLocale()); public readonly locale = input(getLocale());
public readonly paginator = viewChild.required(MatPaginator); public readonly pageSize = model(Number.MAX_SAFE_INTEGER);
public readonly sort = viewChild.required(MatSort);
public readonly holdingClicked = output<AssetProfileIdentifier>();
protected readonly paginator = viewChild.required(MatPaginator);
protected readonly sort = viewChild.required(MatSort);
protected readonly dataSource = new MatTableDataSource<PortfolioPosition>([]); protected readonly dataSource = new MatTableDataSource<PortfolioPosition>([]);
@ -118,7 +117,7 @@ export class GfHoldingsTableComponent {
} }
protected onShowAllHoldings() { protected onShowAllHoldings() {
this.pageSize = Number.MAX_SAFE_INTEGER; this.pageSize.set(Number.MAX_SAFE_INTEGER);
setTimeout(() => { setTimeout(() => {
this.dataSource.paginator = this.paginator(); this.dataSource.paginator = this.paginator();

7
libs/ui/src/lib/toggle/toggle.component.html

@ -3,13 +3,14 @@
[formControl]="optionFormControl" [formControl]="optionFormControl"
(change)="onValueChange()" (change)="onValueChange()"
> >
@for (option of options; track option) { @for (option of options(); track option) {
<mat-radio-button <mat-radio-button
class="d-inline-flex" class="d-inline-flex"
[disabled]="isLoading" [disabled]="isLoading()"
[ngClass]="{ [ngClass]="{
'cursor-default': option.value === optionFormControl.value, 'cursor-default': option.value === optionFormControl.value,
'cursor-pointer': !isLoading && option.value !== optionFormControl.value 'cursor-pointer':
!isLoading() && option.value !== optionFormControl.value
}" }"
[value]="option.value" [value]="option.value"
>{{ option.label }}</mat-radio-button >{{ option.label }}</mat-radio-button

32
libs/ui/src/lib/toggle/toggle.component.ts

@ -4,10 +4,9 @@ import { CommonModule } from '@angular/common';
import { import {
ChangeDetectionStrategy, ChangeDetectionStrategy,
Component, Component,
EventEmitter, effect,
Input, input,
OnChanges, output
Output
} from '@angular/core'; } from '@angular/core';
import { FormControl, ReactiveFormsModule } from '@angular/forms'; import { FormControl, ReactiveFormsModule } from '@angular/forms';
import { MatRadioModule } from '@angular/material/radio'; import { MatRadioModule } from '@angular/material/radio';
@ -19,20 +18,25 @@ import { MatRadioModule } from '@angular/material/radio';
styleUrls: ['./toggle.component.scss'], styleUrls: ['./toggle.component.scss'],
templateUrl: './toggle.component.html' templateUrl: './toggle.component.html'
}) })
export class GfToggleComponent implements OnChanges { export class GfToggleComponent {
@Input() defaultValue: string; public readonly defaultValue = input.required<string>();
@Input() isLoading: boolean; public readonly isLoading = input<boolean>(false);
@Input() options: ToggleOption[] = []; public readonly options = input<ToggleOption[]>([]);
@Output() valueChange = new EventEmitter<Pick<ToggleOption, 'value'>>(); protected readonly optionFormControl = new FormControl<string | null>(null);
protected readonly valueChange = output<Pick<ToggleOption, 'value'>>();
public optionFormControl = new FormControl<string>(undefined); public constructor() {
effect(() => {
public ngOnChanges() { this.optionFormControl.setValue(this.defaultValue());
this.optionFormControl.setValue(this.defaultValue); });
} }
public onValueChange() { public onValueChange() {
this.valueChange.emit({ value: this.optionFormControl.value }); const value = this.optionFormControl.value;
if (value !== null) {
this.valueChange.emit({ value });
}
} }
} }

Loading…
Cancel
Save