Browse Source

Merge branch 'main' into task/improve-user-validation-in-delete-auth-device-endpoint

pull/6614/head
Thomas Kaul 1 week ago
committed by GitHub
parent
commit
1316199925
No known key found for this signature in database GPG Key ID: B5690EEEBB952194
  1. 10
      CHANGELOG.md
  2. 85
      apps/client/src/app/app.component.ts
  3. 36
      apps/client/src/app/components/holding-detail-dialog/holding-detail-dialog.component.ts
  4. 1
      apps/client/src/app/components/home-holdings/home-holdings.html
  5. 8
      apps/client/src/app/interfaces/interfaces.ts
  6. 16
      apps/client/src/app/pages/open/open-page.component.ts
  7. 35
      apps/client/src/app/pages/portfolio/activities/create-or-update-activity-dialog/create-or-update-activity-dialog.component.ts
  8. 20
      apps/client/src/app/pages/portfolio/activities/import-activities-dialog/import-activities-dialog.component.ts
  9. 30
      apps/client/src/app/pages/portfolio/allocations/allocations-page.component.ts
  10. 19
      apps/client/src/app/pages/register/user-account-registration-dialog/user-account-registration-dialog.component.ts
  11. 2
      libs/common/src/lib/permissions.ts
  12. 2
      libs/ui/src/lib/top-holdings/top-holdings.component.html
  13. 28
      libs/ui/src/lib/top-holdings/top-holdings.component.ts
  14. 73
      package-lock.json
  15. 2
      package.json

10
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

85
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']
) {
.subscribe(
({ dataSource, holdingDetailDialog, symbol }: GfAppQueryParams) => {
if (dataSource && holdingDetailDialog && symbol) {
this.openHoldingDetailDialog({
dataSource: params['dataSource'],
symbol: params['symbol']
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);
}
}

36
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<void>();
public constructor(
private changeDetectorRef: ChangeDetectorRef,
private dataService: DataService,
private destroyRef: DestroyRef,
public dialogRef: MatDialogRef<GfHoldingDetailDialogComponent>,
@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;

1
apps/client/src/app/components/home-holdings/home-holdings.html

@ -46,7 +46,6 @@
}
<div [ngClass]="{ 'd-none': viewModeFormControl.value !== 'TABLE' }">
<gf-holdings-table
[hasPermissionToShowQuantities]="false"
[holdings]="holdings"
[locale]="user?.settings?.locale"
(holdingClicked)="onHoldingClicked($event)"

8
apps/client/src/app/interfaces/interfaces.ts

@ -0,0 +1,8 @@
import type { Params } from '@angular/router';
import type { DataSource } from '@prisma/client';
export interface GfAppQueryParams extends Params {
dataSource?: DataSource;
holdingDetailDialog?: string;
symbol?: string;
}

16
apps/client/src/app/pages/open/open-page.component.ts

@ -7,11 +7,11 @@ 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 { Subject, takeUntil } from 'rxjs';
@Component({
host: { class: 'page' },
@ -21,15 +21,14 @@ import { Subject, takeUntil } from 'rxjs';
styleUrls: ['./open-page.scss'],
templateUrl: './open-page.html'
})
export class GfOpenPageComponent implements OnDestroy, OnInit {
export class GfOpenPageComponent implements OnInit {
public statistics: Statistics;
public user: User;
private unsubscribeSubject = new Subject<void>();
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();
}
}

35
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<Type, string>();
public Validators = Validators;
private unsubscribeSubject = new Subject<void>();
public constructor(
private changeDetectorRef: ChangeDetectorRef,
@Inject(MAT_DIALOG_DATA) public data: CreateOrUpdateActivityDialogParams,
private dataService: DataService,
private dateAdapter: DateAdapter<any>,
private destroyRef: DestroyRef,
public dialogRef: MatDialogRef<GfCreateOrUpdateActivityDialogComponent>,
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') {

20
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<void>();
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<GfImportActivitiesDialogComponent>,
@ -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

30
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;
for (const {
allocationInPercentage,
name,
valueInBaseCurrency
} of position.holdings) {
const normalizedAssetName = this.normalizeAssetName(name);
if (this.topHoldingsMap[name]?.value) {
this.topHoldingsMap[name].value += isNumber(valueInBaseCurrency)
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,

19
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<void>();
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();
}
}

2
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;
});

2
libs/ui/src/lib/top-holdings/top-holdings.component.html

@ -16,7 +16,7 @@
<ng-container i18n>Name</ng-container>
</th>
<td *matCellDef="let element" class="px-2" mat-cell>
<div class="text-truncate">{{ element?.name | titlecase }}</div>
<div class="text-truncate">{{ prettifyAssetName(element?.name) }}</div>
</td>
</ng-container>

28
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<void>();
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(' ');
}
}

73
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",

2
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",

Loading…
Cancel
Save