From 68fc365dde59b733d1100ae656b0337199823609 Mon Sep 17 00:00:00 2001 From: tobikugel Date: Mon, 17 Mar 2025 11:33:11 -0300 Subject: [PATCH] add symbol-autocomplete-comp, change assetProfileForm coverage, refactor endpoint --- apps/api/src/app/admin/admin.controller.ts | 11 +- apps/api/src/app/admin/admin.service.ts | 125 +++-- .../src/app/admin/update-asset-profile.dto.ts | 10 +- .../market-data/market-data.service.ts | 22 + .../symbol-profile/symbol-profile.service.ts | 53 +- .../asset-profile-dialog.component.ts | 20 +- .../asset-profile-dialog.html | 498 ++++++++++-------- .../asset-profile-dialog.module.ts | 2 + 8 files changed, 454 insertions(+), 287 deletions(-) diff --git a/apps/api/src/app/admin/admin.controller.ts b/apps/api/src/app/admin/admin.controller.ts index a761bbbae..a9018f08e 100644 --- a/apps/api/src/app/admin/admin.controller.ts +++ b/apps/api/src/app/admin/admin.controller.ts @@ -338,11 +338,12 @@ export class AdminController { @Param('dataSource') dataSource: DataSource, @Param('symbol') symbol: string ): Promise { - return this.adminService.patchAssetProfileData({ - ...assetProfileData, - dataSource, - symbol - }); + return this.adminService.patchAssetProfileData( + { dataSource, symbol }, + { + ...assetProfileData + } + ); } @HasPermission(permissions.accessAdminControl) diff --git a/apps/api/src/app/admin/admin.service.ts b/apps/api/src/app/admin/admin.service.ts index 7f2b0da5a..21c351b58 100644 --- a/apps/api/src/app/admin/admin.service.ts +++ b/apps/api/src/app/admin/admin.service.ts @@ -491,61 +491,102 @@ export class AdminService { return { count, users }; } - public async patchAssetProfileData({ - assetClass, - assetSubClass, - comment, - countries, - currency, - dataSource, - holdings, - name, - scraperConfiguration, - sectors, - symbol, - symbolMapping, - url - }: AssetProfileIdentifier & Prisma.SymbolProfileUpdateInput) { - const symbolProfileOverrides = { - assetClass: assetClass as AssetClass, - assetSubClass: assetSubClass as AssetSubClass, - name: name as string, - url: url as string - }; - - const updatedSymbolProfile: AssetProfileIdentifier & - Prisma.SymbolProfileUpdateInput = { + public async patchAssetProfileData( + assetProfileIdentifier: AssetProfileIdentifier, + { + assetClass, + assetSubClass, comment, countries, currency, dataSource, holdings, + name, scraperConfiguration, sectors, symbol, symbolMapping, - ...(dataSource === 'MANUAL' - ? { assetClass, assetSubClass, name, url } - : { - SymbolProfileOverrides: { - upsert: { - create: symbolProfileOverrides, - update: symbolProfileOverrides - } - } - }) - }; + url + }: Prisma.SymbolProfileUpdateInput + ) { + if ( + symbol && + dataSource && + assetProfileIdentifier.symbol !== symbol && + assetProfileIdentifier.dataSource !== dataSource + ) { + await this.symbolProfileService.updateAssetProfileIdentifier( + assetProfileIdentifier, + { + dataSource: dataSource as DataSource, // TODO change + symbol: symbol as string + } + ); + + await this.marketDataService.updateAssetProfileIdentifier( + assetProfileIdentifier, + { + dataSource: dataSource as DataSource, + symbol: symbol as string + } + ); - await this.symbolProfileService.updateSymbolProfile(updatedSymbolProfile); + const [symbolProfile] = await this.symbolProfileService.getSymbolProfiles( + [ + { + dataSource: dataSource as DataSource, + symbol: symbol as string + } + ] + ); - const [symbolProfile] = await this.symbolProfileService.getSymbolProfiles([ - { + return symbolProfile; + } else { + const symbolProfileOverrides = { + assetClass: assetClass as AssetClass, + assetSubClass: assetSubClass as AssetSubClass, + name: name as string, + url: url as string + }; + + const updatedSymbolProfile: Prisma.SymbolProfileUpdateInput = { + comment, + countries, + currency, dataSource, - symbol - } - ]); + holdings, + scraperConfiguration, + sectors, + symbol, + symbolMapping, + ...(dataSource === 'MANUAL' + ? { assetClass, assetSubClass, name, url } + : { + SymbolProfileOverrides: { + upsert: { + create: symbolProfileOverrides, + update: symbolProfileOverrides + } + } + }) + }; + + await this.symbolProfileService.updateSymbolProfile( + assetProfileIdentifier, + updatedSymbolProfile + ); - return symbolProfile; + const [symbolProfile] = await this.symbolProfileService.getSymbolProfiles( + [ + { + dataSource: dataSource as DataSource, + symbol: symbol as string + } + ] + ); + + return symbolProfile; + } } public async putSetting(key: string, value: string) { diff --git a/apps/api/src/app/admin/update-asset-profile.dto.ts b/apps/api/src/app/admin/update-asset-profile.dto.ts index 8c9ae220b..c9448e46f 100644 --- a/apps/api/src/app/admin/update-asset-profile.dto.ts +++ b/apps/api/src/app/admin/update-asset-profile.dto.ts @@ -1,6 +1,6 @@ import { IsCurrencyCode } from '@ghostfolio/api/validators/is-currency-code'; -import { AssetClass, AssetSubClass, Prisma } from '@prisma/client'; +import { AssetClass, AssetSubClass, DataSource, Prisma } from '@prisma/client'; import { IsArray, IsEnum, @@ -35,6 +35,14 @@ export class UpdateAssetProfileDto { @IsOptional() name?: string; + @IsEnum(DataSource, { each: true }) + @IsOptional() + dataSource?: DataSource; + + @IsString() + @IsOptional() + symbol?: string; + @IsObject() @IsOptional() scraperConfiguration?: Prisma.InputJsonObject; diff --git a/apps/api/src/services/market-data/market-data.service.ts b/apps/api/src/services/market-data/market-data.service.ts index c0abdf04e..0cab0bcc0 100644 --- a/apps/api/src/services/market-data/market-data.service.ts +++ b/apps/api/src/services/market-data/market-data.service.ts @@ -110,6 +110,28 @@ export class MarketDataService { }); } + public async updateAssetProfileIdentifier( + oldAssetProfileIdentifier: AssetProfileIdentifier, + newAssetProfileIdentifier: AssetProfileIdentifier + ) { + return this.prismaService.marketData.updateMany({ + data: { + dataSource: newAssetProfileIdentifier.dataSource, + symbol: newAssetProfileIdentifier.symbol + }, + where: { + AND: [ + { + dataSource: oldAssetProfileIdentifier.dataSource + }, + { + symbol: oldAssetProfileIdentifier.symbol + } + ] + } + }); + } + public async updateMarketData(params: { data: { state: MarketDataState; diff --git a/apps/api/src/services/symbol-profile/symbol-profile.service.ts b/apps/api/src/services/symbol-profile/symbol-profile.service.ts index 0dae63311..a3dc62a32 100644 --- a/apps/api/src/services/symbol-profile/symbol-profile.service.ts +++ b/apps/api/src/services/symbol-profile/symbol-profile.service.ts @@ -125,22 +125,43 @@ export class SymbolProfileService { }); } - public updateSymbolProfile({ - assetClass, - assetSubClass, - comment, - countries, - currency, - dataSource, - holdings, - name, - scraperConfiguration, - sectors, - symbol, - symbolMapping, - SymbolProfileOverrides, - url - }: AssetProfileIdentifier & Prisma.SymbolProfileUpdateInput) { + public updateAssetProfileIdentifier( + oldAssetProfileIdentifier: AssetProfileIdentifier, + newAssetProfileIdentifier: AssetProfileIdentifier + ) { + return this.prismaService.symbolProfile.update({ + data: { + dataSource: newAssetProfileIdentifier.dataSource, + symbol: newAssetProfileIdentifier.symbol + }, + where: { + dataSource_symbol: { + dataSource: oldAssetProfileIdentifier.dataSource, + symbol: oldAssetProfileIdentifier.symbol + } + } + }); + } + + public updateSymbolProfile( + { dataSource, symbol }: AssetProfileIdentifier, + { + assetClass, + assetSubClass, + comment, + countries, + currency, + //dataSource, + holdings, + name, + scraperConfiguration, + sectors, + //symbol, + symbolMapping, + SymbolProfileOverrides, + url + }: Prisma.SymbolProfileUpdateInput + ) { return this.prismaService.symbolProfile.update({ data: { assetClass, 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 1467a1ba3..5b966ed74 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 @@ -55,6 +55,7 @@ export class AssetProfileDialog implements OnDestroy, OnInit { return { id: assetSubClass, label: translate(assetSubClass) }; }); public assetProfile: AdminMarketDataDetails['assetProfile']; + public assetProfileIdentifierForm; public assetProfileForm = this.formBuilder.group({ assetClass: new FormControl(undefined), assetSubClass: new FormControl(undefined), @@ -86,6 +87,7 @@ export class AssetProfileDialog implements OnDestroy, OnInit { public ghostfolioScraperApiSymbolPrefix = ghostfolioScraperApiSymbolPrefix; public historicalDataItems: LineChartItem[]; public isBenchmark = false; + public isEditSymbolMode = false; public marketDataItems: MarketData[] = []; public modeValues = [ { @@ -269,7 +271,23 @@ export class AssetProfileDialog implements OnDestroy, OnInit { }); } - public async onSubmit() { + public onSetEditSymboleMode() { + this.isEditSymbolMode = true; + + this.assetProfileForm.disable(); + + this.changeDetectorRef.markForCheck(); + } + + public onCancelEditSymboleMode() { + this.isEditSymbolMode = false; + + this.assetProfileForm.enable(); + + this.changeDetectorRef.markForCheck(); + } + + public async onSubmitAssetProfileForm() { let countries = []; let scraperConfiguration = {}; let sectors = []; 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 595ec28cb..a0c0153de 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 @@ -1,9 +1,4 @@ -
+

{{ assetProfile?.name ?? data.symbol }} @@ -91,21 +86,55 @@ />
-
- Symbol -
-
- Data Source -
+ @if (isEditSymbolMode) { +
+ + Name, symbol or ISIN + + + + +
+ } @else { +
+ Symbol +
+
+ Data Source +
+
+ +
+ }
Currency -
- - Name - - -
- @if (assetProfile?.dataSource === 'MANUAL') { +
- Currency - + Name +
- } -
- - Asset Class - - - @for (assetClass of assetClasses; track assetClass) { - {{ - assetClass.label - }} - } - - -
-
- - Asset Sub Class - - - @for (assetSubClass of assetSubClasses; track assetSubClass) { - {{ - assetSubClass.label - }} - } - - -
-
-
- Benchmark + @if (assetProfile?.dataSource === 'MANUAL') { +
+ + Currency + + +
+ } +
+ + Asset Class + + + @for (assetClass of assetClasses; track assetClass) { + {{ + assetClass.label + }} + } + +
-
-
- - Symbol Mapping - - -
- @if (assetProfile?.dataSource === 'MANUAL') { -
- - + + Asset Sub Class + + + @for (assetSubClass of assetSubClasses; track assetSubClass) { + {{ + assetSubClass.label + }} + } + + +
+
+
+ Benchmark - - Scraper Configuration - -
-
- - Default Market Price - - -
-
- - HTTP Request Headers - - -
-
- - Locale - - -
-
- - Mode - - @for (modeValue of modeValues; track modeValue) { - {{ - modeValue.viewValue - }} - } - - -
-
- - - Selector* - - - -
-
- - - Url* - - - -
-
- -
-
- - +
- } - @if (assetProfile?.dataSource === 'MANUAL') { -
+
- Sectors + Symbol Mapping
+ @if (assetProfile?.dataSource === 'MANUAL') { +
+ + + + Scraper Configuration + +
+
+ + Default Market Price + + +
+
+ + HTTP Request Headers + + +
+
+ + Locale + + +
+
+ + Mode + + @for (modeValue of modeValues; track modeValue) { + {{ + modeValue.viewValue + }} + } + + +
+
+ + + Selector* + + + +
+
+ + + Url* + + + +
+
+ +
+
+
+
+
+ } + @if (assetProfile?.dataSource === 'MANUAL') { +
+ + Sectors + + +
+
+ + Countries + + +
+ }
+ + Url + + @if (assetProfileForm.get('url').value) { + + } + +
+
- Countries + Note
- } -
- - Url - - @if (assetProfileForm.get('url').value) { - - } - -
-
- - Note - - -
+
@@ -439,4 +493,4 @@ Save
- +
diff --git a/apps/client/src/app/components/admin-market-data/asset-profile-dialog/asset-profile-dialog.module.ts b/apps/client/src/app/components/admin-market-data/asset-profile-dialog/asset-profile-dialog.module.ts index 9b9876dbc..11818381f 100644 --- a/apps/client/src/app/components/admin-market-data/asset-profile-dialog/asset-profile-dialog.module.ts +++ b/apps/client/src/app/components/admin-market-data/asset-profile-dialog/asset-profile-dialog.module.ts @@ -4,6 +4,7 @@ import { GfCurrencySelectorComponent } from '@ghostfolio/ui/currency-selector'; import { GfHistoricalMarketDataEditorComponent } from '@ghostfolio/ui/historical-market-data-editor'; import { GfLineChartComponent } from '@ghostfolio/ui/line-chart'; import { GfPortfolioProportionChartComponent } from '@ghostfolio/ui/portfolio-proportion-chart'; +import { GfSymbolAutocompleteComponent } from '@ghostfolio/ui/symbol-autocomplete'; import { GfValueComponent } from '@ghostfolio/ui/value'; import { TextFieldModule } from '@angular/cdk/text-field'; @@ -30,6 +31,7 @@ import { AssetProfileDialog } from './asset-profile-dialog.component'; GfCurrencySelectorComponent, GfHistoricalMarketDataEditorComponent, GfLineChartComponent, + GfSymbolAutocompleteComponent, GfPortfolioProportionChartComponent, GfValueComponent, MatButtonModule,