From 347d7efee5edc97cbe75e098cda06053dd260b92 Mon Sep 17 00:00:00 2001 From: Thomas Kaul <4159106+dtslvr@users.noreply.github.com> Date: Wed, 22 Jul 2026 17:29:47 +0200 Subject: [PATCH] Bugfix/skip opening holding detail dialog for cash positions (#7390) * Skip opening holding detail dialog for cash positions * Update changelog --- CHANGELOG.md | 1 + .../allocations/allocations-page.component.ts | 7 ++- .../analysis/analysis-page.component.ts | 8 ++- libs/common/src/lib/helper.ts | 10 +++- .../holdings-table.component.ts | 16 +++--- .../portfolio-proportion-chart.component.ts | 15 +++++- .../top-holdings/top-holdings.component.html | 9 +--- .../top-holdings/top-holdings.component.ts | 20 ++++++-- .../treemap-chart/treemap-chart.component.ts | 51 ++++++++++++------- 9 files changed, 91 insertions(+), 46 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 1a21ab9a4..d073e016b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,6 +9,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Fixed +- Skipped opening the holding detail dialog for cash positions on the allocations page, the analysis page and the portfolio holdings page - Resolved an exception in the `GET api/v1/portfolio/holding/:dataSource/:symbol` endpoint for cash positions - Improved the error handling in the access endpoints (`POST` and `PUT`) to return `400 Bad Request` when granting access to a non-existent user 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 de00048e7..dd2c62f98 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 @@ -6,7 +6,10 @@ import { import { ImpersonationStorageService } from '@ghostfolio/client/services/impersonation-storage.service'; import { UserService } from '@ghostfolio/client/services/user/user.service'; import { MAX_TOP_HOLDINGS, UNKNOWN_KEY } from '@ghostfolio/common/config'; -import { getCountryName } from '@ghostfolio/common/helper'; +import { + canOpenHoldingDetail, + getCountryName +} from '@ghostfolio/common/helper'; import { AssetProfileIdentifier, HoldingWithParents, @@ -116,6 +119,7 @@ export class GfAllocationsPageComponent implements OnInit { protected symbols: { [name: string]: { dataSource?: DataSource; + isClickable?: boolean; name: string; symbol: string; value: number; @@ -498,6 +502,7 @@ export class GfAllocationsPageComponent implements OnInit { this.symbols[symbol] = { symbol, dataSource: position.assetProfile.dataSource, + isClickable: canOpenHoldingDetail(position), name: position.assetProfile.name ?? '', value: (isNumber(position.valueInBaseCurrency) diff --git a/apps/client/src/app/pages/portfolio/analysis/analysis-page.component.ts b/apps/client/src/app/pages/portfolio/analysis/analysis-page.component.ts index ae41f54c8..5fa952a4f 100644 --- a/apps/client/src/app/pages/portfolio/analysis/analysis-page.component.ts +++ b/apps/client/src/app/pages/portfolio/analysis/analysis-page.component.ts @@ -6,6 +6,7 @@ import { DEFAULT_DATE_RANGE, NUMERICAL_PRECISION_THRESHOLD_6_FIGURES } from '@ghostfolio/common/config'; +import { canOpenHoldingDetail } from '@ghostfolio/common/helper'; import { HistoricalDataItem, InvestmentItem, @@ -365,8 +366,11 @@ export class GfAnalysisPageComponent implements OnInit { .pipe(takeUntilDestroyed(this.destroyRef)) .subscribe(({ holdings }) => { const holdingsSorted = sortBy( - holdings.filter(({ netPerformancePercentWithCurrencyEffect }) => { - return isNumber(netPerformancePercentWithCurrencyEffect); + holdings.filter((holding) => { + return ( + canOpenHoldingDetail(holding) && + isNumber(holding.netPerformancePercentWithCurrencyEffect) + ); }), 'netPerformancePercentWithCurrencyEffect' ).reverse(); diff --git a/libs/common/src/lib/helper.ts b/libs/common/src/lib/helper.ts index ad8674d81..7787b2a97 100644 --- a/libs/common/src/lib/helper.ts +++ b/libs/common/src/lib/helper.ts @@ -2,6 +2,7 @@ import { NumberParser } from '@internationalized/number'; import { Type as ActivityType, AssetProfileOverrides, + AssetSubClass, MarketData, Prisma, SymbolProfile @@ -45,7 +46,8 @@ import { import { AssetProfileIdentifier, AssetProfileItem, - Benchmark + Benchmark, + PortfolioPosition } from './interfaces'; import { BenchmarkTrend, ColorScheme } from './types'; @@ -172,6 +174,12 @@ export function canDeleteUser({ return currentUserId !== userId; } +export function canOpenHoldingDetail({ + assetProfile +}: Pick): boolean { + return assetProfile?.assetSubClass !== AssetSubClass.CASH; +} + export function capitalize(aString: string) { return aString.charAt(0).toUpperCase() + aString.slice(1).toLowerCase(); } 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 5b5a2bcfe..7fe21720e 100644 --- a/libs/ui/src/lib/holdings-table/holdings-table.component.ts +++ b/libs/ui/src/lib/holdings-table/holdings-table.component.ts @@ -1,4 +1,8 @@ -import { getLocale, getLowercase } from '@ghostfolio/common/helper'; +import { + canOpenHoldingDetail, + getLocale, + getLowercase +} from '@ghostfolio/common/helper'; import { AssetProfileIdentifier, PortfolioPosition @@ -20,7 +24,6 @@ import { MatDialogModule } from '@angular/material/dialog'; import { MatPaginator, MatPaginatorModule } from '@angular/material/paginator'; 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 { GfEntityLogoComponent } from '../entity-logo/entity-logo.component'; @@ -79,10 +82,6 @@ export class GfHoldingsTableComponent { return columns; }); - protected readonly ignoreAssetSubClasses: AssetSubClass[] = [ - AssetSubClass.CASH - ]; - protected readonly isLoading = computed(() => !this.holdings()); public constructor() { @@ -101,10 +100,7 @@ export class GfHoldingsTableComponent { } protected canShowDetails(holding: PortfolioPosition): boolean { - return ( - this.hasPermissionToOpenDetails() && - !this.ignoreAssetSubClasses.includes(holding.assetProfile.assetSubClass) - ); + return this.hasPermissionToOpenDetails() && canOpenHoldingDetail(holding); } protected onOpenHoldingDialog({ diff --git a/libs/ui/src/lib/portfolio-proportion-chart/portfolio-proportion-chart.component.ts b/libs/ui/src/lib/portfolio-proportion-chart/portfolio-proportion-chart.component.ts index e24446c34..b50f89a8e 100644 --- a/libs/ui/src/lib/portfolio-proportion-chart/portfolio-proportion-chart.component.ts +++ b/libs/ui/src/lib/portfolio-proportion-chart/portfolio-proportion-chart.component.ts @@ -66,6 +66,7 @@ export class GfPortfolioProportionChartComponent @Input() data: { [symbol: string]: Pick & { dataSource?: DataSource; + isClickable?: boolean; name: string; value: number; }; @@ -356,6 +357,10 @@ export class GfPortfolioProportionChartComponent const dataIndex = activeElements[0].index; const symbol = chart.data.labels?.[dataIndex] as string; + if (this.data[symbol]?.isClickable === false) { + return; + } + const dataSource = this.data[symbol]?.dataSource; this.proportionChartClicked.emit( @@ -363,10 +368,16 @@ export class GfPortfolioProportionChartComponent ); } catch {} }, - onHover: (event, chartElement) => { + onHover: (event, chartElement, chart) => { if (this.cursor) { + const symbol = chartElement[0] + ? (chart.data.labels?.[chartElement[0].index] as string) + : undefined; + (event.native?.target as HTMLElement).style.cursor = - chartElement[0] ? this.cursor : 'default'; + symbol && this.data[symbol]?.isClickable !== false + ? this.cursor + : 'default'; } }, plugins: { 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 3e09ab5ba..11900106a 100644 --- a/libs/ui/src/lib/top-holdings/top-holdings.component.html +++ b/libs/ui/src/lib/top-holdings/top-holdings.component.html @@ -120,13 +120,8 @@ , + activeElement: ActiveElement + ): PortfolioPosition | undefined { + if (!activeElement) { + return undefined; + } + + const dataset = orderBy( + chart.data.datasets[activeElement.datasetIndex].tree, + ['allocationInPercentage'], + ['desc'] + ) as PortfolioPosition[]; + + return dataset[activeElement.index]; + } + private initialize() { const holdings = this.holdings(); @@ -323,27 +339,24 @@ export class GfTreemapChartComponent animation: false, onClick: (_, activeElements, chart: Chart<'treemap'>) => { try { - const dataIndex = activeElements[0].index; - const datasetIndex = activeElements[0].datasetIndex; + const holding = this.getHolding(chart, activeElements[0]); - const dataset = orderBy( - chart.data.datasets[datasetIndex].tree, - ['allocationInPercentage'], - ['desc'] - ) as PortfolioPosition[]; - - const dataSource: DataSource = - dataset[dataIndex].assetProfile.dataSource; - - const symbol: string = dataset[dataIndex].assetProfile.symbol; - - this.treemapChartClicked.emit({ dataSource, symbol }); + if (holding && canOpenHoldingDetail(holding)) { + this.treemapChartClicked.emit({ + dataSource: holding.assetProfile.dataSource, + symbol: holding.assetProfile.symbol + }); + } } catch {} }, - onHover: (event, chartElement) => { + onHover: (event, chartElements, chart: Chart<'treemap'>) => { if (this.cursor()) { + const holding = this.getHolding(chart, chartElements[0]); + (event.native?.target as HTMLElement).style.cursor = - chartElement[0] ? this.cursor() : 'default'; + holding && canOpenHoldingDetail(holding) + ? this.cursor() + : 'default'; } }, plugins: {