diff --git a/CHANGELOG.md b/CHANGELOG.md index 9b511a311..6fec332fc 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,10 +7,20 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## Unreleased +### Added + +- Added the quantity column to the holdings table of the portfolio holdings page + ### Changed - Hardened the endpoint `DELETE /api/v1/auth-device/:id` by improving the user validation +- Improved the allocations by ETF holding on the allocations page by refining the grouping of the same assets with diverging names (experimental) - Improved the language localization for Polish (`pl`) +- Upgraded `@trivago/prettier-plugin-sort-imports` from version `5.2.2` to `6.0.2` + +### Fixed + +- Fixed an issue by adding a missing guard in the public access for portfolio sharing ## 2.250.0 - 2026-03-17 diff --git a/apps/client/src/app/app.component.ts b/apps/client/src/app/app.component.ts index 4e83268a4..201c2f994 100644 --- a/apps/client/src/app/app.component.ts +++ b/apps/client/src/app/app.component.ts @@ -13,7 +13,7 @@ import { DestroyRef, DOCUMENT, HostBinding, - Inject, + inject, OnInit } from '@angular/core'; import { takeUntilDestroyed } from '@angular/core/rxjs-interop'; @@ -36,7 +36,7 @@ import { filter } from 'rxjs/operators'; import { GfFooterComponent } from './components/footer/footer.component'; 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 { GfAppQueryParams } from './interfaces/interfaces'; import { ImpersonationStorageService } from './services/impersonation-storage.service'; import { UserService } from './services/user/user.service'; @@ -48,10 +48,6 @@ import { UserService } from './services/user/user.service'; templateUrl: './app.component.html' }) export class GfAppComponent implements OnInit { - @HostBinding('class.has-info-message') get getHasMessage() { - return this.hasInfoMessage; - } - public canCreateAccount: boolean; public currentRoute: string; public currentSubRoute: string; @@ -66,43 +62,47 @@ export class GfAppComponent implements OnInit { public pageTitle: string; public routerLinkRegister = publicRoutes.register.routerLink; public showFooter = false; - public user: User; - - public constructor( - private changeDetectorRef: ChangeDetectorRef, - private dataService: DataService, - private destroyRef: DestroyRef, - private deviceService: DeviceDetectorService, - private dialog: MatDialog, - @Inject(DOCUMENT) private document: Document, - private impersonationStorageService: ImpersonationStorageService, - private notificationService: NotificationService, - private route: ActivatedRoute, - private router: Router, - private title: Title, - private userService: UserService - ) { + public user: User | undefined; + + private readonly changeDetectorRef = inject(ChangeDetectorRef); + private readonly dataService = inject(DataService); + private readonly destroyRef = inject(DestroyRef); + private readonly deviceService = inject(DeviceDetectorService); + private readonly dialog = inject(MatDialog); + private readonly document = inject(DOCUMENT); + private readonly impersonationStorageService = inject( + ImpersonationStorageService + ); + private readonly notificationService = inject(NotificationService); + private readonly route = inject(ActivatedRoute); + private readonly router = inject(Router); + private readonly title = inject(Title); + private readonly userService = inject(UserService); + + public constructor() { this.initializeTheme(); this.user = undefined; this.route.queryParams .pipe(takeUntilDestroyed(this.destroyRef)) - .subscribe((params) => { - if ( - params['dataSource'] && - params['holdingDetailDialog'] && - params['symbol'] - ) { - this.openHoldingDetailDialog({ - dataSource: params['dataSource'], - symbol: params['symbol'] - }); + .subscribe( + ({ dataSource, holdingDetailDialog, symbol }: GfAppQueryParams) => { + if (dataSource && holdingDetailDialog && symbol) { + this.openHoldingDetailDialog({ + dataSource, + symbol + }); + } } - }); + ); addIcons({ openOutline }); } + @HostBinding('class.has-info-message') get getHasMessage() { + return this.hasInfoMessage; + } + public ngOnInit() { this.deviceType = this.deviceService.getDeviceInfo().deviceType; this.info = this.dataService.fetchInfo(); @@ -128,7 +128,7 @@ export class GfAppComponent implements OnInit { !this.currentSubRoute) || (this.currentRoute === internalRoutes.home.path && this.currentSubRoute === - internalRoutes.home.subRoutes.holdings.path) || + internalRoutes.home.subRoutes?.holdings.path) || (this.currentRoute === internalRoutes.portfolio.path && !this.currentSubRoute)) && this.user?.settings?.viewMode !== 'ZEN' @@ -223,11 +223,17 @@ export class GfAppComponent implements OnInit { } public onClickSystemMessage() { - if (this.user.systemMessage.routerLink) { - this.router.navigate(this.user.systemMessage.routerLink); + const systemMessage = this.user?.systemMessage; + + if (!systemMessage) { + return; + } + + if (systemMessage.routerLink) { + void this.router.navigate(systemMessage.routerLink); } else { this.notificationService.alert({ - title: this.user.systemMessage.message + title: systemMessage.message }); } } @@ -269,10 +275,7 @@ export class GfAppComponent implements OnInit { .subscribe((user) => { this.user = user; - const dialogRef = this.dialog.open< - GfHoldingDetailDialogComponent, - HoldingDetailDialogParams - >(GfHoldingDetailDialogComponent, { + const dialogRef = this.dialog.open(GfHoldingDetailDialogComponent, { autoFocus: false, data: { dataSource, @@ -313,7 +316,7 @@ export class GfAppComponent implements OnInit { .afterClosed() .pipe(takeUntilDestroyed(this.destroyRef)) .subscribe(() => { - this.router.navigate([], { + void this.router.navigate([], { queryParams: { dataSource: null, holdingDetailDialog: null, @@ -339,6 +342,6 @@ export class GfAppComponent implements OnInit { this.document .querySelector('meta[name="theme-color"]') - .setAttribute('content', themeColor); + ?.setAttribute('content', themeColor); } } diff --git a/apps/client/src/app/components/holding-detail-dialog/holding-detail-dialog.component.ts b/apps/client/src/app/components/holding-detail-dialog/holding-detail-dialog.component.ts index 4d89cfbe1..13ded73eb 100644 --- a/apps/client/src/app/components/holding-detail-dialog/holding-detail-dialog.component.ts +++ b/apps/client/src/app/components/holding-detail-dialog/holding-detail-dialog.component.ts @@ -35,10 +35,11 @@ import { ChangeDetectionStrategy, ChangeDetectorRef, Component, + DestroyRef, Inject, - OnDestroy, OnInit } from '@angular/core'; +import { takeUntilDestroyed } from '@angular/core/rxjs-interop'; import { FormBuilder, FormGroup, ReactiveFormsModule } from '@angular/forms'; import { MatButtonModule } from '@angular/material/button'; import { MatChipsModule } from '@angular/material/chips'; @@ -67,8 +68,7 @@ import { walletOutline } from 'ionicons/icons'; import { NgxSkeletonLoaderModule } from 'ngx-skeleton-loader'; -import { Subject } from 'rxjs'; -import { switchMap, takeUntil } from 'rxjs/operators'; +import { switchMap } from 'rxjs/operators'; import { HoldingDetailDialogParams } from './interfaces/interfaces'; @@ -102,7 +102,7 @@ import { HoldingDetailDialogParams } from './interfaces/interfaces'; styleUrls: ['./holding-detail-dialog.component.scss'], templateUrl: 'holding-detail-dialog.html' }) -export class GfHoldingDetailDialogComponent implements OnDestroy, OnInit { +export class GfHoldingDetailDialogComponent implements OnInit { public activitiesCount: number; public accounts: Account[]; public assetClass: string; @@ -158,11 +158,10 @@ export class GfHoldingDetailDialogComponent implements OnDestroy, OnInit { public user: User; public value: number; - private unsubscribeSubject = new Subject(); - public constructor( private changeDetectorRef: ChangeDetectorRef, private dataService: DataService, + private destroyRef: DestroyRef, public dialogRef: MatDialogRef, @Inject(MAT_DIALOG_DATA) public data: HoldingDetailDialogParams, private formBuilder: FormBuilder, @@ -192,7 +191,7 @@ export class GfHoldingDetailDialogComponent implements OnDestroy, OnInit { this.holdingForm .get('tags') - .valueChanges.pipe(takeUntil(this.unsubscribeSubject)) + .valueChanges.pipe(takeUntilDestroyed(this.destroyRef)) .subscribe((tags: Tag[]) => { const newTag = tags.find(({ id }) => { return id === undefined; @@ -217,7 +216,7 @@ export class GfHoldingDetailDialogComponent implements OnDestroy, OnInit { switchMap(() => { return this.userService.get(true); }), - takeUntil(this.unsubscribeSubject) + takeUntilDestroyed(this.destroyRef) ) .subscribe(); } else { @@ -227,7 +226,7 @@ export class GfHoldingDetailDialogComponent implements OnDestroy, OnInit { dataSource: this.data.dataSource, symbol: this.data.symbol }) - .pipe(takeUntil(this.unsubscribeSubject)) + .pipe(takeUntilDestroyed(this.destroyRef)) .subscribe(); } }); @@ -236,7 +235,7 @@ export class GfHoldingDetailDialogComponent implements OnDestroy, OnInit { .fetchAccounts({ filters }) - .pipe(takeUntil(this.unsubscribeSubject)) + .pipe(takeUntilDestroyed(this.destroyRef)) .subscribe(({ accounts }) => { this.accounts = accounts; @@ -249,7 +248,7 @@ export class GfHoldingDetailDialogComponent implements OnDestroy, OnInit { sortColumn: this.sortColumn, sortDirection: this.sortDirection }) - .pipe(takeUntil(this.unsubscribeSubject)) + .pipe(takeUntilDestroyed(this.destroyRef)) .subscribe(({ activities }) => { this.dataSource = new MatTableDataSource(activities); @@ -261,7 +260,7 @@ export class GfHoldingDetailDialogComponent implements OnDestroy, OnInit { dataSource: this.data.dataSource, symbol: this.data.symbol }) - .pipe(takeUntil(this.unsubscribeSubject)) + .pipe(takeUntilDestroyed(this.destroyRef)) .subscribe( ({ activitiesCount, @@ -524,7 +523,7 @@ export class GfHoldingDetailDialogComponent implements OnDestroy, OnInit { ); this.userService.stateChanged - .pipe(takeUntil(this.unsubscribeSubject)) + .pipe(takeUntilDestroyed(this.destroyRef)) .subscribe((state) => { if (state?.user) { this.user = state.user; @@ -582,7 +581,7 @@ export class GfHoldingDetailDialogComponent implements OnDestroy, OnInit { this.dataService .postActivity(activity) - .pipe(takeUntil(this.unsubscribeSubject)) + .pipe(takeUntilDestroyed(this.destroyRef)) .subscribe(() => { this.router.navigate( internalRoutes.portfolio.subRoutes.activities.routerLink @@ -599,7 +598,7 @@ export class GfHoldingDetailDialogComponent implements OnDestroy, OnInit { this.dataService .fetchExport({ activityIds }) - .pipe(takeUntil(this.unsubscribeSubject)) + .pipe(takeUntilDestroyed(this.destroyRef)) .subscribe((data) => { downloadAsFile({ content: data, @@ -629,18 +628,13 @@ export class GfHoldingDetailDialogComponent implements OnDestroy, OnInit { this.dialogRef.close(); } - public ngOnDestroy() { - this.unsubscribeSubject.next(); - this.unsubscribeSubject.complete(); - } - private fetchMarketData() { this.dataService .fetchMarketDataBySymbol({ dataSource: this.data.dataSource, symbol: this.data.symbol }) - .pipe(takeUntil(this.unsubscribeSubject)) + .pipe(takeUntilDestroyed(this.destroyRef)) .subscribe(({ marketData }) => { this.marketDataItems = marketData; diff --git a/apps/client/src/app/components/home-holdings/home-holdings.html b/apps/client/src/app/components/home-holdings/home-holdings.html index ec131cd39..175c88606 100644 --- a/apps/client/src/app/components/home-holdings/home-holdings.html +++ b/apps/client/src/app/components/home-holdings/home-holdings.html @@ -46,7 +46,6 @@ }
(); - public constructor( private changeDetectorRef: ChangeDetectorRef, private dataService: DataService, + private destroyRef: DestroyRef, private userService: UserService ) { const { statistics } = this.dataService.fetchInfo(); @@ -39,7 +38,7 @@ export class GfOpenPageComponent implements OnDestroy, OnInit { public ngOnInit() { this.userService.stateChanged - .pipe(takeUntil(this.unsubscribeSubject)) + .pipe(takeUntilDestroyed(this.destroyRef)) .subscribe((state) => { if (state?.user) { this.user = state.user; @@ -48,9 +47,4 @@ export class GfOpenPageComponent implements OnDestroy, OnInit { } }); } - - public ngOnDestroy() { - this.unsubscribeSubject.next(); - this.unsubscribeSubject.complete(); - } } diff --git a/apps/client/src/app/pages/portfolio/activities/create-or-update-activity-dialog/create-or-update-activity-dialog.component.ts b/apps/client/src/app/pages/portfolio/activities/create-or-update-activity-dialog/create-or-update-activity-dialog.component.ts index c7cd63191..9cc312b25 100644 --- a/apps/client/src/app/pages/portfolio/activities/create-or-update-activity-dialog/create-or-update-activity-dialog.component.ts +++ b/apps/client/src/app/pages/portfolio/activities/create-or-update-activity-dialog/create-or-update-activity-dialog.component.ts @@ -20,9 +20,10 @@ import { ChangeDetectionStrategy, ChangeDetectorRef, Component, - Inject, - OnDestroy + DestroyRef, + Inject } from '@angular/core'; +import { takeUntilDestroyed } from '@angular/core/rxjs-interop'; import { FormBuilder, FormGroup, @@ -46,8 +47,8 @@ import { AssetClass, Tag, Type } from '@prisma/client'; import { isAfter, isToday } from 'date-fns'; import { addIcons } from 'ionicons'; import { calendarClearOutline, refreshOutline } from 'ionicons/icons'; -import { EMPTY, Subject } from 'rxjs'; -import { catchError, delay, takeUntil } from 'rxjs/operators'; +import { EMPTY } from 'rxjs'; +import { catchError, delay } from 'rxjs/operators'; import { CreateOrUpdateActivityDialogParams } from './interfaces/interfaces'; import { ActivityType } from './types/activity-type.type'; @@ -75,7 +76,7 @@ import { ActivityType } from './types/activity-type.type'; styleUrls: ['./create-or-update-activity-dialog.scss'], templateUrl: 'create-or-update-activity-dialog.html' }) -export class GfCreateOrUpdateActivityDialogComponent implements OnDestroy { +export class GfCreateOrUpdateActivityDialogComponent { public activityForm: FormGroup; public assetClassOptions: AssetClassSelectorOption[] = Object.keys(AssetClass) @@ -101,13 +102,12 @@ export class GfCreateOrUpdateActivityDialogComponent implements OnDestroy { public typesTranslationMap = new Map(); public Validators = Validators; - private unsubscribeSubject = new Subject(); - public constructor( private changeDetectorRef: ChangeDetectorRef, @Inject(MAT_DIALOG_DATA) public data: CreateOrUpdateActivityDialogParams, private dataService: DataService, private dateAdapter: DateAdapter, + private destroyRef: DestroyRef, public dialogRef: MatDialogRef, private formBuilder: FormBuilder, @Inject(MAT_DATE_LOCALE) private locale: string, @@ -133,7 +133,7 @@ export class GfCreateOrUpdateActivityDialogComponent implements OnDestroy { this.dataService .fetchPortfolioHoldings() - .pipe(takeUntil(this.unsubscribeSubject)) + .pipe(takeUntilDestroyed(this.destroyRef)) .subscribe(({ holdings }) => { this.defaultLookupItems = holdings .filter(({ assetSubClass }) => { @@ -237,7 +237,7 @@ export class GfCreateOrUpdateActivityDialogComponent implements OnDestroy { // Slightly delay until the more specific form control value changes have // completed delay(300), - takeUntil(this.unsubscribeSubject) + takeUntilDestroyed(this.destroyRef) ) .subscribe(async () => { if ( @@ -284,7 +284,7 @@ export class GfCreateOrUpdateActivityDialogComponent implements OnDestroy { this.activityForm .get('assetClass') - .valueChanges.pipe(takeUntil(this.unsubscribeSubject)) + .valueChanges.pipe(takeUntilDestroyed(this.destroyRef)) .subscribe((assetClass) => { const assetSubClasses = ASSET_CLASS_MAPPING.get(assetClass) ?? []; @@ -335,7 +335,7 @@ export class GfCreateOrUpdateActivityDialogComponent implements OnDestroy { if (newTag && this.hasPermissionToCreateOwnTag) { this.dataService .postTag({ ...newTag, userId: this.data.user.id }) - .pipe(takeUntil(this.unsubscribeSubject)) + .pipe(takeUntilDestroyed(this.destroyRef)) .subscribe((tag) => { this.activityForm.get('tags').setValue( tags.map((currentTag) => { @@ -349,7 +349,7 @@ export class GfCreateOrUpdateActivityDialogComponent implements OnDestroy { this.userService .get(true) - .pipe(takeUntil(this.unsubscribeSubject)) + .pipe(takeUntilDestroyed(this.destroyRef)) .subscribe(); }); } @@ -357,7 +357,7 @@ export class GfCreateOrUpdateActivityDialogComponent implements OnDestroy { this.activityForm .get('type') - .valueChanges.pipe(takeUntil(this.unsubscribeSubject)) + .valueChanges.pipe(takeUntilDestroyed(this.destroyRef)) .subscribe((type: ActivityType) => { if ( type === 'VALUABLE' || @@ -465,7 +465,7 @@ export class GfCreateOrUpdateActivityDialogComponent implements OnDestroy { dataSource: this.data.activity?.SymbolProfile?.dataSource, symbol: this.data.activity?.SymbolProfile?.symbol }) - .pipe(takeUntil(this.unsubscribeSubject)) + .pipe(takeUntilDestroyed(this.destroyRef)) .subscribe(({ marketPrice }) => { this.currentMarketPrice = marketPrice; @@ -557,11 +557,6 @@ export class GfCreateOrUpdateActivityDialogComponent implements OnDestroy { } } - public ngOnDestroy() { - this.unsubscribeSubject.next(); - this.unsubscribeSubject.complete(); - } - private updateAssetProfile() { this.isLoading = true; this.changeDetectorRef.markForCheck(); @@ -581,7 +576,7 @@ export class GfCreateOrUpdateActivityDialogComponent implements OnDestroy { return EMPTY; }), - takeUntil(this.unsubscribeSubject) + takeUntilDestroyed(this.destroyRef) ) .subscribe(({ currency, dataSource, marketPrice }) => { if (this.mode === 'create') { diff --git a/apps/client/src/app/pages/portfolio/activities/import-activities-dialog/import-activities-dialog.component.ts b/apps/client/src/app/pages/portfolio/activities/import-activities-dialog/import-activities-dialog.component.ts index 1a84e9f31..00f0508fe 100644 --- a/apps/client/src/app/pages/portfolio/activities/import-activities-dialog/import-activities-dialog.component.ts +++ b/apps/client/src/app/pages/portfolio/activities/import-activities-dialog/import-activities-dialog.component.ts @@ -21,9 +21,10 @@ import { ChangeDetectionStrategy, ChangeDetectorRef, Component, - Inject, - OnDestroy + DestroyRef, + Inject } from '@angular/core'; +import { takeUntilDestroyed } from '@angular/core/rxjs-interop'; import { FormBuilder, FormGroup, @@ -52,7 +53,6 @@ import { cloudUploadOutline, warningOutline } from 'ionicons/icons'; import { isArray, sortBy } from 'lodash'; import ms from 'ms'; import { DeviceDetectorService } from 'ngx-device-detector'; -import { Subject, takeUntil } from 'rxjs'; import { ImportStep } from './enums/import-step'; import { ImportActivitiesDialogParams } from './interfaces/interfaces'; @@ -81,7 +81,7 @@ import { ImportActivitiesDialogParams } from './interfaces/interfaces'; styleUrls: ['./import-activities-dialog.scss'], templateUrl: 'import-activities-dialog.html' }) -export class GfImportActivitiesDialogComponent implements OnDestroy { +export class GfImportActivitiesDialogComponent { public accounts: CreateAccountWithBalancesDto[] = []; public activities: Activity[] = []; public assetProfileForm: FormGroup; @@ -104,12 +104,11 @@ export class GfImportActivitiesDialogComponent implements OnDestroy { public tags: CreateTagDto[] = []; public totalItems: number; - private unsubscribeSubject = new Subject(); - public constructor( private changeDetectorRef: ChangeDetectorRef, @Inject(MAT_DIALOG_DATA) public data: ImportActivitiesDialogParams, private dataService: DataService, + private destroyRef: DestroyRef, private deviceService: DeviceDetectorService, private formBuilder: FormBuilder, public dialogRef: MatDialogRef, @@ -152,7 +151,7 @@ export class GfImportActivitiesDialogComponent implements OnDestroy { ], range: 'max' }) - .pipe(takeUntil(this.unsubscribeSubject)) + .pipe(takeUntilDestroyed(this.destroyRef)) .subscribe(({ holdings }) => { this.holdings = sortBy(holdings, ({ name }) => { return name.toLowerCase(); @@ -237,7 +236,7 @@ export class GfImportActivitiesDialogComponent implements OnDestroy { dataSource, symbol }) - .pipe(takeUntil(this.unsubscribeSubject)) + .pipe(takeUntilDestroyed(this.destroyRef)) .subscribe(({ activities }) => { this.activities = activities; this.dataSource = new MatTableDataSource(activities.reverse()); @@ -284,11 +283,6 @@ export class GfImportActivitiesDialogComponent implements OnDestroy { }); } - public ngOnDestroy() { - this.unsubscribeSubject.next(); - this.unsubscribeSubject.complete(); - } - private async handleFile({ file, stepper diff --git a/apps/client/src/app/pages/portfolio/allocations/allocations-page.component.ts b/apps/client/src/app/pages/portfolio/allocations/allocations-page.component.ts index 5226c3c12..367716d2d 100644 --- a/apps/client/src/app/pages/portfolio/allocations/allocations-page.component.ts +++ b/apps/client/src/app/pages/portfolio/allocations/allocations-page.component.ts @@ -405,17 +405,22 @@ export class GfAllocationsPageComponent implements OnInit { } if (position.holdings.length > 0) { - for (const holding of position.holdings) { - const { allocationInPercentage, name, valueInBaseCurrency } = - holding; - - if (this.topHoldingsMap[name]?.value) { - this.topHoldingsMap[name].value += isNumber(valueInBaseCurrency) + for (const { + allocationInPercentage, + name, + valueInBaseCurrency + } of position.holdings) { + const normalizedAssetName = this.normalizeAssetName(name); + + if (this.topHoldingsMap[normalizedAssetName]?.value) { + this.topHoldingsMap[normalizedAssetName].value += isNumber( + valueInBaseCurrency + ) ? valueInBaseCurrency : allocationInPercentage * this.portfolioDetails.holdings[symbol].valueInPercentage; } else { - this.topHoldingsMap[name] = { + this.topHoldingsMap[normalizedAssetName] = { name, value: isNumber(valueInBaseCurrency) ? valueInBaseCurrency @@ -518,7 +523,10 @@ export class GfAllocationsPageComponent implements OnInit { if (holding.holdings.length > 0) { const currentParentHolding = holding.holdings.find( (parentHolding) => { - return parentHolding.name === name; + return ( + this.normalizeAssetName(parentHolding.name) === + this.normalizeAssetName(name) + ); } ); @@ -555,6 +563,14 @@ export class GfAllocationsPageComponent implements OnInit { } } + private normalizeAssetName(name: string) { + if (!name) { + return ''; + } + + return name.trim().toLowerCase(); + } + private openAccountDetailDialog(aAccountId: string) { const dialogRef = this.dialog.open< GfAccountDetailDialogComponent, diff --git a/apps/client/src/app/pages/register/user-account-registration-dialog/user-account-registration-dialog.component.ts b/apps/client/src/app/pages/register/user-account-registration-dialog/user-account-registration-dialog.component.ts index 4cc0f52f8..cbbe2d29c 100644 --- a/apps/client/src/app/pages/register/user-account-registration-dialog/user-account-registration-dialog.component.ts +++ b/apps/client/src/app/pages/register/user-account-registration-dialog/user-account-registration-dialog.component.ts @@ -8,10 +8,11 @@ import { ChangeDetectorRef, Component, CUSTOM_ELEMENTS_SCHEMA, + DestroyRef, Inject, - OnDestroy, ViewChild } from '@angular/core'; +import { takeUntilDestroyed } from '@angular/core/rxjs-interop'; import { FormsModule, ReactiveFormsModule } from '@angular/forms'; import { MatButtonModule } from '@angular/material/button'; import { MatCheckboxModule } from '@angular/material/checkbox'; @@ -27,8 +28,6 @@ import { checkmarkOutline, copyOutline } from 'ionicons/icons'; -import { Subject } from 'rxjs'; -import { takeUntil } from 'rxjs/operators'; import { UserAccountRegistrationDialogParams } from './interfaces/interfaces'; @@ -53,7 +52,7 @@ import { UserAccountRegistrationDialogParams } from './interfaces/interfaces'; styleUrls: ['./user-account-registration-dialog.scss'], templateUrl: 'user-account-registration-dialog.html' }) -export class GfUserAccountRegistrationDialogComponent implements OnDestroy { +export class GfUserAccountRegistrationDialogComponent { @ViewChild(MatStepper) stepper!: MatStepper; public accessToken: string; @@ -64,12 +63,11 @@ export class GfUserAccountRegistrationDialogComponent implements OnDestroy { public routerLinkAboutTermsOfService = publicRoutes.about.subRoutes.termsOfService.routerLink; - private unsubscribeSubject = new Subject(); - public constructor( private changeDetectorRef: ChangeDetectorRef, @Inject(MAT_DIALOG_DATA) public data: UserAccountRegistrationDialogParams, - private dataService: DataService + private dataService: DataService, + private destroyRef: DestroyRef ) { addIcons({ arrowForwardOutline, checkmarkOutline, copyOutline }); } @@ -77,7 +75,7 @@ export class GfUserAccountRegistrationDialogComponent implements OnDestroy { public createAccount() { this.dataService .postUser() - .pipe(takeUntil(this.unsubscribeSubject)) + .pipe(takeUntilDestroyed(this.destroyRef)) .subscribe(({ accessToken, authToken, role }) => { this.accessToken = accessToken; this.authToken = authToken; @@ -96,9 +94,4 @@ export class GfUserAccountRegistrationDialogComponent implements OnDestroy { public onChangeDislaimerChecked() { this.isDisclaimerChecked = !this.isDisclaimerChecked; } - - public ngOnDestroy() { - this.unsubscribeSubject.next(); - this.unsubscribeSubject.complete(); - } } diff --git a/libs/common/src/lib/permissions.ts b/libs/common/src/lib/permissions.ts index a70e61bac..f9cb19562 100644 --- a/libs/common/src/lib/permissions.ts +++ b/libs/common/src/lib/permissions.ts @@ -195,7 +195,7 @@ export function hasReadRestrictedAccessPermission({ return false; } - const access = user.accessesGet?.find(({ id }) => { + const access = user?.accessesGet?.find(({ id }) => { return id === impersonationId; }); diff --git a/libs/ui/src/lib/top-holdings/top-holdings.component.html b/libs/ui/src/lib/top-holdings/top-holdings.component.html index 7a2a84126..6ff4ecf5b 100644 --- a/libs/ui/src/lib/top-holdings/top-holdings.component.html +++ b/libs/ui/src/lib/top-holdings/top-holdings.component.html @@ -16,7 +16,7 @@ Name -
{{ element?.name | titlecase }}
+
{{ prettifyAssetName(element?.name) }}
diff --git a/libs/ui/src/lib/top-holdings/top-holdings.component.ts b/libs/ui/src/lib/top-holdings/top-holdings.component.ts index 75a96fc5c..7c9ae033f 100644 --- a/libs/ui/src/lib/top-holdings/top-holdings.component.ts +++ b/libs/ui/src/lib/top-holdings/top-holdings.component.ts @@ -20,15 +20,14 @@ import { EventEmitter, Input, OnChanges, - OnDestroy, Output, ViewChild } from '@angular/core'; import { MatButtonModule } from '@angular/material/button'; import { MatPaginator, MatPaginatorModule } from '@angular/material/paginator'; import { MatTableDataSource, MatTableModule } from '@angular/material/table'; +import { capitalize } from 'lodash'; import { NgxSkeletonLoaderModule } from 'ngx-skeleton-loader'; -import { Subject } from 'rxjs'; import { GfValueComponent } from '../value/value.component'; @@ -58,7 +57,7 @@ import { GfValueComponent } from '../value/value.component'; styleUrls: ['./top-holdings.component.scss'], templateUrl: './top-holdings.component.html' }) -export class GfTopHoldingsComponent implements OnChanges, OnDestroy { +export class GfTopHoldingsComponent implements OnChanges { @Input() baseCurrency: string; @Input() locale = getLocale(); @Input() pageSize = Number.MAX_SAFE_INTEGER; @@ -76,8 +75,6 @@ export class GfTopHoldingsComponent implements OnChanges, OnDestroy { ]; public isLoading = true; - private unsubscribeSubject = new Subject(); - public ngOnChanges() { this.isLoading = true; @@ -101,8 +98,23 @@ export class GfTopHoldingsComponent implements OnChanges, OnDestroy { }); } - public ngOnDestroy() { - this.unsubscribeSubject.next(); - this.unsubscribeSubject.complete(); + public prettifyAssetName(name: string) { + if (!name) { + return ''; + } + + return name + .split(' ') + .filter((token) => { + return !token.startsWith('(') && !token.endsWith(')'); + }) + .map((token) => { + if (token.length <= 2) { + return token.toUpperCase(); + } + + return capitalize(token); + }) + .join(' '); } } diff --git a/package-lock.json b/package-lock.json index 80260794c..494f1e34a 100644 --- a/package-lock.json +++ b/package-lock.json @@ -125,7 +125,7 @@ "@schematics/angular": "21.1.1", "@storybook/addon-docs": "10.1.10", "@storybook/angular": "10.1.10", - "@trivago/prettier-plugin-sort-imports": "5.2.2", + "@trivago/prettier-plugin-sort-imports": "6.0.2", "@types/big.js": "6.2.2", "@types/cookie-parser": "1.4.10", "@types/fast-redact": "3.0.4", @@ -12509,25 +12509,28 @@ "license": "MIT" }, "node_modules/@trivago/prettier-plugin-sort-imports": { - "version": "5.2.2", - "resolved": "https://registry.npmjs.org/@trivago/prettier-plugin-sort-imports/-/prettier-plugin-sort-imports-5.2.2.tgz", - "integrity": "sha512-fYDQA9e6yTNmA13TLVSA+WMQRc5Bn/c0EUBditUHNfMMxN7M82c38b1kEggVE3pLpZ0FwkwJkUEKMiOi52JXFA==", + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/@trivago/prettier-plugin-sort-imports/-/prettier-plugin-sort-imports-6.0.2.tgz", + "integrity": "sha512-3DgfkukFyC/sE/VuYjaUUWoFfuVjPK55vOFDsxD56XXynFMCZDYFogH2l/hDfOsQAm1myoU/1xByJ3tWqtulXA==", "dev": true, "license": "Apache-2.0", "dependencies": { - "@babel/generator": "^7.26.5", - "@babel/parser": "^7.26.7", - "@babel/traverse": "^7.26.7", - "@babel/types": "^7.26.7", + "@babel/generator": "^7.28.0", + "@babel/parser": "^7.28.0", + "@babel/traverse": "^7.28.0", + "@babel/types": "^7.28.0", "javascript-natural-sort": "^0.7.1", - "lodash": "^4.17.21" + "lodash-es": "^4.17.21", + "minimatch": "^9.0.0", + "parse-imports-exports": "^0.2.4" }, "engines": { - "node": ">18.12" + "node": ">= 20" }, "peerDependencies": { "@vue/compiler-sfc": "3.x", "prettier": "2.x - 3.x", + "prettier-plugin-ember-template-tag": ">= 2.0.0", "prettier-plugin-svelte": "3.x", "svelte": "4.x || 5.x" }, @@ -12535,6 +12538,9 @@ "@vue/compiler-sfc": { "optional": true }, + "prettier-plugin-ember-template-tag": { + "optional": true + }, "prettier-plugin-svelte": { "optional": true }, @@ -12543,6 +12549,32 @@ } } }, + "node_modules/@trivago/prettier-plugin-sort-imports/node_modules/brace-expansion": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz", + "integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0" + } + }, + "node_modules/@trivago/prettier-plugin-sort-imports/node_modules/minimatch": { + "version": "9.0.9", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.9.tgz", + "integrity": "sha512-OBwBN9AL4dqmETlpS2zasx+vTeWclWzkblfZk7KTA5j3jeOONz/tRCnZomUyvNg83wL5Zv9Ss6HMJXAgL8R2Yg==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^2.0.2" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, "node_modules/@trysound/sax": { "version": "0.2.0", "resolved": "https://registry.npmjs.org/@trysound/sax/-/sax-0.2.0.tgz", @@ -25294,8 +25326,8 @@ "version": "4.17.22", "resolved": "https://registry.npmjs.org/lodash-es/-/lodash-es-4.17.22.tgz", "integrity": "sha512-XEawp1t0gxSi9x01glktRZ5HDy0HXqrM0x5pXQM98EaI0NxO6jVM7omDOxsuEo5UIASAnm2bRp1Jt/e0a2XU8Q==", - "license": "MIT", - "optional": true + "devOptional": true, + "license": "MIT" }, "node_modules/lodash.clonedeepwith": { "version": "4.5.0", @@ -27766,6 +27798,16 @@ "node": ">=6" } }, + "node_modules/parse-imports-exports": { + "version": "0.2.4", + "resolved": "https://registry.npmjs.org/parse-imports-exports/-/parse-imports-exports-0.2.4.tgz", + "integrity": "sha512-4s6vd6dx1AotCx/RCI2m7t7GCh5bDRUtGNvRfHSP2wbBQdMi67pPe7mtzmgwcaQ8VKK/6IB7Glfyu3qdZJPybQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "parse-statements": "1.0.11" + } + }, "node_modules/parse-json": { "version": "5.2.0", "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-5.2.0.tgz", @@ -27819,6 +27861,13 @@ "node": ">=0.10.0" } }, + "node_modules/parse-statements": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/parse-statements/-/parse-statements-1.0.11.tgz", + "integrity": "sha512-HlsyYdMBnbPQ9Jr/VgJ1YF4scnldvJpJxCVx6KgqPL4dxppsWrJHCIIxQXMJrqGnsRkNPATbeMJ8Yxu7JMsYcA==", + "dev": true, + "license": "MIT" + }, "node_modules/parse5": { "version": "8.0.0", "resolved": "https://registry.npmjs.org/parse5/-/parse5-8.0.0.tgz", diff --git a/package.json b/package.json index 411b36620..3f22136e0 100644 --- a/package.json +++ b/package.json @@ -170,7 +170,7 @@ "@schematics/angular": "21.1.1", "@storybook/addon-docs": "10.1.10", "@storybook/angular": "10.1.10", - "@trivago/prettier-plugin-sort-imports": "5.2.2", + "@trivago/prettier-plugin-sort-imports": "6.0.2", "@types/big.js": "6.2.2", "@types/cookie-parser": "1.4.10", "@types/fast-redact": "3.0.4",