Browse Source

Bugfix/skip opening holding detail dialog for cash positions (#7390)

* Skip opening holding detail dialog for cash positions

* Update changelog
pull/7397/head
Thomas Kaul 2 days ago
committed by GitHub
parent
commit
347d7efee5
No known key found for this signature in database GPG Key ID: B5690EEEBB952194
  1. 1
      CHANGELOG.md
  2. 7
      apps/client/src/app/pages/portfolio/allocations/allocations-page.component.ts
  3. 8
      apps/client/src/app/pages/portfolio/analysis/analysis-page.component.ts
  4. 10
      libs/common/src/lib/helper.ts
  5. 16
      libs/ui/src/lib/holdings-table/holdings-table.component.ts
  6. 15
      libs/ui/src/lib/portfolio-proportion-chart/portfolio-proportion-chart.component.ts
  7. 9
      libs/ui/src/lib/top-holdings/top-holdings.component.html
  8. 20
      libs/ui/src/lib/top-holdings/top-holdings.component.ts
  9. 51
      libs/ui/src/lib/treemap-chart/treemap-chart.component.ts

1
CHANGELOG.md

@ -9,6 +9,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
### Fixed ### 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 - 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 - Improved the error handling in the access endpoints (`POST` and `PUT`) to return `400 Bad Request` when granting access to a non-existent user

7
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 { ImpersonationStorageService } from '@ghostfolio/client/services/impersonation-storage.service';
import { UserService } from '@ghostfolio/client/services/user/user.service'; import { UserService } from '@ghostfolio/client/services/user/user.service';
import { MAX_TOP_HOLDINGS, UNKNOWN_KEY } from '@ghostfolio/common/config'; import { MAX_TOP_HOLDINGS, UNKNOWN_KEY } from '@ghostfolio/common/config';
import { getCountryName } from '@ghostfolio/common/helper'; import {
canOpenHoldingDetail,
getCountryName
} from '@ghostfolio/common/helper';
import { import {
AssetProfileIdentifier, AssetProfileIdentifier,
HoldingWithParents, HoldingWithParents,
@ -116,6 +119,7 @@ export class GfAllocationsPageComponent implements OnInit {
protected symbols: { protected symbols: {
[name: string]: { [name: string]: {
dataSource?: DataSource; dataSource?: DataSource;
isClickable?: boolean;
name: string; name: string;
symbol: string; symbol: string;
value: number; value: number;
@ -498,6 +502,7 @@ export class GfAllocationsPageComponent implements OnInit {
this.symbols[symbol] = { this.symbols[symbol] = {
symbol, symbol,
dataSource: position.assetProfile.dataSource, dataSource: position.assetProfile.dataSource,
isClickable: canOpenHoldingDetail(position),
name: position.assetProfile.name ?? '', name: position.assetProfile.name ?? '',
value: value:
(isNumber(position.valueInBaseCurrency) (isNumber(position.valueInBaseCurrency)

8
apps/client/src/app/pages/portfolio/analysis/analysis-page.component.ts

@ -6,6 +6,7 @@ import {
DEFAULT_DATE_RANGE, DEFAULT_DATE_RANGE,
NUMERICAL_PRECISION_THRESHOLD_6_FIGURES NUMERICAL_PRECISION_THRESHOLD_6_FIGURES
} from '@ghostfolio/common/config'; } from '@ghostfolio/common/config';
import { canOpenHoldingDetail } from '@ghostfolio/common/helper';
import { import {
HistoricalDataItem, HistoricalDataItem,
InvestmentItem, InvestmentItem,
@ -365,8 +366,11 @@ export class GfAnalysisPageComponent implements OnInit {
.pipe(takeUntilDestroyed(this.destroyRef)) .pipe(takeUntilDestroyed(this.destroyRef))
.subscribe(({ holdings }) => { .subscribe(({ holdings }) => {
const holdingsSorted = sortBy( const holdingsSorted = sortBy(
holdings.filter(({ netPerformancePercentWithCurrencyEffect }) => { holdings.filter((holding) => {
return isNumber(netPerformancePercentWithCurrencyEffect); return (
canOpenHoldingDetail(holding) &&
isNumber(holding.netPerformancePercentWithCurrencyEffect)
);
}), }),
'netPerformancePercentWithCurrencyEffect' 'netPerformancePercentWithCurrencyEffect'
).reverse(); ).reverse();

10
libs/common/src/lib/helper.ts

@ -2,6 +2,7 @@ import { NumberParser } from '@internationalized/number';
import { import {
Type as ActivityType, Type as ActivityType,
AssetProfileOverrides, AssetProfileOverrides,
AssetSubClass,
MarketData, MarketData,
Prisma, Prisma,
SymbolProfile SymbolProfile
@ -45,7 +46,8 @@ import {
import { import {
AssetProfileIdentifier, AssetProfileIdentifier,
AssetProfileItem, AssetProfileItem,
Benchmark Benchmark,
PortfolioPosition
} from './interfaces'; } from './interfaces';
import { BenchmarkTrend, ColorScheme } from './types'; import { BenchmarkTrend, ColorScheme } from './types';
@ -172,6 +174,12 @@ export function canDeleteUser({
return currentUserId !== userId; return currentUserId !== userId;
} }
export function canOpenHoldingDetail({
assetProfile
}: Pick<PortfolioPosition, 'assetProfile'>): boolean {
return assetProfile?.assetSubClass !== AssetSubClass.CASH;
}
export function capitalize(aString: string) { export function capitalize(aString: string) {
return aString.charAt(0).toUpperCase() + aString.slice(1).toLowerCase(); return aString.charAt(0).toUpperCase() + aString.slice(1).toLowerCase();
} }

16
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 { import {
AssetProfileIdentifier, AssetProfileIdentifier,
PortfolioPosition PortfolioPosition
@ -20,7 +24,6 @@ import { MatDialogModule } from '@angular/material/dialog';
import { MatPaginator, MatPaginatorModule } from '@angular/material/paginator'; import { MatPaginator, MatPaginatorModule } from '@angular/material/paginator';
import { MatSort, MatSortModule } from '@angular/material/sort'; 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 { NgxSkeletonLoaderModule } from 'ngx-skeleton-loader'; import { NgxSkeletonLoaderModule } from 'ngx-skeleton-loader';
import { GfEntityLogoComponent } from '../entity-logo/entity-logo.component'; import { GfEntityLogoComponent } from '../entity-logo/entity-logo.component';
@ -79,10 +82,6 @@ export class GfHoldingsTableComponent {
return columns; return columns;
}); });
protected readonly ignoreAssetSubClasses: AssetSubClass[] = [
AssetSubClass.CASH
];
protected readonly isLoading = computed(() => !this.holdings()); protected readonly isLoading = computed(() => !this.holdings());
public constructor() { public constructor() {
@ -101,10 +100,7 @@ export class GfHoldingsTableComponent {
} }
protected canShowDetails(holding: PortfolioPosition): boolean { protected canShowDetails(holding: PortfolioPosition): boolean {
return ( return this.hasPermissionToOpenDetails() && canOpenHoldingDetail(holding);
this.hasPermissionToOpenDetails() &&
!this.ignoreAssetSubClasses.includes(holding.assetProfile.assetSubClass)
);
} }
protected onOpenHoldingDialog({ protected onOpenHoldingDialog({

15
libs/ui/src/lib/portfolio-proportion-chart/portfolio-proportion-chart.component.ts

@ -66,6 +66,7 @@ export class GfPortfolioProportionChartComponent
@Input() data: { @Input() data: {
[symbol: string]: Pick<PortfolioPosition, 'type'> & { [symbol: string]: Pick<PortfolioPosition, 'type'> & {
dataSource?: DataSource; dataSource?: DataSource;
isClickable?: boolean;
name: string; name: string;
value: number; value: number;
}; };
@ -356,6 +357,10 @@ export class GfPortfolioProportionChartComponent
const dataIndex = activeElements[0].index; const dataIndex = activeElements[0].index;
const symbol = chart.data.labels?.[dataIndex] as string; const symbol = chart.data.labels?.[dataIndex] as string;
if (this.data[symbol]?.isClickable === false) {
return;
}
const dataSource = this.data[symbol]?.dataSource; const dataSource = this.data[symbol]?.dataSource;
this.proportionChartClicked.emit( this.proportionChartClicked.emit(
@ -363,10 +368,16 @@ export class GfPortfolioProportionChartComponent
); );
} catch {} } catch {}
}, },
onHover: (event, chartElement) => { onHover: (event, chartElement, chart) => {
if (this.cursor) { if (this.cursor) {
const symbol = chartElement[0]
? (chart.data.labels?.[chartElement[0].index] as string)
: undefined;
(event.native?.target as HTMLElement).style.cursor = (event.native?.target as HTMLElement).style.cursor =
chartElement[0] ? this.cursor : 'default'; symbol && this.data[symbol]?.isClickable !== false
? this.cursor
: 'default';
} }
}, },
plugins: { plugins: {

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

@ -120,13 +120,8 @@
<tr <tr
*matRowDef="let row; columns: displayedColumns" *matRowDef="let row; columns: displayedColumns"
mat-row mat-row
[class.cursor-pointer]="row.position" [class.cursor-pointer]="canShowDetails(row)"
(click)=" (click)="onClickHolding(row)"
onClickHolding({
dataSource: row.position.assetProfile.dataSource,
symbol: row.position.assetProfile.symbol
})
"
></tr> ></tr>
<tr <tr
*matFooterRowDef="displayedColumns" *matFooterRowDef="displayedColumns"

20
libs/ui/src/lib/top-holdings/top-holdings.component.ts

@ -1,7 +1,8 @@
import { getLocale } from '@ghostfolio/common/helper'; import { canOpenHoldingDetail, getLocale } from '@ghostfolio/common/helper';
import { import {
AssetProfileIdentifier, AssetProfileIdentifier,
HoldingWithParents HoldingWithParents,
PortfolioPosition
} from '@ghostfolio/common/interfaces'; } from '@ghostfolio/common/interfaces';
import { import {
@ -82,8 +83,19 @@ export class GfTopHoldingsComponent implements OnChanges {
} }
} }
public onClickHolding({ dataSource, symbol }: AssetProfileIdentifier) { public canShowDetails(holding: { position?: PortfolioPosition }): boolean {
this.holdingClicked.emit({ dataSource, symbol }); return !!holding?.position && canOpenHoldingDetail(holding.position);
}
public onClickHolding({ position }: { position?: PortfolioPosition }) {
if (!position || !canOpenHoldingDetail(position)) {
return;
}
this.holdingClicked.emit({
dataSource: position.assetProfile.dataSource,
symbol: position.assetProfile.symbol
});
} }
public onShowAllHoldings() { public onShowAllHoldings() {

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

@ -3,7 +3,7 @@ import {
getIntervalFromDateRange getIntervalFromDateRange
} from '@ghostfolio/common/calculation-helper'; } from '@ghostfolio/common/calculation-helper';
import { getTooltipOptions } from '@ghostfolio/common/chart-helper'; import { getTooltipOptions } from '@ghostfolio/common/chart-helper';
import { getLocale } from '@ghostfolio/common/helper'; import { canOpenHoldingDetail, getLocale } from '@ghostfolio/common/helper';
import { import {
AssetProfileIdentifier, AssetProfileIdentifier,
PortfolioPosition PortfolioPosition
@ -21,9 +21,8 @@ import {
output, output,
viewChild viewChild
} from '@angular/core'; } from '@angular/core';
import { DataSource } from '@prisma/client';
import { Big } from 'big.js'; import { Big } from 'big.js';
import type { ChartData, TooltipOptions } from 'chart.js'; import type { ActiveElement, ChartData, TooltipOptions } from 'chart.js';
import { Chart, LinearScale, Tooltip } from 'chart.js'; import { Chart, LinearScale, Tooltip } from 'chart.js';
import { TreemapController, TreemapElement } from 'chartjs-chart-treemap'; import { TreemapController, TreemapElement } from 'chartjs-chart-treemap';
import { isUUID } from 'class-validator'; import { isUUID } from 'class-validator';
@ -154,6 +153,23 @@ export class GfTreemapChartComponent
} }
} }
private getHolding(
chart: Chart<'treemap'>,
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() { private initialize() {
const holdings = this.holdings(); const holdings = this.holdings();
@ -323,27 +339,24 @@ export class GfTreemapChartComponent
animation: false, animation: false,
onClick: (_, activeElements, chart: Chart<'treemap'>) => { onClick: (_, activeElements, chart: Chart<'treemap'>) => {
try { try {
const dataIndex = activeElements[0].index; const holding = this.getHolding(chart, activeElements[0]);
const datasetIndex = activeElements[0].datasetIndex;
const dataset = orderBy( if (holding && canOpenHoldingDetail(holding)) {
chart.data.datasets[datasetIndex].tree, this.treemapChartClicked.emit({
['allocationInPercentage'], dataSource: holding.assetProfile.dataSource,
['desc'] symbol: holding.assetProfile.symbol
) as PortfolioPosition[]; });
}
const dataSource: DataSource =
dataset[dataIndex].assetProfile.dataSource;
const symbol: string = dataset[dataIndex].assetProfile.symbol;
this.treemapChartClicked.emit({ dataSource, symbol });
} catch {} } catch {}
}, },
onHover: (event, chartElement) => { onHover: (event, chartElements, chart: Chart<'treemap'>) => {
if (this.cursor()) { if (this.cursor()) {
const holding = this.getHolding(chart, chartElements[0]);
(event.native?.target as HTMLElement).style.cursor = (event.native?.target as HTMLElement).style.cursor =
chartElement[0] ? this.cursor() : 'default'; holding && canOpenHoldingDetail(holding)
? this.cursor()
: 'default';
} }
}, },
plugins: { plugins: {

Loading…
Cancel
Save