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. 103
      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>
</ng-template>
<gf-holdings-table
[baseCurrency]="user?.settings?.baseCurrency"
[deviceType]="data.deviceType"
[hasPermissionToOpenDetails]="false"
[holdings]="holdings"
[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' }">
<gf-holdings-table
[baseCurrency]="user?.settings?.baseCurrency"
[deviceType]="deviceType"
[hasPermissionToShowQuantities]="false"
[holdings]="holdings"
[locale]="user?.settings?.locale"

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

@ -78,7 +78,6 @@
[showLabels]="deviceType !== 'mobile'"
/>
<gf-holdings-table
[deviceType]="deviceType"
[hasPermissionToOpenDetails]="false"
[hasPermissionToShowQuantities]="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">
<gf-value
[isDate]="element.dateOfFirstActivity ? true : false"
[locale]="locale"
[locale]="locale()"
[value]="element.dateOfFirstActivity ?? ''"
/>
</div>
@ -76,8 +76,8 @@
<div class="d-flex justify-content-end">
<gf-value
[isCurrency]="true"
[locale]="locale"
[value]="isLoading ? undefined : element.quantity"
[locale]="locale()"
[value]="isLoading() ? undefined : element.quantity"
/>
</div>
</td>
@ -100,8 +100,8 @@
<div class="d-flex justify-content-end">
<gf-value
[isCurrency]="true"
[locale]="locale"
[value]="isLoading ? undefined : element.valueInBaseCurrency"
[locale]="locale()"
[value]="isLoading() ? undefined : element.valueInBaseCurrency"
/>
</div>
</td>
@ -121,8 +121,8 @@
<div class="d-flex justify-content-end">
<gf-value
[isPercent]="true"
[locale]="locale"
[value]="isLoading ? undefined : element.allocationInPercentage"
[locale]="locale()"
[value]="isLoading() ? undefined : element.allocationInPercentage"
/>
</div>
</td>
@ -142,9 +142,9 @@
<gf-value
[colorizeSign]="true"
[isCurrency]="true"
[locale]="locale"
[locale]="locale()"
[value]="
isLoading ? undefined : element.netPerformanceWithCurrencyEffect
isLoading() ? undefined : element.netPerformanceWithCurrencyEffect
"
/>
</div>
@ -166,9 +166,9 @@
<gf-value
[colorizeSign]="true"
[isPercent]="true"
[locale]="locale"
[locale]="locale()"
[value]="
isLoading
isLoading()
? undefined
: element.netPerformancePercentWithCurrencyEffect
"
@ -177,17 +177,13 @@
</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()"
mat-row
[ngClass]="{
'cursor-pointer':
hasPermissionToOpenDetails &&
!ignoreAssetSubClasses.includes(row.assetSubClass)
}"
[class.cursor-pointer]="canShowDetails(row)"
(click)="
!ignoreAssetSubClasses.includes(row.assetSubClass) &&
canShowDetails(row) &&
onOpenHoldingDialog({
dataSource: row.dataSource,
symbol: row.symbol
@ -199,7 +195,7 @@
<mat-paginator class="d-none" [pageSize]="pageSize" />
@if (isLoading) {
@if (isLoading()) {
<ngx-skeleton-loader
animation="pulse"
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">
<button mat-stroked-button (click)="onShowAllHoldings()">
<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 = {
args: {
holdings: undefined,
baseCurrency: 'USD',
deviceType: 'desktop',
hasPermissionToOpenDetails: false,
hasPermissionToShowQuantities: true,
hasPermissionToShowValues: true,
@ -51,8 +49,6 @@ export const Loading: Story = {
export const Default: Story = {
args: {
holdings,
baseCurrency: 'USD',
deviceType: 'desktop',
hasPermissionToOpenDetails: false,
hasPermissionToShowQuantities: true,
hasPermissionToShowValues: true,

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

@ -11,10 +11,11 @@ import {
Component,
EventEmitter,
Input,
OnChanges,
OnDestroy,
Output,
ViewChild
computed,
effect,
input,
viewChild
} from '@angular/core';
import { MatButtonModule } from '@angular/material/button';
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 { AssetSubClass } from '@prisma/client';
import { NgxSkeletonLoaderModule } from 'ngx-skeleton-loader';
import { Subject, Subscription } from 'rxjs';
import { GfEntityLogoComponent } from '../entity-logo/entity-logo.component';
import { GfValueComponent } from '../value/value.component';
@ -46,77 +46,82 @@ import { GfValueComponent } from '../value/value.component';
styleUrls: ['./holdings-table.component.scss'],
templateUrl: './holdings-table.component.html'
})
export class GfHoldingsTableComponent implements OnChanges, OnDestroy {
@Input() baseCurrency: string;
@Input() deviceType: string;
@Input() hasPermissionToOpenDetails = true;
@Input() hasPermissionToShowQuantities = true;
@Input() hasPermissionToShowValues = true;
@Input() holdings: PortfolioPosition[];
@Input() locale = getLocale();
export class GfHoldingsTableComponent {
@Input() pageSize = Number.MAX_SAFE_INTEGER;
@Output() holdingClicked = new EventEmitter<AssetProfileIdentifier>();
@ViewChild(MatPaginator) paginator: MatPaginator;
@ViewChild(MatSort) sort: MatSort;
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 dataSource = new MatTableDataSource<PortfolioPosition>();
public displayedColumns = [];
public ignoreAssetSubClasses = [AssetSubClass.CASH];
public isLoading = true;
public routeQueryParams: Subscription;
protected readonly dataSource = new MatTableDataSource<PortfolioPosition>([]);
private unsubscribeSubject = new Subject<void>();
protected readonly displayedColumns = computed(() => {
const columns = ['icon', 'nameWithSymbol', 'dateOfFirstActivity'];
public ngOnChanges() {
this.displayedColumns = ['icon', 'nameWithSymbol', 'dateOfFirstActivity'];
if (this.hasPermissionToShowQuantities) {
this.displayedColumns.push('quantity');
if (this.hasPermissionToShowQuantities()) {
columns.push('quantity');
}
if (this.hasPermissionToShowValues) {
this.displayedColumns.push('valueInBaseCurrency');
if (this.hasPermissionToShowValues()) {
columns.push('valueInBaseCurrency');
}
this.displayedColumns.push('allocationInPercentage');
columns.push('allocationInPercentage');
if (this.hasPermissionToShowValues) {
this.displayedColumns.push('performance');
if (this.hasPermissionToShowValues()) {
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);
this.dataSource.paginator = this.paginator;
public constructor() {
this.dataSource.sortingDataAccessor = getLowercase;
this.dataSource.sort = this.sort;
// Reactive data update
effect(() => {
this.dataSource.data = this.holdings();
});
if (this.holdings) {
this.isLoading = false;
}
// Reactive view connection
effect(() => {
this.dataSource.paginator = this.paginator();
this.dataSource.sort = this.sort();
});
}
public onOpenHoldingDialog({ dataSource, symbol }: AssetProfileIdentifier) {
if (this.hasPermissionToOpenDetails) {
this.holdingClicked.emit({ dataSource, symbol });
}
protected canShowDetails(holding: PortfolioPosition): boolean {
return (
this.hasPermissionToOpenDetails() &&
!this.ignoreAssetSubClasses.includes(holding.assetSubClass)
);
}
protected onOpenHoldingDialog({
dataSource,
symbol
}: AssetProfileIdentifier) {
this.holdingClicked.emit({ dataSource, symbol });
}
public onShowAllHoldings() {
protected onShowAllHoldings() {
this.pageSize = Number.MAX_SAFE_INTEGER;
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,
allocationInPercentage: 0.042990776363386086,
assetClass: 'EQUITY' as any,
assetClass: 'EQUITY',
assetClassLabel: 'Equity',
assetSubClass: 'STOCK' as any,
assetSubClass: 'STOCK',
assetSubClassLabel: 'Stock',
countries: [
{
@ -17,7 +17,7 @@ export const holdings: PortfolioPosition[] = [
}
],
currency: 'USD',
dataSource: 'YAHOO' as any,
dataSource: 'YAHOO',
dateOfFirstActivity: new Date('2021-12-01T00:00:00.000Z'),
dividend: 0,
grossPerformance: 3856,
@ -47,9 +47,9 @@ export const holdings: PortfolioPosition[] = [
{
activitiesCount: 2,
allocationInPercentage: 0.02377401948293552,
assetClass: 'EQUITY' as any,
assetClass: 'EQUITY',
assetClassLabel: 'Equity',
assetSubClass: 'STOCK' as any,
assetSubClass: 'STOCK',
assetSubClassLabel: 'Stock',
countries: [
{
@ -60,7 +60,7 @@ export const holdings: PortfolioPosition[] = [
}
],
currency: 'EUR',
dataSource: 'YAHOO' as any,
dataSource: 'YAHOO',
dateOfFirstActivity: new Date('2021-04-23T00:00:00.000Z'),
dividend: 192,
grossPerformance: 2226.700251889169,
@ -90,9 +90,9 @@ export const holdings: PortfolioPosition[] = [
{
activitiesCount: 1,
allocationInPercentage: 0.08038536990007467,
assetClass: 'EQUITY' as any,
assetClass: 'EQUITY',
assetClassLabel: 'Equity',
assetSubClass: 'STOCK' as any,
assetSubClass: 'STOCK',
assetSubClassLabel: 'Stock',
countries: [
{
@ -103,7 +103,7 @@ export const holdings: PortfolioPosition[] = [
}
],
currency: 'USD',
dataSource: 'YAHOO' as any,
dataSource: 'YAHOO',
dateOfFirstActivity: new Date('2018-10-01T00:00:00.000Z'),
dividend: 0,
grossPerformance: 12758.05,
@ -133,13 +133,13 @@ export const holdings: PortfolioPosition[] = [
{
activitiesCount: 1,
allocationInPercentage: 0.19216416482928922,
assetClass: 'LIQUIDITY' as any,
assetClass: 'LIQUIDITY',
assetClassLabel: 'Liquidity',
assetSubClass: 'CRYPTOCURRENCY' as any,
assetSubClass: 'CRYPTOCURRENCY',
assetSubClassLabel: 'Cryptocurrency',
countries: [],
currency: 'USD',
dataSource: 'COINGECKO' as any,
dataSource: 'COINGECKO',
dateOfFirstActivity: new Date('2017-08-16T00:00:00.000Z'),
dividend: 0,
grossPerformance: 52666.7898248,
@ -158,15 +158,15 @@ export const holdings: PortfolioPosition[] = [
sectors: [],
symbol: 'bitcoin',
tags: [],
url: null,
url: undefined,
valueInBaseCurrency: 54666.7898248
},
{
activitiesCount: 1,
allocationInPercentage: 0.04307127421937313,
assetClass: 'EQUITY' as any,
assetClass: 'EQUITY',
assetClassLabel: 'Equity',
assetSubClass: 'STOCK' as any,
assetSubClass: 'STOCK',
assetSubClassLabel: 'Stock',
countries: [
{
@ -177,7 +177,7 @@ export const holdings: PortfolioPosition[] = [
}
],
currency: 'USD',
dataSource: 'YAHOO' as any,
dataSource: 'YAHOO',
dateOfFirstActivity: new Date('2023-01-03T00:00:00.000Z'),
dividend: 0,
grossPerformance: 5065.5,
@ -207,9 +207,9 @@ export const holdings: PortfolioPosition[] = [
{
activitiesCount: 1,
allocationInPercentage: 0.18762679306394897,
assetClass: 'EQUITY' as any,
assetClass: 'EQUITY',
assetClassLabel: 'Equity',
assetSubClass: 'STOCK' as any,
assetSubClass: 'STOCK',
assetSubClassLabel: 'Stock',
countries: [
{
@ -220,7 +220,7 @@ export const holdings: PortfolioPosition[] = [
}
],
currency: 'USD',
dataSource: 'YAHOO' as any,
dataSource: 'YAHOO',
dateOfFirstActivity: new Date('2017-01-03T00:00:00.000Z'),
dividend: 0,
grossPerformance: 51227.500000005,
@ -250,9 +250,9 @@ export const holdings: PortfolioPosition[] = [
{
activitiesCount: 5,
allocationInPercentage: 0.053051250766657634,
assetClass: 'EQUITY' as any,
assetClass: 'EQUITY',
assetClassLabel: 'Equity',
assetSubClass: 'ETF' as any,
assetSubClass: 'ETF',
assetSubClassLabel: 'ETF',
countries: [
{
@ -263,7 +263,7 @@ export const holdings: PortfolioPosition[] = [
}
],
currency: 'USD',
dataSource: 'YAHOO' as any,
dataSource: 'YAHOO',
dateOfFirstActivity: new Date('2019-03-01T00:00:00.000Z'),
dividend: 0,
grossPerformance: 6845.8,

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

@ -25,7 +25,7 @@ import {
} from '@angular/core';
import { DataSource } from '@prisma/client';
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 { Chart, Tooltip } from 'chart.js';
import { TreemapController, TreemapElement } from 'chartjs-chart-treemap';

Loading…
Cancel
Save