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
- 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

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 { 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)

8
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();

10
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<PortfolioPosition, 'assetProfile'>): boolean {
return assetProfile?.assetSubClass !== AssetSubClass.CASH;
}
export function capitalize(aString: string) {
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 {
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({

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

@ -66,6 +66,7 @@ export class GfPortfolioProportionChartComponent
@Input() data: {
[symbol: string]: Pick<PortfolioPosition, 'type'> & {
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: {

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

@ -120,13 +120,8 @@
<tr
*matRowDef="let row; columns: displayedColumns"
mat-row
[class.cursor-pointer]="row.position"
(click)="
onClickHolding({
dataSource: row.position.assetProfile.dataSource,
symbol: row.position.assetProfile.symbol
})
"
[class.cursor-pointer]="canShowDetails(row)"
(click)="onClickHolding(row)"
></tr>
<tr
*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 {
AssetProfileIdentifier,
HoldingWithParents
HoldingWithParents,
PortfolioPosition
} from '@ghostfolio/common/interfaces';
import {
@ -82,8 +83,19 @@ export class GfTopHoldingsComponent implements OnChanges {
}
}
public onClickHolding({ dataSource, symbol }: AssetProfileIdentifier) {
this.holdingClicked.emit({ dataSource, symbol });
public canShowDetails(holding: { position?: PortfolioPosition }): boolean {
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() {

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

@ -3,7 +3,7 @@ import {
getIntervalFromDateRange
} from '@ghostfolio/common/calculation-helper';
import { getTooltipOptions } from '@ghostfolio/common/chart-helper';
import { getLocale } from '@ghostfolio/common/helper';
import { canOpenHoldingDetail, getLocale } from '@ghostfolio/common/helper';
import {
AssetProfileIdentifier,
PortfolioPosition
@ -21,9 +21,8 @@ import {
output,
viewChild
} from '@angular/core';
import { DataSource } from '@prisma/client';
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 { TreemapController, TreemapElement } from 'chartjs-chart-treemap';
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() {
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: {

Loading…
Cancel
Save