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. 147
      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 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`
## 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 {
Controller,
Get,
OnModuleInit,
Param,
Res,
Version,
@ -14,12 +15,14 @@ import { readFileSync } from 'node:fs';
import { join } from 'node:path';
@Controller('assets')
export class AssetsController {
export class AssetsController implements OnModuleInit {
private webManifest = '';
public constructor(
public readonly configurationService: ConfigurationService
) {
) {}
public onModuleInit() {
try {
this.webManifest = readFileSync(
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 { Injectable, Logger } from '@nestjs/common';
import { Injectable, Logger, OnModuleInit } from '@nestjs/common';
import * as cheerio from 'cheerio';
import { readFileSync, readdirSync } from 'node:fs';
import { join } from 'node:path';
@Injectable()
export class I18nService {
export class I18nService implements OnModuleInit {
private localesPath = join(__dirname, 'assets', 'locales');
private translations: { [locale: string]: cheerio.CheerioAPI } = {};
public constructor() {
public onModuleInit() {
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 { HoldingDetailDialogParams } from './components/holding-detail-dialog/interfaces/interfaces';
import { ImpersonationStorageService } from './services/impersonation-storage.service';
import { TokenStorageService } from './services/token-storage.service';
import { UserService } from './services/user/user.service';
@Component({
@ -82,7 +81,6 @@ export class GfAppComponent implements OnDestroy, OnInit {
private route: ActivatedRoute,
private router: Router,
private title: Title,
private tokenStorageService: TokenStorageService,
private userService: UserService
) {
this.initializeTheme();
@ -236,12 +234,11 @@ export class GfAppComponent implements OnDestroy, OnInit {
}
public onCreateAccount() {
this.tokenStorageService.signOut();
this.userService.signOut();
}
public onSignOut() {
this.tokenStorageService.signOut();
this.userService.remove();
this.userService.signOut();
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() {
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');
}

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

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

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

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

@ -17,12 +17,11 @@ import {
ChangeDetectorRef,
Component,
CUSTOM_ELEMENTS_SCHEMA,
OnDestroy,
DestroyRef,
OnInit
} from '@angular/core';
import { takeUntilDestroyed } from '@angular/core/rxjs-interop';
import { DeviceDetectorService } from 'ngx-device-detector';
import { Subject } from 'rxjs';
import { takeUntil } from 'rxjs/operators';
@Component({
imports: [
@ -35,7 +34,7 @@ import { takeUntil } from 'rxjs/operators';
styleUrls: ['./home-market.scss'],
templateUrl: './home-market.html'
})
export class GfHomeMarketComponent implements OnDestroy, OnInit {
export class GfHomeMarketComponent implements OnInit {
public benchmarks: Benchmark[];
public deviceType: string;
public fearAndGreedIndex: number;
@ -47,11 +46,10 @@ export class GfHomeMarketComponent implements OnDestroy, OnInit {
public readonly numberOfDays = 365;
public user: User;
private unsubscribeSubject = new Subject<void>();
public constructor(
private changeDetectorRef: ChangeDetectorRef,
private dataService: DataService,
private destroyRef: DestroyRef,
private deviceService: DeviceDetectorService,
private userService: UserService
) {
@ -59,7 +57,7 @@ export class GfHomeMarketComponent implements OnDestroy, OnInit {
this.info = this.dataService.fetchInfo();
this.userService.stateChanged
.pipe(takeUntil(this.unsubscribeSubject))
.pipe(takeUntilDestroyed(this.destroyRef))
.subscribe((state) => {
if (state?.user) {
this.user = state.user;
@ -82,7 +80,7 @@ export class GfHomeMarketComponent implements OnDestroy, OnInit {
includeHistoricalData: this.numberOfDays,
symbol: ghostfolioFearAndGreedIndexSymbol
})
.pipe(takeUntil(this.unsubscribeSubject))
.pipe(takeUntilDestroyed(this.destroyRef))
.subscribe(({ historicalData, marketPrice }) => {
this.fearAndGreedIndex = marketPrice;
this.historicalDataItems = [
@ -99,16 +97,11 @@ export class GfHomeMarketComponent implements OnDestroy, OnInit {
this.dataService
.fetchBenchmarks()
.pipe(takeUntil(this.unsubscribeSubject))
.pipe(takeUntilDestroyed(this.destroyRef))
.subscribe(({ benchmarks }) => {
this.benchmarks = benchmarks;
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,
Component,
CUSTOM_ELEMENTS_SCHEMA,
OnDestroy,
DestroyRef,
OnInit
} from '@angular/core';
import { takeUntilDestroyed } from '@angular/core/rxjs-interop';
import { MatButtonModule } from '@angular/material/button';
import { RouterModule } from '@angular/router';
import { DeviceDetectorService } from 'ngx-device-detector';
import { Subject } from 'rxjs';
import { takeUntil } from 'rxjs/operators';
@Component({
imports: [
@ -41,7 +40,7 @@ import { takeUntil } from 'rxjs/operators';
styleUrls: ['./home-overview.scss'],
templateUrl: './home-overview.html'
})
export class GfHomeOverviewComponent implements OnDestroy, OnInit {
export class GfHomeOverviewComponent implements OnInit {
public deviceType: string;
public errors: AssetProfileIdentifier[];
public hasError: boolean;
@ -62,18 +61,17 @@ export class GfHomeOverviewComponent implements OnDestroy, OnInit {
public unit: string;
public user: User;
private unsubscribeSubject = new Subject<void>();
public constructor(
private changeDetectorRef: ChangeDetectorRef,
private dataService: DataService,
private destroyRef: DestroyRef,
private deviceService: DeviceDetectorService,
private impersonationStorageService: ImpersonationStorageService,
private layoutService: LayoutService,
private userService: UserService
) {
this.userService.stateChanged
.pipe(takeUntil(this.unsubscribeSubject))
.pipe(takeUntilDestroyed(this.destroyRef))
.subscribe((state) => {
if (state?.user) {
this.user = state.user;
@ -99,7 +97,7 @@ export class GfHomeOverviewComponent implements OnDestroy, OnInit {
this.impersonationStorageService
.onChangeHasImpersonation()
.pipe(takeUntil(this.unsubscribeSubject))
.pipe(takeUntilDestroyed(this.destroyRef))
.subscribe((impersonationId) => {
this.hasImpersonationId = !!impersonationId;
@ -107,17 +105,12 @@ export class GfHomeOverviewComponent implements OnDestroy, OnInit {
});
this.layoutService.shouldReloadContent$
.pipe(takeUntil(this.unsubscribeSubject))
.pipe(takeUntilDestroyed(this.destroyRef))
.subscribe(() => {
this.update();
});
}
public ngOnDestroy() {
this.unsubscribeSubject.next();
this.unsubscribeSubject.complete();
}
private update() {
this.historicalDataItems = null;
this.isLoadingPerformance = true;
@ -126,7 +119,7 @@ export class GfHomeOverviewComponent implements OnDestroy, OnInit {
.fetchPortfolioPerformance({
range: this.user?.settings?.dateRange
})
.pipe(takeUntil(this.unsubscribeSubject))
.pipe(takeUntilDestroyed(this.destroyRef))
.subscribe(({ chart, errors, performance }) => {
this.errors = errors;
this.performance = performance;

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

@ -13,14 +13,13 @@ import {
ChangeDetectorRef,
Component,
CUSTOM_ELEMENTS_SCHEMA,
OnDestroy,
DestroyRef,
OnInit
} from '@angular/core';
import { takeUntilDestroyed } from '@angular/core/rxjs-interop';
import { MatCardModule } from '@angular/material/card';
import { MatSnackBarRef, TextOnlySnackBar } from '@angular/material/snack-bar';
import { DeviceDetectorService } from 'ngx-device-detector';
import { Subject } from 'rxjs';
import { takeUntil } from 'rxjs/operators';
@Component({
imports: [GfPortfolioSummaryComponent, MatCardModule],
@ -29,7 +28,7 @@ import { takeUntil } from 'rxjs/operators';
styleUrls: ['./home-summary.scss'],
templateUrl: './home-summary.html'
})
export class GfHomeSummaryComponent implements OnDestroy, OnInit {
export class GfHomeSummaryComponent implements OnInit {
public deviceType: string;
public hasImpersonationId: boolean;
public hasPermissionForSubscription: boolean;
@ -40,11 +39,10 @@ export class GfHomeSummaryComponent implements OnDestroy, OnInit {
public summary: PortfolioSummary;
public user: User;
private unsubscribeSubject = new Subject<void>();
public constructor(
private changeDetectorRef: ChangeDetectorRef,
private dataService: DataService,
private destroyRef: DestroyRef,
private deviceService: DeviceDetectorService,
private impersonationStorageService: ImpersonationStorageService,
private userService: UserService
@ -57,7 +55,7 @@ export class GfHomeSummaryComponent implements OnDestroy, OnInit {
);
this.userService.stateChanged
.pipe(takeUntil(this.unsubscribeSubject))
.pipe(takeUntilDestroyed(this.destroyRef))
.subscribe((state) => {
if (state?.user) {
this.user = state.user;
@ -77,7 +75,7 @@ export class GfHomeSummaryComponent implements OnDestroy, OnInit {
this.impersonationStorageService
.onChangeHasImpersonation()
.pipe(takeUntil(this.unsubscribeSubject))
.pipe(takeUntilDestroyed(this.destroyRef))
.subscribe((impersonationId) => {
this.hasImpersonationId = !!impersonationId;
});
@ -86,11 +84,11 @@ export class GfHomeSummaryComponent implements OnDestroy, OnInit {
public onChangeEmergencyFund(emergencyFund: number) {
this.dataService
.putUserSetting({ emergencyFund })
.pipe(takeUntil(this.unsubscribeSubject))
.pipe(takeUntilDestroyed(this.destroyRef))
.subscribe(() => {
this.userService
.get(true)
.pipe(takeUntil(this.unsubscribeSubject))
.pipe(takeUntilDestroyed(this.destroyRef))
.subscribe((user) => {
this.user = user;
@ -99,17 +97,12 @@ export class GfHomeSummaryComponent implements OnDestroy, OnInit {
});
}
public ngOnDestroy() {
this.unsubscribeSubject.next();
this.unsubscribeSubject.complete();
}
private update() {
this.isLoading = true;
this.dataService
.fetchPortfolioDetails()
.pipe(takeUntil(this.unsubscribeSubject))
.pipe(takeUntilDestroyed(this.destroyRef))
.subscribe(({ summary }) => {
this.summary = summary;
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 { TokenStorageService } from '@ghostfolio/client/services/token-storage.service';
import { UserService } from '@ghostfolio/client/services/user/user.service';
import { CreateAccessDto } from '@ghostfolio/common/dtos';
import { ConfirmationDialogType } from '@ghostfolio/common/enums';
@ -76,7 +75,6 @@ export class GfUserAccountAccessComponent implements OnDestroy, OnInit {
private notificationService: NotificationService,
private route: ActivatedRoute,
private router: Router,
private tokenStorageService: TokenStorageService,
private userService: UserService
) {
const { globalPermissions } = this.dataService.fetchInfo();
@ -161,8 +159,7 @@ export class GfUserAccountAccessComponent implements OnDestroy, OnInit {
.subscribe(({ accessToken }) => {
this.notificationService.alert({
discardFn: () => {
this.tokenStorageService.signOut();
this.userService.remove();
this.userService.signOut();
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,
SettingsStorageService
} 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 { WebAuthnService } from '@ghostfolio/client/services/web-authn.service';
import { ConfirmationDialogType } from '@ghostfolio/common/enums';
@ -108,7 +107,6 @@ export class GfUserAccountSettingsComponent implements OnDestroy, OnInit {
private notificationService: NotificationService,
private settingsStorageService: SettingsStorageService,
private snackBar: MatSnackBar,
private tokenStorageService: TokenStorageService,
private userService: UserService,
public webAuthnService: WebAuthnService
) {
@ -198,8 +196,7 @@ export class GfUserAccountSettingsComponent implements OnDestroy, OnInit {
takeUntil(this.unsubscribeSubject)
)
.subscribe(() => {
this.tokenStorageService.signOut();
this.userService.remove();
this.userService.signOut();
document.location.href = `/${document.documentElement.lang}`;
});

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

@ -68,7 +68,7 @@ export class AuthGuard {
this.dataService
.putUserSetting({ language: document.documentElement.lang })
.subscribe(() => {
this.userService.remove();
this.userService.reset();
setTimeout(() => {
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 { InfoItem } from '@ghostfolio/common/interfaces';
import { internalRoutes, publicRoutes } from '@ghostfolio/common/routes/routes';
@ -32,8 +32,8 @@ export class HttpResponseInterceptor implements HttpInterceptor {
public constructor(
private dataService: DataService,
private router: Router,
private tokenStorageService: TokenStorageService,
private snackBar: MatSnackBar,
private userService: UserService,
private webAuthnService: WebAuthnService
) {
this.info = this.dataService.fetchInfo();
@ -115,7 +115,7 @@ export class HttpResponseInterceptor implements HttpInterceptor {
if (this.webAuthnService.isEnabled()) {
this.router.navigate(internalRoutes.webauthn.routerLink);
} else {
this.tokenStorageService.signOut();
this.userService.signOut();
}
}
}

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

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

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

@ -24,10 +24,11 @@ import { Clipboard } from '@angular/cdk/clipboard';
import {
ChangeDetectorRef,
Component,
OnDestroy,
DestroyRef,
OnInit,
ViewChild
} from '@angular/core';
import { takeUntilDestroyed } from '@angular/core/rxjs-interop';
import { MatButtonModule } from '@angular/material/button';
import { MatCardModule } from '@angular/material/card';
import { MatMenuModule, MatMenuTrigger } from '@angular/material/menu';
@ -42,8 +43,6 @@ import { isNumber, sortBy } from 'lodash';
import ms from 'ms';
import { DeviceDetectorService } from 'ngx-device-detector';
import { NgxSkeletonLoaderModule } from 'ngx-skeleton-loader';
import { Subject } from 'rxjs';
import { takeUntil } from 'rxjs/operators';
@Component({
imports: [
@ -64,7 +63,7 @@ import { takeUntil } from 'rxjs/operators';
styleUrls: ['./analysis-page.scss'],
templateUrl: './analysis-page.html'
})
export class GfAnalysisPageComponent implements OnDestroy, OnInit {
export class GfAnalysisPageComponent implements OnInit {
@ViewChild(MatMenuTrigger) actionsMenuButton!: MatMenuTrigger;
public benchmark: Partial<SymbolProfile>;
@ -102,12 +101,11 @@ export class GfAnalysisPageComponent implements OnDestroy, OnInit {
public unitLongestStreak: string;
public user: User;
private unsubscribeSubject = new Subject<void>();
public constructor(
private changeDetectorRef: ChangeDetectorRef,
private clipboard: Clipboard,
private dataService: DataService,
private destroyRef: DestroyRef,
private deviceService: DeviceDetectorService,
private impersonationStorageService: ImpersonationStorageService,
private snackBar: MatSnackBar,
@ -135,13 +133,13 @@ export class GfAnalysisPageComponent implements OnDestroy, OnInit {
this.impersonationStorageService
.onChangeHasImpersonation()
.pipe(takeUntil(this.unsubscribeSubject))
.pipe(takeUntilDestroyed(this.destroyRef))
.subscribe((impersonationId) => {
this.hasImpersonationId = !!impersonationId;
});
this.userService.stateChanged
.pipe(takeUntil(this.unsubscribeSubject))
.pipe(takeUntilDestroyed(this.destroyRef))
.subscribe((state) => {
if (state?.user) {
this.user = state.user;
@ -163,11 +161,11 @@ export class GfAnalysisPageComponent implements OnDestroy, OnInit {
public onChangeBenchmark(symbolProfileId: string) {
this.dataService
.putUserSetting({ benchmark: symbolProfileId })
.pipe(takeUntil(this.unsubscribeSubject))
.pipe(takeUntilDestroyed(this.destroyRef))
.subscribe(() => {
this.userService
.get(true)
.pipe(takeUntil(this.unsubscribeSubject))
.pipe(takeUntilDestroyed(this.destroyRef))
.subscribe((user) => {
this.user = user;
@ -193,7 +191,7 @@ export class GfAnalysisPageComponent implements OnDestroy, OnInit {
mode,
filters: this.userService.getFilters()
})
.pipe(takeUntil(this.unsubscribeSubject))
.pipe(takeUntilDestroyed(this.destroyRef))
.subscribe(({ prompt }) => {
this.clipboard.copy(prompt);
@ -207,7 +205,7 @@ export class GfAnalysisPageComponent implements OnDestroy, OnInit {
snackBarRef
.onAction()
.pipe(takeUntil(this.unsubscribeSubject))
.pipe(takeUntilDestroyed(this.destroyRef))
.subscribe(() => {
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() {
this.isLoadingDividendTimelineChart = true;
this.isLoadingInvestmentTimelineChart = true;
@ -237,7 +230,7 @@ export class GfAnalysisPageComponent implements OnDestroy, OnInit {
groupBy: this.mode,
range: this.user?.settings?.dateRange
})
.pipe(takeUntil(this.unsubscribeSubject))
.pipe(takeUntilDestroyed(this.destroyRef))
.subscribe(({ dividends }) => {
this.dividendsByGroup = dividends;
@ -252,7 +245,7 @@ export class GfAnalysisPageComponent implements OnDestroy, OnInit {
groupBy: this.mode,
range: this.user?.settings?.dateRange
})
.pipe(takeUntil(this.unsubscribeSubject))
.pipe(takeUntilDestroyed(this.destroyRef))
.subscribe(({ investments, streaks }) => {
this.investmentsByGroup = investments;
this.streaks = streaks;
@ -287,7 +280,7 @@ export class GfAnalysisPageComponent implements OnDestroy, OnInit {
filters: this.userService.getFilters(),
range: this.user?.settings?.dateRange
})
.pipe(takeUntil(this.unsubscribeSubject))
.pipe(takeUntilDestroyed(this.destroyRef))
.subscribe(({ chart, firstOrderDate, performance }) => {
this.firstOrderDate = firstOrderDate ?? new Date();
@ -346,7 +339,7 @@ export class GfAnalysisPageComponent implements OnDestroy, OnInit {
filters: this.userService.getFilters(),
range: this.user?.settings?.dateRange
})
.pipe(takeUntil(this.unsubscribeSubject))
.pipe(takeUntilDestroyed(this.destroyRef))
.subscribe(({ holdings }) => {
const holdingsSorted = sortBy(
holdings.filter(({ netPerformancePercentWithCurrencyEffect }) => {
@ -397,7 +390,7 @@ export class GfAnalysisPageComponent implements OnDestroy, OnInit {
range: this.user?.settings?.dateRange,
startDate: this.firstOrderDate
})
.pipe(takeUntil(this.unsubscribeSubject))
.pipe(takeUntilDestroyed(this.destroyRef))
.subscribe(({ marketData }) => {
this.benchmarkDataItems = marketData.map(({ date, value }) => {
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 { CommonModule, NgStyle } from '@angular/common';
import { ChangeDetectorRef, Component, OnDestroy, OnInit } from '@angular/core';
import {
ChangeDetectorRef,
Component,
DestroyRef,
OnInit
} from '@angular/core';
import { takeUntilDestroyed } from '@angular/core/rxjs-interop';
import { FormsModule, ReactiveFormsModule } from '@angular/forms';
import { FormControl } from '@angular/forms';
import { Big } from 'big.js';
import { DeviceDetectorService } from 'ngx-device-detector';
import { NgxSkeletonLoaderModule } from 'ngx-skeleton-loader';
import { Subject } from 'rxjs';
import { takeUntil } from 'rxjs/operators';
@Component({
imports: [
@ -36,7 +40,7 @@ import { takeUntil } from 'rxjs/operators';
styleUrls: ['./fire-page.scss'],
templateUrl: './fire-page.html'
})
export class GfFirePageComponent implements OnDestroy, OnInit {
export class GfFirePageComponent implements OnInit {
public deviceType: string;
public fireWealth: FireWealth;
public hasImpersonationId: boolean;
@ -52,11 +56,10 @@ export class GfFirePageComponent implements OnDestroy, OnInit {
public withdrawalRatePerYear: Big;
public withdrawalRatePerYearProjected: Big;
private unsubscribeSubject = new Subject<void>();
public constructor(
private changeDetectorRef: ChangeDetectorRef,
private dataService: DataService,
private destroyRef: DestroyRef,
private deviceService: DeviceDetectorService,
private impersonationStorageService: ImpersonationStorageService,
private userService: UserService
@ -68,7 +71,7 @@ export class GfFirePageComponent implements OnDestroy, OnInit {
this.dataService
.fetchPortfolioDetails()
.pipe(takeUntil(this.unsubscribeSubject))
.pipe(takeUntilDestroyed(this.destroyRef))
.subscribe(({ summary }) => {
this.fireWealth = {
today: {
@ -92,19 +95,19 @@ export class GfFirePageComponent implements OnDestroy, OnInit {
this.impersonationStorageService
.onChangeHasImpersonation()
.pipe(takeUntil(this.unsubscribeSubject))
.pipe(takeUntilDestroyed(this.destroyRef))
.subscribe((impersonationId) => {
this.hasImpersonationId = !!impersonationId;
});
this.safeWithdrawalRateControl.valueChanges
.pipe(takeUntil(this.unsubscribeSubject))
.pipe(takeUntilDestroyed(this.destroyRef))
.subscribe((value) => {
this.onSafeWithdrawalRateChange(Number(value));
});
this.userService.stateChanged
.pipe(takeUntil(this.unsubscribeSubject))
.pipe(takeUntilDestroyed(this.destroyRef))
.subscribe((state) => {
if (state?.user) {
this.user = state.user;
@ -132,11 +135,11 @@ export class GfFirePageComponent implements OnDestroy, OnInit {
public onAnnualInterestRateChange(annualInterestRate: number) {
this.dataService
.putUserSetting({ annualInterestRate })
.pipe(takeUntil(this.unsubscribeSubject))
.pipe(takeUntilDestroyed(this.destroyRef))
.subscribe(() => {
this.userService
.get(true)
.pipe(takeUntil(this.unsubscribeSubject))
.pipe(takeUntilDestroyed(this.destroyRef))
.subscribe((user) => {
this.user = user;
@ -163,11 +166,11 @@ export class GfFirePageComponent implements OnDestroy, OnInit {
retirementDate: retirementDate.toISOString(),
projectedTotalAmount: null
})
.pipe(takeUntil(this.unsubscribeSubject))
.pipe(takeUntilDestroyed(this.destroyRef))
.subscribe(() => {
this.userService
.get(true)
.pipe(takeUntil(this.unsubscribeSubject))
.pipe(takeUntilDestroyed(this.destroyRef))
.subscribe((user) => {
this.user = user;
@ -179,11 +182,11 @@ export class GfFirePageComponent implements OnDestroy, OnInit {
public onSafeWithdrawalRateChange(safeWithdrawalRate: number) {
this.dataService
.putUserSetting({ safeWithdrawalRate })
.pipe(takeUntil(this.unsubscribeSubject))
.pipe(takeUntilDestroyed(this.destroyRef))
.subscribe(() => {
this.userService
.get(true)
.pipe(takeUntil(this.unsubscribeSubject))
.pipe(takeUntilDestroyed(this.destroyRef))
.subscribe((user) => {
this.user = user;
@ -198,11 +201,11 @@ export class GfFirePageComponent implements OnDestroy, OnInit {
public onSavingsRateChange(savingsRate: number) {
this.dataService
.putUserSetting({ savingsRate })
.pipe(takeUntil(this.unsubscribeSubject))
.pipe(takeUntilDestroyed(this.destroyRef))
.subscribe(() => {
this.userService
.get(true)
.pipe(takeUntil(this.unsubscribeSubject))
.pipe(takeUntilDestroyed(this.destroyRef))
.subscribe((user) => {
this.user = user;
@ -217,11 +220,11 @@ export class GfFirePageComponent implements OnDestroy, OnInit {
projectedTotalAmount,
retirementDate: null
})
.pipe(takeUntil(this.unsubscribeSubject))
.pipe(takeUntilDestroyed(this.destroyRef))
.subscribe(() => {
this.userService
.get(true)
.pipe(takeUntil(this.unsubscribeSubject))
.pipe(takeUntilDestroyed(this.destroyRef))
.subscribe((user) => {
this.user = user;
@ -230,11 +233,6 @@ export class GfFirePageComponent implements OnDestroy, OnInit {
});
}
public ngOnDestroy() {
this.unsubscribeSubject.next();
this.unsubscribeSubject.complete();
}
private calculateWithdrawalRates() {
if (this.fireWealth && this.user?.settings?.safeWithdrawalRate) {
this.withdrawalRatePerYear = new Big(

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

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 { UserService } from '@ghostfolio/client/services/user/user.service';
import { InfoItem, LineChartItem } from '@ghostfolio/common/interfaces';
import { hasPermission, permissions } from '@ghostfolio/common/permissions';
import { GfLogoComponent } from '@ghostfolio/ui/logo';
@ -42,11 +43,12 @@ export class GfRegisterPageComponent implements OnInit {
private deviceService: DeviceDetectorService,
private dialog: MatDialog,
private router: Router,
private tokenStorageService: TokenStorageService
private tokenStorageService: TokenStorageService,
private userService: UserService
) {
this.info = this.dataService.fetchInfo();
this.tokenStorageService.signOut();
this.userService.signOut();
}
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 { KEY_TOKEN } from './settings-storage.service';
import { UserService } from './user/user.service';
@Injectable({
providedIn: 'root'
})
export class TokenStorageService {
public constructor(
private userService: UserService,
private webAuthnService: WebAuthnService
) {}
public getToken(): string {
return (
window.sessionStorage.getItem(KEY_TOKEN) ||
@ -25,23 +17,7 @@ export class TokenStorageService {
if (staySignedIn) {
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()) {
this.webAuthnService.deregister().subscribe();
}
window.localStorage.clear();
window.sessionStorage.clear();
this.userService.remove();
if (utmSource) {
window.localStorage.setItem('utm_source', utmSource);
}
window.sessionStorage.setItem(KEY_TOKEN, token);
}
}

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 { hasPermission, permissions } from '@ghostfolio/common/permissions';
@ -26,7 +27,8 @@ export class UserService extends ObservableStore<UserStoreState> {
public constructor(
private deviceService: DeviceDetectorService,
private dialog: MatDialog,
private http: HttpClient
private http: HttpClient,
private webAuthnService: WebAuthnService
) {
super({ trackStateHistory: true });
@ -93,10 +95,40 @@ export class UserService extends ObservableStore<UserStoreState> {
return this.getFilters().length > 0;
}
public remove() {
public reset() {
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> {
return this.http.get<any>('/api/v1/user').pipe(
map((user) => {

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

@ -660,7 +660,7 @@
</trans-unit>
<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>
<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 context-type="sourcefile">apps/client/src/app/pages/about/overview/about-overview-page.html</context>
<context context-type="linenumber">49</context>
@ -1580,7 +1580,7 @@
</trans-unit>
<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>
<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 context-type="sourcefile">apps/client/src/app/pages/about/overview/about-overview-page.html</context>
<context context-type="linenumber">16</context>
@ -2148,7 +2148,7 @@
</trans-unit>
<trans-unit id="366169681580494481" datatype="html">
<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 context-type="sourcefile">apps/client/src/app/pages/portfolio/analysis/analysis-page.html</context>
<context context-type="linenumber">135</context>
@ -2376,7 +2376,7 @@
</trans-unit>
<trans-unit id="3004519800638083911" datatype="html">
<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 context-type="sourcefile">apps/client/src/app/pages/portfolio/fire/fire-page.html</context>
<context context-type="linenumber">147</context>
@ -3564,7 +3564,7 @@
</trans-unit>
<trans-unit id="70768492340592330" datatype="html">
<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 context-type="sourcefile">apps/client/src/app/pages/portfolio/fire/fire-page.html</context>
<context context-type="linenumber">108</context>
@ -4124,7 +4124,7 @@
</trans-unit>
<trans-unit id="5211792611718918888" datatype="html">
<source>annual interest rate</source>
<target state="new">annual interest rate</target>
<target state="translated">rocznej stopy zwrotu</target>
<context-group purpose="location">
<context context-type="sourcefile">apps/client/src/app/pages/portfolio/fire/fire-page.html</context>
<context context-type="linenumber">185</context>
@ -4464,7 +4464,7 @@
</trans-unit>
<trans-unit id="2003818202621229370" datatype="html">
<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 context-type="sourcefile">apps/client/src/app/pages/portfolio/fire/fire-page.html</context>
<context context-type="linenumber">41</context>
@ -4597,7 +4597,7 @@
</trans-unit>
<trans-unit id="4905798562247431262" datatype="html">
<source>per month</source>
<target state="new">per month</target>
<target state="translated">miesięcznie</target>
<context-group purpose="location">
<context context-type="sourcefile">apps/client/src/app/pages/portfolio/fire/fire-page.html</context>
<context context-type="linenumber">94</context>
@ -5397,7 +5397,7 @@
</trans-unit>
<trans-unit id="1468015720862673946" datatype="html">
<source>View Details</source>
<target state="new">View Details</target>
<target state="translated">Zobacz szczegóły</target>
<context-group purpose="location">
<context context-type="sourcefile">apps/client/src/app/components/admin-users/admin-users.html</context>
<context context-type="linenumber">225</context>
@ -5425,7 +5425,7 @@
</trans-unit>
<trans-unit id="2149165958319691680" datatype="html">
<source>Buy</source>
<target state="translated">Zakup</target>
<target state="translated">Kupno</target>
<context-group purpose="location">
<context context-type="sourcefile">apps/client/src/app/components/portfolio-summary/portfolio-summary.component.html</context>
<context context-type="linenumber">31</context>
@ -5469,7 +5469,7 @@
</trans-unit>
<trans-unit id="4881880242577556" datatype="html">
<source>Sell</source>
<target state="translated">Sprzedaj</target>
<target state="translated">Sprzedaż</target>
<context-group purpose="location">
<context context-type="sourcefile">apps/client/src/app/components/portfolio-summary/portfolio-summary.component.html</context>
<context context-type="linenumber">44</context>
@ -5625,7 +5625,7 @@
</trans-unit>
<trans-unit id="3227075298129844075" datatype="html">
<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 context-type="sourcefile">apps/client/src/app/pages/portfolio/fire/fire-page.html</context>
<context context-type="linenumber">68</context>
@ -6710,7 +6710,7 @@
</trans-unit>
<trans-unit id="1355312194390410495" datatype="html">
<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 context-type="sourcefile">apps/client/src/app/pages/portfolio/fire/fire-page.html</context>
<context context-type="linenumber">96</context>
@ -6974,7 +6974,7 @@
</trans-unit>
<trans-unit id="2878377610946588870" datatype="html">
<source>, assuming a</source>
<target state="new">, assuming a</target>
<target state="translated">, przyjmując</target>
<context-group purpose="location">
<context context-type="sourcefile">apps/client/src/app/pages/portfolio/fire/fire-page.html</context>
<context context-type="linenumber">174</context>
@ -7062,7 +7062,7 @@
</trans-unit>
<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>
<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 context-type="sourcefile">apps/client/src/app/pages/about/overview/about-overview-page.html</context>
<context context-type="linenumber">10</context>
@ -7392,7 +7392,7 @@
</trans-unit>
<trans-unit id="7825231215382064101" datatype="html">
<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 context-type="sourcefile">apps/client/src/app/pages/portfolio/analysis/analysis-page.html</context>
<context context-type="linenumber">116</context>
@ -7532,7 +7532,7 @@
</trans-unit>
<trans-unit id="1325095699053123251" datatype="html">
<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 context-type="sourcefile">apps/client/src/app/pages/about/overview/about-overview-page.html</context>
<context context-type="linenumber">40</context>
@ -7556,7 +7556,7 @@
</trans-unit>
<trans-unit id="5004550577313573215" datatype="html">
<source>Total amount</source>
<target state="new">Total amount</target>
<target state="translated">Wartość portfela</target>
<context-group purpose="location">
<context context-type="sourcefile">apps/client/src/app/pages/portfolio/analysis/analysis-page.html</context>
<context context-type="linenumber">95</context>
@ -7898,7 +7898,7 @@
</trans-unit>
<trans-unit id="rule.feeRatioTotalInvestmentVolume" datatype="html">
<source>Fee Ratio</source>
<target state="new">Fee Ratio</target>
<target state="translated">Wskaźnik opłat</target>
<context-group purpose="location">
<context context-type="sourcefile">apps/client/src/app/pages/i18n/i18n-page.html</context>
<context context-type="linenumber">152</context>
@ -8333,7 +8333,7 @@
</trans-unit>
<trans-unit id="rule.accountClusterRisk.category" datatype="html">
<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 context-type="sourcefile">apps/client/src/app/pages/i18n/i18n-page.html</context>
<context context-type="linenumber">14</context>
@ -8341,7 +8341,7 @@
</trans-unit>
<trans-unit id="rule.assetClassClusterRisk.category" datatype="html">
<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 context-type="sourcefile">apps/client/src/app/pages/i18n/i18n-page.html</context>
<context context-type="linenumber">39</context>
@ -8349,7 +8349,7 @@
</trans-unit>
<trans-unit id="rule.currencyClusterRisk.category" datatype="html">
<source>Currency Cluster Risks</source>
<target state="new">Currency Cluster Risks</target>
<target state="translated">Ryzyka koncentracji walutowej</target>
<context-group purpose="location">
<context context-type="sourcefile">apps/client/src/app/pages/i18n/i18n-page.html</context>
<context context-type="linenumber">83</context>
@ -8357,7 +8357,7 @@
</trans-unit>
<trans-unit id="rule.economicMarketClusterRisk.category" datatype="html">
<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 context-type="sourcefile">apps/client/src/app/pages/i18n/i18n-page.html</context>
<context context-type="linenumber">106</context>
@ -8373,7 +8373,7 @@
</trans-unit>
<trans-unit id="rule.fees.category" datatype="html">
<source>Fees</source>
<target state="new">Fees</target>
<target state="translated">Opłaty</target>
<context-group purpose="location">
<context context-type="sourcefile">apps/client/src/app/pages/i18n/i18n-page.html</context>
<context context-type="linenumber">161</context>
@ -8381,7 +8381,7 @@
</trans-unit>
<trans-unit id="rule.liquidity.category" datatype="html">
<source>Liquidity</source>
<target state="new">Liquidity</target>
<target state="translated">Płynność</target>
<context-group purpose="location">
<context context-type="sourcefile">apps/client/src/app/pages/i18n/i18n-page.html</context>
<context context-type="linenumber">70</context>
@ -8389,7 +8389,7 @@
</trans-unit>
<trans-unit id="rule.liquidityBuyingPower" datatype="html">
<source>Buying Power</source>
<target state="new">Buying Power</target>
<target state="translated">Siła nabywcza</target>
<context-group purpose="location">
<context context-type="sourcefile">apps/client/src/app/pages/i18n/i18n-page.html</context>
<context context-type="linenumber">71</context>
@ -8413,7 +8413,7 @@
</trans-unit>
<trans-unit id="rule.liquidityBuyingPower.true" datatype="html">
<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 context-type="sourcefile">apps/client/src/app/pages/i18n/i18n-page.html</context>
<context context-type="linenumber">80</context>
@ -8421,7 +8421,7 @@
</trans-unit>
<trans-unit id="rule.regionalMarketClusterRisk.category" datatype="html">
<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 context-type="sourcefile">apps/client/src/app/pages/i18n/i18n-page.html</context>
<context context-type="linenumber">163</context>
@ -8437,7 +8437,7 @@
</trans-unit>
<trans-unit id="rule.economicMarketClusterRiskDevelopedMarkets" datatype="html">
<source>Developed Markets</source>
<target state="new">Developed Markets</target>
<target state="translated">Rynki rozwinięte</target>
<context-group purpose="location">
<context context-type="sourcefile">apps/client/src/app/pages/i18n/i18n-page.html</context>
<context context-type="linenumber">109</context>
@ -8469,7 +8469,7 @@
</trans-unit>
<trans-unit id="rule.economicMarketClusterRiskEmergingMarkets" datatype="html">
<source>Emerging Markets</source>
<target state="new">Emerging Markets</target>
<target state="translated">Rynki wschodzące</target>
<context-group purpose="location">
<context context-type="sourcefile">apps/client/src/app/pages/i18n/i18n-page.html</context>
<context context-type="linenumber">127</context>
@ -8581,7 +8581,7 @@
</trans-unit>
<trans-unit id="rule.regionalMarketClusterRiskEurope" datatype="html">
<source>Europe</source>
<target state="new">Europe</target>
<target state="translated">Europa</target>
<context-group purpose="location">
<context context-type="sourcefile">apps/client/src/app/pages/i18n/i18n-page.html</context>
<context context-type="linenumber">195</context>
@ -8613,7 +8613,7 @@
</trans-unit>
<trans-unit id="rule.regionalMarketClusterRiskJapan" datatype="html">
<source>Japan</source>
<target state="new">Japan</target>
<target state="translated">Japonia</target>
<context-group purpose="location">
<context context-type="sourcefile">apps/client/src/app/pages/i18n/i18n-page.html</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';
export interface BenchmarkDetailDialogParams {
colorScheme: ColorScheme;
colorScheme?: ColorScheme;
dataSource: DataSource;
deviceType: string;
locale: string;

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

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

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

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

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

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

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

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

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

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

Loading…
Cancel
Save