Browse Source

Task/improve holdings table type safety (#6306)

* fix(lib): update displayedColumns type

* feat(lib): use input function for holdings

* feat(lib): make isLoading a computed signal

* feat(lib): make paginator and sort viewchild signals

* feat(lib): make dataSource a computed signal

* feat(lib): use input function for hasPermission fields

* feat(lib): make displayedColumns a computed signal

* feat(lib): remove ngOnChanges

* feat(lib): update types in holdings mock

* fix(lib): update imports for treemap chart component

* fix(lib): remove unused routeQueryParams variable

* fix(lib): prevent creating new table data source every time the signal changes

* fix(lib): remove unused unsubscribe subject as there is no observable subscription

* fix(lib): revert changes to dataSource in the template

* fix(lib): changed locale to input signal

* fix(lib): change ignoreAssetSubClasses to protected

* fix(lib): create canShowDetails function

* fix(lib): remove unused baseCurrency and deviceType inputs

* fix(lib): remove unused baseCurrency and deviceType inputs from stories

* feat(lib): make constructor as public
pull/5570/head^2
Kenrick Tandrian 2 weeks ago
committed by GitHub
parent
commit
0c970e2a14
No known key found for this signature in database GPG Key ID: B5690EEEBB952194
  1. 2
      apps/client/src/app/components/account-detail-dialog/account-detail-dialog.html
  2. 2
      apps/client/src/app/components/home-holdings/home-holdings.html
  3. 1
      apps/client/src/app/pages/public/public-page.html
  4. 38
      libs/ui/src/lib/holdings-table/holdings-table.component.html
  5. 4
      libs/ui/src/lib/holdings-table/holdings-table.component.stories.ts
  6. 99
      libs/ui/src/lib/holdings-table/holdings-table.component.ts
  7. 44
      libs/ui/src/lib/mocks/holdings.ts
  8. 2
      libs/ui/src/lib/treemap-chart/treemap-chart.component.ts

2
apps/client/src/app/components/account-detail-dialog/account-detail-dialog.html

@ -102,8 +102,6 @@
<div class="d-none d-sm-block ml-2" i18n>Holdings</div> <div class="d-none d-sm-block ml-2" i18n>Holdings</div>
</ng-template> </ng-template>
<gf-holdings-table <gf-holdings-table
[baseCurrency]="user?.settings?.baseCurrency"
[deviceType]="data.deviceType"
[hasPermissionToOpenDetails]="false" [hasPermissionToOpenDetails]="false"
[holdings]="holdings" [holdings]="holdings"
[locale]="user?.settings?.locale" [locale]="user?.settings?.locale"

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

@ -46,8 +46,6 @@
} }
<div [ngClass]="{ 'd-none': viewModeFormControl.value !== 'TABLE' }"> <div [ngClass]="{ 'd-none': viewModeFormControl.value !== 'TABLE' }">
<gf-holdings-table <gf-holdings-table
[baseCurrency]="user?.settings?.baseCurrency"
[deviceType]="deviceType"
[hasPermissionToShowQuantities]="false" [hasPermissionToShowQuantities]="false"
[holdings]="holdings" [holdings]="holdings"
[locale]="user?.settings?.locale" [locale]="user?.settings?.locale"

1
apps/client/src/app/pages/public/public-page.html

@ -78,7 +78,6 @@
[showLabels]="deviceType !== 'mobile'" [showLabels]="deviceType !== 'mobile'"
/> />
<gf-holdings-table <gf-holdings-table
[deviceType]="deviceType"
[hasPermissionToOpenDetails]="false" [hasPermissionToOpenDetails]="false"
[hasPermissionToShowQuantities]="false" [hasPermissionToShowQuantities]="false"
[hasPermissionToShowValues]="false" [hasPermissionToShowValues]="false"

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

@ -52,7 +52,7 @@
<div class="d-flex justify-content-end"> <div class="d-flex justify-content-end">
<gf-value <gf-value
[isDate]="element.dateOfFirstActivity ? true : false" [isDate]="element.dateOfFirstActivity ? true : false"
[locale]="locale" [locale]="locale()"
[value]="element.dateOfFirstActivity ?? ''" [value]="element.dateOfFirstActivity ?? ''"
/> />
</div> </div>
@ -76,8 +76,8 @@
<div class="d-flex justify-content-end"> <div class="d-flex justify-content-end">
<gf-value <gf-value
[isCurrency]="true" [isCurrency]="true"
[locale]="locale" [locale]="locale()"
[value]="isLoading ? undefined : element.quantity" [value]="isLoading() ? undefined : element.quantity"
/> />
</div> </div>
</td> </td>
@ -100,8 +100,8 @@
<div class="d-flex justify-content-end"> <div class="d-flex justify-content-end">
<gf-value <gf-value
[isCurrency]="true" [isCurrency]="true"
[locale]="locale" [locale]="locale()"
[value]="isLoading ? undefined : element.valueInBaseCurrency" [value]="isLoading() ? undefined : element.valueInBaseCurrency"
/> />
</div> </div>
</td> </td>
@ -121,8 +121,8 @@
<div class="d-flex justify-content-end"> <div class="d-flex justify-content-end">
<gf-value <gf-value
[isPercent]="true" [isPercent]="true"
[locale]="locale" [locale]="locale()"
[value]="isLoading ? undefined : element.allocationInPercentage" [value]="isLoading() ? undefined : element.allocationInPercentage"
/> />
</div> </div>
</td> </td>
@ -142,9 +142,9 @@
<gf-value <gf-value
[colorizeSign]="true" [colorizeSign]="true"
[isCurrency]="true" [isCurrency]="true"
[locale]="locale" [locale]="locale()"
[value]=" [value]="
isLoading ? undefined : element.netPerformanceWithCurrencyEffect isLoading() ? undefined : element.netPerformanceWithCurrencyEffect
" "
/> />
</div> </div>
@ -166,9 +166,9 @@
<gf-value <gf-value
[colorizeSign]="true" [colorizeSign]="true"
[isPercent]="true" [isPercent]="true"
[locale]="locale" [locale]="locale()"
[value]=" [value]="
isLoading isLoading()
? undefined ? undefined
: element.netPerformancePercentWithCurrencyEffect : element.netPerformancePercentWithCurrencyEffect
" "
@ -177,17 +177,13 @@
</td> </td>
</ng-container> </ng-container>
<tr *matHeaderRowDef="displayedColumns" mat-header-row></tr> <tr *matHeaderRowDef="displayedColumns()" mat-header-row></tr>
<tr <tr
*matRowDef="let row; columns: displayedColumns" *matRowDef="let row; columns: displayedColumns()"
mat-row mat-row
[ngClass]="{ [class.cursor-pointer]="canShowDetails(row)"
'cursor-pointer':
hasPermissionToOpenDetails &&
!ignoreAssetSubClasses.includes(row.assetSubClass)
}"
(click)=" (click)="
!ignoreAssetSubClasses.includes(row.assetSubClass) && canShowDetails(row) &&
onOpenHoldingDialog({ onOpenHoldingDialog({
dataSource: row.dataSource, dataSource: row.dataSource,
symbol: row.symbol symbol: row.symbol
@ -199,7 +195,7 @@
<mat-paginator class="d-none" [pageSize]="pageSize" /> <mat-paginator class="d-none" [pageSize]="pageSize" />
@if (isLoading) { @if (isLoading()) {
<ngx-skeleton-loader <ngx-skeleton-loader
animation="pulse" animation="pulse"
class="px-4 py-3" class="px-4 py-3"
@ -210,7 +206,7 @@
/> />
} }
@if (dataSource.data.length > pageSize && !isLoading) { @if (dataSource.data.length > pageSize && !isLoading()) {
<div class="my-3 text-center"> <div class="my-3 text-center">
<button mat-stroked-button (click)="onShowAllHoldings()"> <button mat-stroked-button (click)="onShowAllHoldings()">
<ng-container i18n>Show all</ng-container> <ng-container i18n>Show all</ng-container>

4
libs/ui/src/lib/holdings-table/holdings-table.component.stories.ts

@ -38,8 +38,6 @@ type Story = StoryObj<GfHoldingsTableComponent>;
export const Loading: Story = { export const Loading: Story = {
args: { args: {
holdings: undefined, holdings: undefined,
baseCurrency: 'USD',
deviceType: 'desktop',
hasPermissionToOpenDetails: false, hasPermissionToOpenDetails: false,
hasPermissionToShowQuantities: true, hasPermissionToShowQuantities: true,
hasPermissionToShowValues: true, hasPermissionToShowValues: true,
@ -51,8 +49,6 @@ export const Loading: Story = {
export const Default: Story = { export const Default: Story = {
args: { args: {
holdings, holdings,
baseCurrency: 'USD',
deviceType: 'desktop',
hasPermissionToOpenDetails: false, hasPermissionToOpenDetails: false,
hasPermissionToShowQuantities: true, hasPermissionToShowQuantities: true,
hasPermissionToShowValues: true, hasPermissionToShowValues: true,

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

@ -11,10 +11,11 @@ import {
Component, Component,
EventEmitter, EventEmitter,
Input, Input,
OnChanges,
OnDestroy,
Output, Output,
ViewChild computed,
effect,
input,
viewChild
} from '@angular/core'; } from '@angular/core';
import { MatButtonModule } from '@angular/material/button'; import { MatButtonModule } from '@angular/material/button';
import { MatDialogModule } from '@angular/material/dialog'; import { MatDialogModule } from '@angular/material/dialog';
@ -23,7 +24,6 @@ import { MatSort, MatSortModule } from '@angular/material/sort';
import { MatTableDataSource, MatTableModule } from '@angular/material/table'; import { MatTableDataSource, MatTableModule } from '@angular/material/table';
import { AssetSubClass } from '@prisma/client'; import { AssetSubClass } from '@prisma/client';
import { NgxSkeletonLoaderModule } from 'ngx-skeleton-loader'; import { NgxSkeletonLoaderModule } from 'ngx-skeleton-loader';
import { Subject, Subscription } from 'rxjs';
import { GfEntityLogoComponent } from '../entity-logo/entity-logo.component'; import { GfEntityLogoComponent } from '../entity-logo/entity-logo.component';
import { GfValueComponent } from '../value/value.component'; import { GfValueComponent } from '../value/value.component';
@ -46,77 +46,82 @@ import { GfValueComponent } from '../value/value.component';
styleUrls: ['./holdings-table.component.scss'], styleUrls: ['./holdings-table.component.scss'],
templateUrl: './holdings-table.component.html' templateUrl: './holdings-table.component.html'
}) })
export class GfHoldingsTableComponent implements OnChanges, OnDestroy { export class GfHoldingsTableComponent {
@Input() baseCurrency: string;
@Input() deviceType: string;
@Input() hasPermissionToOpenDetails = true;
@Input() hasPermissionToShowQuantities = true;
@Input() hasPermissionToShowValues = true;
@Input() holdings: PortfolioPosition[];
@Input() locale = getLocale();
@Input() pageSize = Number.MAX_SAFE_INTEGER; @Input() pageSize = Number.MAX_SAFE_INTEGER;
@Output() holdingClicked = new EventEmitter<AssetProfileIdentifier>(); @Output() holdingClicked = new EventEmitter<AssetProfileIdentifier>();
@ViewChild(MatPaginator) paginator: MatPaginator; public readonly hasPermissionToOpenDetails = input(true);
@ViewChild(MatSort) sort: MatSort; 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 dataSource = new MatTableDataSource<PortfolioPosition>(); protected readonly dataSource = new MatTableDataSource<PortfolioPosition>([]);
public displayedColumns = [];
public ignoreAssetSubClasses = [AssetSubClass.CASH];
public isLoading = true;
public routeQueryParams: Subscription;
private unsubscribeSubject = new Subject<void>(); protected readonly displayedColumns = computed(() => {
const columns = ['icon', 'nameWithSymbol', 'dateOfFirstActivity'];
public ngOnChanges() { if (this.hasPermissionToShowQuantities()) {
this.displayedColumns = ['icon', 'nameWithSymbol', 'dateOfFirstActivity']; columns.push('quantity');
if (this.hasPermissionToShowQuantities) {
this.displayedColumns.push('quantity');
} }
if (this.hasPermissionToShowValues) { if (this.hasPermissionToShowValues()) {
this.displayedColumns.push('valueInBaseCurrency'); columns.push('valueInBaseCurrency');
} }
this.displayedColumns.push('allocationInPercentage'); columns.push('allocationInPercentage');
if (this.hasPermissionToShowValues) { if (this.hasPermissionToShowValues()) {
this.displayedColumns.push('performance'); columns.push('performance');
} }
this.displayedColumns.push('performanceInPercentage'); columns.push('performanceInPercentage');
return columns;
});
protected readonly ignoreAssetSubClasses: AssetSubClass[] = [
AssetSubClass.CASH
];
this.isLoading = true; protected readonly isLoading = computed(() => !this.holdings());
this.dataSource = new MatTableDataSource(this.holdings); public constructor() {
this.dataSource.paginator = this.paginator;
this.dataSource.sortingDataAccessor = getLowercase; this.dataSource.sortingDataAccessor = getLowercase;
this.dataSource.sort = this.sort; // Reactive data update
effect(() => {
this.dataSource.data = this.holdings();
});
if (this.holdings) { // Reactive view connection
this.isLoading = false; effect(() => {
this.dataSource.paginator = this.paginator();
this.dataSource.sort = this.sort();
});
} }
protected canShowDetails(holding: PortfolioPosition): boolean {
return (
this.hasPermissionToOpenDetails() &&
!this.ignoreAssetSubClasses.includes(holding.assetSubClass)
);
} }
public onOpenHoldingDialog({ dataSource, symbol }: AssetProfileIdentifier) { protected onOpenHoldingDialog({
if (this.hasPermissionToOpenDetails) { dataSource,
symbol
}: AssetProfileIdentifier) {
this.holdingClicked.emit({ dataSource, symbol }); this.holdingClicked.emit({ dataSource, symbol });
} }
}
public onShowAllHoldings() { protected onShowAllHoldings() {
this.pageSize = Number.MAX_SAFE_INTEGER; this.pageSize = Number.MAX_SAFE_INTEGER;
setTimeout(() => { setTimeout(() => {
this.dataSource.paginator = this.paginator; this.dataSource.paginator = this.paginator();
}); });
} }
public ngOnDestroy() {
this.unsubscribeSubject.next();
this.unsubscribeSubject.complete();
}
} }

44
libs/ui/src/lib/mocks/holdings.ts

@ -4,9 +4,9 @@ export const holdings: PortfolioPosition[] = [
{ {
activitiesCount: 1, activitiesCount: 1,
allocationInPercentage: 0.042990776363386086, allocationInPercentage: 0.042990776363386086,
assetClass: 'EQUITY' as any, assetClass: 'EQUITY',
assetClassLabel: 'Equity', assetClassLabel: 'Equity',
assetSubClass: 'STOCK' as any, assetSubClass: 'STOCK',
assetSubClassLabel: 'Stock', assetSubClassLabel: 'Stock',
countries: [ countries: [
{ {
@ -17,7 +17,7 @@ export const holdings: PortfolioPosition[] = [
} }
], ],
currency: 'USD', currency: 'USD',
dataSource: 'YAHOO' as any, dataSource: 'YAHOO',
dateOfFirstActivity: new Date('2021-12-01T00:00:00.000Z'), dateOfFirstActivity: new Date('2021-12-01T00:00:00.000Z'),
dividend: 0, dividend: 0,
grossPerformance: 3856, grossPerformance: 3856,
@ -47,9 +47,9 @@ export const holdings: PortfolioPosition[] = [
{ {
activitiesCount: 2, activitiesCount: 2,
allocationInPercentage: 0.02377401948293552, allocationInPercentage: 0.02377401948293552,
assetClass: 'EQUITY' as any, assetClass: 'EQUITY',
assetClassLabel: 'Equity', assetClassLabel: 'Equity',
assetSubClass: 'STOCK' as any, assetSubClass: 'STOCK',
assetSubClassLabel: 'Stock', assetSubClassLabel: 'Stock',
countries: [ countries: [
{ {
@ -60,7 +60,7 @@ export const holdings: PortfolioPosition[] = [
} }
], ],
currency: 'EUR', currency: 'EUR',
dataSource: 'YAHOO' as any, dataSource: 'YAHOO',
dateOfFirstActivity: new Date('2021-04-23T00:00:00.000Z'), dateOfFirstActivity: new Date('2021-04-23T00:00:00.000Z'),
dividend: 192, dividend: 192,
grossPerformance: 2226.700251889169, grossPerformance: 2226.700251889169,
@ -90,9 +90,9 @@ export const holdings: PortfolioPosition[] = [
{ {
activitiesCount: 1, activitiesCount: 1,
allocationInPercentage: 0.08038536990007467, allocationInPercentage: 0.08038536990007467,
assetClass: 'EQUITY' as any, assetClass: 'EQUITY',
assetClassLabel: 'Equity', assetClassLabel: 'Equity',
assetSubClass: 'STOCK' as any, assetSubClass: 'STOCK',
assetSubClassLabel: 'Stock', assetSubClassLabel: 'Stock',
countries: [ countries: [
{ {
@ -103,7 +103,7 @@ export const holdings: PortfolioPosition[] = [
} }
], ],
currency: 'USD', currency: 'USD',
dataSource: 'YAHOO' as any, dataSource: 'YAHOO',
dateOfFirstActivity: new Date('2018-10-01T00:00:00.000Z'), dateOfFirstActivity: new Date('2018-10-01T00:00:00.000Z'),
dividend: 0, dividend: 0,
grossPerformance: 12758.05, grossPerformance: 12758.05,
@ -133,13 +133,13 @@ export const holdings: PortfolioPosition[] = [
{ {
activitiesCount: 1, activitiesCount: 1,
allocationInPercentage: 0.19216416482928922, allocationInPercentage: 0.19216416482928922,
assetClass: 'LIQUIDITY' as any, assetClass: 'LIQUIDITY',
assetClassLabel: 'Liquidity', assetClassLabel: 'Liquidity',
assetSubClass: 'CRYPTOCURRENCY' as any, assetSubClass: 'CRYPTOCURRENCY',
assetSubClassLabel: 'Cryptocurrency', assetSubClassLabel: 'Cryptocurrency',
countries: [], countries: [],
currency: 'USD', currency: 'USD',
dataSource: 'COINGECKO' as any, dataSource: 'COINGECKO',
dateOfFirstActivity: new Date('2017-08-16T00:00:00.000Z'), dateOfFirstActivity: new Date('2017-08-16T00:00:00.000Z'),
dividend: 0, dividend: 0,
grossPerformance: 52666.7898248, grossPerformance: 52666.7898248,
@ -158,15 +158,15 @@ export const holdings: PortfolioPosition[] = [
sectors: [], sectors: [],
symbol: 'bitcoin', symbol: 'bitcoin',
tags: [], tags: [],
url: null, url: undefined,
valueInBaseCurrency: 54666.7898248 valueInBaseCurrency: 54666.7898248
}, },
{ {
activitiesCount: 1, activitiesCount: 1,
allocationInPercentage: 0.04307127421937313, allocationInPercentage: 0.04307127421937313,
assetClass: 'EQUITY' as any, assetClass: 'EQUITY',
assetClassLabel: 'Equity', assetClassLabel: 'Equity',
assetSubClass: 'STOCK' as any, assetSubClass: 'STOCK',
assetSubClassLabel: 'Stock', assetSubClassLabel: 'Stock',
countries: [ countries: [
{ {
@ -177,7 +177,7 @@ export const holdings: PortfolioPosition[] = [
} }
], ],
currency: 'USD', currency: 'USD',
dataSource: 'YAHOO' as any, dataSource: 'YAHOO',
dateOfFirstActivity: new Date('2023-01-03T00:00:00.000Z'), dateOfFirstActivity: new Date('2023-01-03T00:00:00.000Z'),
dividend: 0, dividend: 0,
grossPerformance: 5065.5, grossPerformance: 5065.5,
@ -207,9 +207,9 @@ export const holdings: PortfolioPosition[] = [
{ {
activitiesCount: 1, activitiesCount: 1,
allocationInPercentage: 0.18762679306394897, allocationInPercentage: 0.18762679306394897,
assetClass: 'EQUITY' as any, assetClass: 'EQUITY',
assetClassLabel: 'Equity', assetClassLabel: 'Equity',
assetSubClass: 'STOCK' as any, assetSubClass: 'STOCK',
assetSubClassLabel: 'Stock', assetSubClassLabel: 'Stock',
countries: [ countries: [
{ {
@ -220,7 +220,7 @@ export const holdings: PortfolioPosition[] = [
} }
], ],
currency: 'USD', currency: 'USD',
dataSource: 'YAHOO' as any, dataSource: 'YAHOO',
dateOfFirstActivity: new Date('2017-01-03T00:00:00.000Z'), dateOfFirstActivity: new Date('2017-01-03T00:00:00.000Z'),
dividend: 0, dividend: 0,
grossPerformance: 51227.500000005, grossPerformance: 51227.500000005,
@ -250,9 +250,9 @@ export const holdings: PortfolioPosition[] = [
{ {
activitiesCount: 5, activitiesCount: 5,
allocationInPercentage: 0.053051250766657634, allocationInPercentage: 0.053051250766657634,
assetClass: 'EQUITY' as any, assetClass: 'EQUITY',
assetClassLabel: 'Equity', assetClassLabel: 'Equity',
assetSubClass: 'ETF' as any, assetSubClass: 'ETF',
assetSubClassLabel: 'ETF', assetSubClassLabel: 'ETF',
countries: [ countries: [
{ {
@ -263,7 +263,7 @@ export const holdings: PortfolioPosition[] = [
} }
], ],
currency: 'USD', currency: 'USD',
dataSource: 'YAHOO' as any, dataSource: 'YAHOO',
dateOfFirstActivity: new Date('2019-03-01T00:00:00.000Z'), dateOfFirstActivity: new Date('2019-03-01T00:00:00.000Z'),
dividend: 0, dividend: 0,
grossPerformance: 6845.8, grossPerformance: 6845.8,

2
libs/ui/src/lib/treemap-chart/treemap-chart.component.ts

@ -25,7 +25,7 @@ import {
} from '@angular/core'; } from '@angular/core';
import { DataSource } from '@prisma/client'; import { DataSource } from '@prisma/client';
import { Big } from 'big.js'; import { Big } from 'big.js';
import type { TooltipOptions, ChartData } from 'chart.js'; import type { ChartData, TooltipOptions } from 'chart.js';
import { LinearScale } from 'chart.js'; import { LinearScale } from 'chart.js';
import { Chart, Tooltip } from 'chart.js'; import { Chart, Tooltip } from 'chart.js';
import { TreemapController, TreemapElement } from 'chartjs-chart-treemap'; import { TreemapController, TreemapElement } from 'chartjs-chart-treemap';

Loading…
Cancel
Save