From 2f118d02b89c0f432df03ace3ae7d60454bc96c4 Mon Sep 17 00:00:00 2001 From: Attila Cseh <77381875+csehatt741@users.noreply.github.com> Date: Fri, 11 Jul 2025 18:22:46 +0200 Subject: [PATCH] Feature/filter asset sub class options based on selected asset class (#5148) * Filter asset sub class options based on selected asset class * Update changelog --- CHANGELOG.md | 1 + .../asset-profile-dialog.component.ts | 48 ++++++++++++++----- .../asset-profile-dialog.html | 26 ++++++---- .../interfaces/interfaces.ts | 7 ++- libs/common/src/lib/config.ts | 19 +++++++- 5 files changed, 78 insertions(+), 23 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index ffa8f6539..9eb230afb 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,6 +10,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Changed - Improved the portfolio calculations for activities without historical market data +- Improved the asset profile dialog’s asset sub class selector of the admin control panel to update the options dynamically based on the selected asset class - Improved the asset profile dialog’s data gathering checkbox of the admin control panel to reflect the global settings - Improved the language localization for Catalan (`ca`) - Improved the language localization for Chinese (`zh`) diff --git a/apps/client/src/app/components/admin-market-data/asset-profile-dialog/asset-profile-dialog.component.ts b/apps/client/src/app/components/admin-market-data/asset-profile-dialog/asset-profile-dialog.component.ts index 3dad22488..b9982cc81 100644 --- a/apps/client/src/app/components/admin-market-data/asset-profile-dialog/asset-profile-dialog.component.ts +++ b/apps/client/src/app/components/admin-market-data/asset-profile-dialog/asset-profile-dialog.component.ts @@ -6,6 +6,7 @@ import { DataService } from '@ghostfolio/client/services/data.service'; import { UserService } from '@ghostfolio/client/services/user/user.service'; import { validateObjectForForm } from '@ghostfolio/client/util/form.util'; import { + ASSET_CLASS_MAPPING, ghostfolioScraperApiSymbolPrefix, PROPERTY_IS_DATA_GATHERING_ENABLED } from '@ghostfolio/common/config'; @@ -56,7 +57,10 @@ import ms from 'ms'; import { EMPTY, Subject } from 'rxjs'; import { catchError, takeUntil } from 'rxjs/operators'; -import { AssetProfileDialogParams } from './interfaces/interfaces'; +import { + AssetClassSelectorOption, + AssetProfileDialogParams +} from './interfaces/interfaces'; @Component({ host: { class: 'd-flex flex-column h-100' }, @@ -75,15 +79,18 @@ export class AssetProfileDialog implements OnDestroy, OnInit { @ViewChild('assetProfileFormElement') assetProfileFormElement: ElementRef; - public assetProfileClass: string; + public assetClassLabel: string; + public assetSubClassLabel: string; - public assetClasses = Object.keys(AssetClass).map((assetClass) => { - return { id: assetClass, label: translate(assetClass) }; - }); + public assetClassOptions: AssetClassSelectorOption[] = Object.keys(AssetClass) + .map((id) => { + return { id, label: translate(id) } as AssetClassSelectorOption; + }) + .sort((a, b) => { + return a.label.localeCompare(b.label); + }); - public assetSubClasses = Object.keys(AssetSubClass).map((assetSubClass) => { - return { id: assetSubClass, label: translate(assetSubClass) }; - }); + public assetSubClassOptions: AssetClassSelectorOption[] = []; public assetProfile: AdminMarketDataDetails['assetProfile']; @@ -125,7 +132,6 @@ export class AssetProfileDialog implements OnDestroy, OnInit { } ); - public assetProfileSubClass: string; public benchmarks: Partial[]; public countries: { @@ -218,6 +224,26 @@ export class AssetProfileDialog implements OnDestroy, OnInit { } }); + this.assetProfileForm + .get('assetClass') + .valueChanges.pipe(takeUntil(this.unsubscribeSubject)) + .subscribe((assetClass) => { + const assetSubClasses = ASSET_CLASS_MAPPING.get(assetClass) ?? []; + + this.assetSubClassOptions = assetSubClasses + .map((assetSubClass) => { + return { + id: assetSubClass, + label: translate(assetSubClass) + }; + }) + .sort((a, b) => a.label.localeCompare(b.label)); + + this.assetProfileForm.get('assetSubClass').setValue(null); + + this.changeDetectorRef.markForCheck(); + }); + this.dataService .fetchMarketDataBySymbol({ dataSource: this.data.dataSource, @@ -227,8 +253,8 @@ export class AssetProfileDialog implements OnDestroy, OnInit { .subscribe(({ assetProfile, marketData }) => { this.assetProfile = assetProfile; - this.assetProfileClass = translate(this.assetProfile?.assetClass); - this.assetProfileSubClass = translate(this.assetProfile?.assetSubClass); + this.assetClassLabel = translate(this.assetProfile?.assetClass); + this.assetSubClassLabel = translate(this.assetProfile?.assetSubClass); this.countries = {}; this.isBenchmark = this.benchmarks.some(({ id }) => { diff --git a/apps/client/src/app/components/admin-market-data/asset-profile-dialog/asset-profile-dialog.html b/apps/client/src/app/components/admin-market-data/asset-profile-dialog/asset-profile-dialog.html index 0657d9fa5..53e87cf45 100644 --- a/apps/client/src/app/components/admin-market-data/asset-profile-dialog/asset-profile-dialog.html +++ b/apps/client/src/app/components/admin-market-data/asset-profile-dialog/asset-profile-dialog.html @@ -209,8 +209,8 @@ Asset Class @@ -218,8 +218,8 @@ Asset Sub Class @@ -304,9 +304,12 @@ Asset Class - @for (assetClass of assetClasses; track assetClass) { - {{ - assetClass.label + @for ( + assetClassOption of assetClassOptions; + track assetClassOption.id + ) { + {{ + assetClassOption.label }} } @@ -317,9 +320,12 @@ Asset Sub Class - @for (assetSubClass of assetSubClasses; track assetSubClass) { - {{ - assetSubClass.label + @for ( + assetSubClassOption of assetSubClassOptions; + track assetSubClassOption.id + ) { + {{ + assetSubClassOption.label }} } diff --git a/apps/client/src/app/components/admin-market-data/asset-profile-dialog/interfaces/interfaces.ts b/apps/client/src/app/components/admin-market-data/asset-profile-dialog/interfaces/interfaces.ts index 6a966b427..0de94c1cb 100644 --- a/apps/client/src/app/components/admin-market-data/asset-profile-dialog/interfaces/interfaces.ts +++ b/apps/client/src/app/components/admin-market-data/asset-profile-dialog/interfaces/interfaces.ts @@ -1,6 +1,11 @@ import { ColorScheme } from '@ghostfolio/common/types'; -import { DataSource } from '@prisma/client'; +import { AssetClass, AssetSubClass, DataSource } from '@prisma/client'; + +export interface AssetClassSelectorOption { + id: AssetClass | AssetSubClass; + label: string; +} export interface AssetProfileDialogParams { colorScheme: ColorScheme; diff --git a/libs/common/src/lib/config.ts b/libs/common/src/lib/config.ts index 4f3bbd77c..5aee7e0e6 100644 --- a/libs/common/src/lib/config.ts +++ b/libs/common/src/lib/config.ts @@ -1,4 +1,4 @@ -import { DataSource } from '@prisma/client'; +import { AssetClass, AssetSubClass, DataSource } from '@prisma/client'; import { JobOptions, JobStatus } from 'bull'; import ms from 'ms'; @@ -34,6 +34,23 @@ export const warnColorRgb = { b: 69 }; +export const ASSET_CLASS_MAPPING = new Map([ + [AssetClass.ALTERNATIVE_INVESTMENT, [AssetSubClass.COLLECTIBLE]], + [AssetClass.COMMODITY, [AssetSubClass.PRECIOUS_METAL]], + [ + AssetClass.EQUITY, + [ + AssetSubClass.ETF, + AssetSubClass.MUTUALFUND, + AssetSubClass.PRIVATE_EQUITY, + AssetSubClass.STOCK + ] + ], + [AssetClass.FIXED_INCOME, [AssetSubClass.BOND]], + [AssetClass.LIQUIDITY, [AssetSubClass.CRYPTOCURRENCY]], + [AssetClass.REAL_ESTATE, []] +]); + export const CACHE_TTL_NO_CACHE = 1; export const CACHE_TTL_INFINITE = 0;