From 0c970e2a14f8a0a2d544adb8ae2d39f7003d228a Mon Sep 17 00:00:00 2001
From: Kenrick Tandrian <60643640+KenTandrian@users.noreply.github.com>
Date: Fri, 13 Feb 2026 23:30:53 +0700
Subject: [PATCH] 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
---
.../account-detail-dialog.html | 2 -
.../home-holdings/home-holdings.html | 2 -
.../src/app/pages/public/public-page.html | 1 -
.../holdings-table.component.html | 38 +++----
.../holdings-table.component.stories.ts | 4 -
.../holdings-table.component.ts | 103 +++++++++---------
libs/ui/src/lib/mocks/holdings.ts | 44 ++++----
.../treemap-chart/treemap-chart.component.ts | 2 +-
8 files changed, 94 insertions(+), 102 deletions(-)
diff --git a/apps/client/src/app/components/account-detail-dialog/account-detail-dialog.html b/apps/client/src/app/components/account-detail-dialog/account-detail-dialog.html
index 15dd8f13a..e41d3415c 100644
--- a/apps/client/src/app/components/account-detail-dialog/account-detail-dialog.html
+++ b/apps/client/src/app/components/account-detail-dialog/account-detail-dialog.html
@@ -102,8 +102,6 @@
Holdings
@@ -76,8 +76,8 @@
@@ -100,8 +100,8 @@
@@ -121,8 +121,8 @@
@@ -142,9 +142,9 @@
@@ -166,9 +166,9 @@
-
+
-@if (isLoading) {
+@if (isLoading()) {
}
-@if (dataSource.data.length > pageSize && !isLoading) {
+@if (dataSource.data.length > pageSize && !isLoading()) {
Show all
diff --git a/libs/ui/src/lib/holdings-table/holdings-table.component.stories.ts b/libs/ui/src/lib/holdings-table/holdings-table.component.stories.ts
index 37e73e9e4..8748a830f 100644
--- a/libs/ui/src/lib/holdings-table/holdings-table.component.stories.ts
+++ b/libs/ui/src/lib/holdings-table/holdings-table.component.stories.ts
@@ -38,8 +38,6 @@ type Story = StoryObj;
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,
diff --git a/libs/ui/src/lib/holdings-table/holdings-table.component.ts b/libs/ui/src/lib/holdings-table/holdings-table.component.ts
index f408f7d9b..bea555a0b 100644
--- a/libs/ui/src/lib/holdings-table/holdings-table.component.ts
+++ b/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();
- @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();
+ public readonly locale = input(getLocale());
+ public readonly paginator = viewChild.required(MatPaginator);
+ public readonly sort = viewChild.required(MatSort);
- public dataSource = new MatTableDataSource();
- public displayedColumns = [];
- public ignoreAssetSubClasses = [AssetSubClass.CASH];
- public isLoading = true;
- public routeQueryParams: Subscription;
+ protected readonly dataSource = new MatTableDataSource([]);
- private unsubscribeSubject = new Subject();
+ 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();
- }
}
diff --git a/libs/ui/src/lib/mocks/holdings.ts b/libs/ui/src/lib/mocks/holdings.ts
index 3f57af884..5ab3e89eb 100644
--- a/libs/ui/src/lib/mocks/holdings.ts
+++ b/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,
diff --git a/libs/ui/src/lib/treemap-chart/treemap-chart.component.ts b/libs/ui/src/lib/treemap-chart/treemap-chart.component.ts
index ce85c300e..a80876f6a 100644
--- a/libs/ui/src/lib/treemap-chart/treemap-chart.component.ts
+++ b/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';