From fd62153407890a96fbfa02a32a9ca25a577988f3 Mon Sep 17 00:00:00 2001 From: Dan Date: Mon, 6 Nov 2023 17:38:54 +0100 Subject: [PATCH] Further Work on Holdings on tags --- apps/api/src/app/admin/admin.controller.ts | 15 +++++- apps/api/src/app/admin/admin.service.ts | 20 ++++++-- .../src/app/admin/update-asset-profile.dto.ts | 4 +- .../symbol-profile/symbol-profile.service.ts | 15 ++++-- .../asset-profile-dialog.component.ts | 47 +++++++++++++++++-- .../asset-profile-dialog.html | 30 ++++++++++-- .../asset-profile-dialog.module.ts | 4 ++ apps/client/src/app/services/admin.service.ts | 6 ++- .../interfaces/admin-market-data.interface.ts | 3 +- .../enhanced-symbol-profile.interface.ts | 4 +- .../migration.sql | 20 ++++++++ prisma/schema.prisma | 2 + 12 files changed, 144 insertions(+), 26 deletions(-) create mode 100644 prisma/migrations/20231106132716_added_tag_to_symbol_profile/migration.sql diff --git a/apps/api/src/app/admin/admin.controller.ts b/apps/api/src/app/admin/admin.controller.ts index e277e77e4..14f24360f 100644 --- a/apps/api/src/app/admin/admin.controller.ts +++ b/apps/api/src/app/admin/admin.controller.ts @@ -448,10 +448,23 @@ export class AdminController { ); } + await this.adminService.patchAssetProfileData({ + dataSource, + symbol, + tags: { + set: [] + } + }); + return this.adminService.patchAssetProfileData({ ...assetProfileData, dataSource, - symbol + symbol, + tags: { + connect: assetProfileData.tags?.map(({ id }) => { + return { id }; + }) + } }); } diff --git a/apps/api/src/app/admin/admin.service.ts b/apps/api/src/app/admin/admin.service.ts index 4332bd8bb..a3733df01 100644 --- a/apps/api/src/app/admin/admin.service.ts +++ b/apps/api/src/app/admin/admin.service.ts @@ -23,7 +23,13 @@ import { } from '@ghostfolio/common/interfaces'; import { MarketDataPreset } from '@ghostfolio/common/types'; import { BadRequestException, Injectable } from '@nestjs/common'; -import { AssetSubClass, Prisma, Property, SymbolProfile } from '@prisma/client'; +import { + AssetSubClass, + Prisma, + Property, + SymbolProfile, + Tag +} from '@prisma/client'; import { differenceInDays } from 'date-fns'; import { groupBy } from 'lodash'; @@ -204,7 +210,8 @@ export class AdminService { }, scraperConfiguration: true, sectors: true, - symbol: true + symbol: true, + tags: true } }), this.prismaService.symbolProfile.count({ where }) @@ -222,7 +229,8 @@ export class AdminService { name, Order, sectors, - symbol + symbol, + tags }) => { const countriesCount = countries ? Object.keys(countries).length : 0; const marketDataItemCount = @@ -246,7 +254,8 @@ export class AdminService { marketDataItemCount, sectorsCount, activitiesCount: _count.Order, - date: Order?.[0]?.date + date: Order?.[0]?.date, + tags }; } ); @@ -378,7 +387,8 @@ export class AdminService { countriesCount: 0, currency: symbol.replace(DEFAULT_CURRENCY, ''), name: symbol, - sectorsCount: 0 + sectorsCount: 0, + tags: [] }; }); 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 d4efd8c8b..ee801cab5 100644 --- a/apps/api/src/app/admin/update-asset-profile.dto.ts +++ b/apps/api/src/app/admin/update-asset-profile.dto.ts @@ -1,4 +1,4 @@ -import { AssetClass, AssetSubClass, Prisma } from '@prisma/client'; +import { AssetClass, AssetSubClass, Prisma, Tag } from '@prisma/client'; import { IsArray, IsEnum, @@ -26,7 +26,7 @@ export class UpdateAssetProfileDto { @IsArray() @IsOptional() - tags?: string[]; + tags?: Tag[]; @IsObject() @IsOptional() 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 5b3ea1b7d..3747b07f3 100644 --- a/apps/api/src/services/symbol-profile/symbol-profile.service.ts +++ b/apps/api/src/services/symbol-profile/symbol-profile.service.ts @@ -8,7 +8,12 @@ import { import { Country } from '@ghostfolio/common/interfaces/country.interface'; import { Sector } from '@ghostfolio/common/interfaces/sector.interface'; import { Injectable } from '@nestjs/common'; -import { Prisma, SymbolProfile, SymbolProfileOverrides } from '@prisma/client'; +import { + Prisma, + SymbolProfile, + SymbolProfileOverrides, + Tag +} from '@prisma/client'; import { continents, countries } from 'countries-list'; @Injectable() @@ -49,6 +54,7 @@ export class SymbolProfileService { select: { date: true }, take: 1 }, + tags: true, SymbolProfileOverrides: true }, where: { @@ -72,7 +78,8 @@ export class SymbolProfileService { _count: { select: { Order: true } }, - SymbolProfileOverrides: true + SymbolProfileOverrides: true, + tags: true }, where: { id: { @@ -116,6 +123,7 @@ export class SymbolProfileService { Order?: { date: Date; }[]; + tags?: Tag[]; SymbolProfileOverrides: SymbolProfileOverrides; })[] ): EnhancedSymbolProfile[] { @@ -129,7 +137,8 @@ export class SymbolProfileService { dateOfFirstActivity: undefined, scraperConfiguration: this.getScraperConfiguration(symbolProfile), sectors: this.getSectors(symbolProfile), - symbolMapping: this.getSymbolMapping(symbolProfile) + symbolMapping: this.getSymbolMapping(symbolProfile), + tags: symbolProfile?.tags }; item.activitiesCount = symbolProfile._count.Order; 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 818564e24..4de9a1357 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 @@ -2,9 +2,11 @@ import { ChangeDetectionStrategy, ChangeDetectorRef, Component, + ElementRef, Inject, OnDestroy, - OnInit + OnInit, + ViewChild } from '@angular/core'; import { FormBuilder, FormControl, Validators } from '@angular/forms'; import { MAT_DIALOG_DATA, MatDialogRef } from '@angular/material/dialog'; @@ -21,7 +23,8 @@ import { AssetClass, AssetSubClass, MarketData, - SymbolProfile + SymbolProfile, + Tag } from '@prisma/client'; import { format } from 'date-fns'; import { parse as csvToJson } from 'papaparse'; @@ -29,6 +32,8 @@ import { Subject } from 'rxjs'; import { takeUntil } from 'rxjs/operators'; import { AssetProfileDialogParams } from './interfaces/interfaces'; +import { MatAutocompleteSelectedEvent } from '@angular/material/autocomplete'; +import { COMMA, ENTER } from '@angular/cdk/keycodes'; @Component({ host: { class: 'd-flex flex-column h-100' }, @@ -38,6 +43,8 @@ import { AssetProfileDialogParams } from './interfaces/interfaces'; styleUrls: ['./asset-profile-dialog.component.scss'] }) export class AssetProfileDialog implements OnDestroy, OnInit { + @ViewChild('tagInput') tagInput: ElementRef; + public separatorKeysCodes: number[] = [ENTER, COMMA]; public assetProfileClass: string; public assetClasses = Object.keys(AssetClass).map((assetClass) => { return { id: assetClass, label: translate(assetClass) }; @@ -51,7 +58,7 @@ export class AssetProfileDialog implements OnDestroy, OnInit { assetSubClass: new FormControl(undefined), comment: '', name: ['', Validators.required], - tags: new FormControl(undefined), + tags: new FormControl(undefined), scraperConfiguration: '', symbolMapping: '' }); @@ -67,7 +74,7 @@ export class AssetProfileDialog implements OnDestroy, OnInit { [name: string]: { name: string; value: number }; }; - public HoldingTags: { id: string; label: string }[]; + public HoldingTags: { id: string; name: string }[]; private static readonly HISTORICAL_DATA_TEMPLATE = `date;marketPrice\n${format( new Date(), @@ -94,6 +101,18 @@ export class AssetProfileDialog implements OnDestroy, OnInit { this.historicalDataAsCsvString = AssetProfileDialog.HISTORICAL_DATA_TEMPLATE; + this.adminService + .fetchTags() + .pipe(takeUntil(this.unsubscribeSubject)) + .subscribe((tags) => { + this.HoldingTags = tags.map(({ id, name }) => { + return { id, name }; + }); + this.dataService.updateInfo(); + + this.changeDetectorRef.markForCheck(); + }); + this.adminService .fetchAdminMarketDataBySymbol({ dataSource: this.data.dataSource, @@ -229,7 +248,7 @@ export class AssetProfileDialog implements OnDestroy, OnInit { assetSubClass: this.assetProfileForm.controls['assetSubClass'].value, comment: this.assetProfileForm.controls['comment'].value ?? null, name: this.assetProfileForm.controls['name'].value, - tag: this.assetProfileForm.controls['tags'].value, + tags: this.assetProfileForm.controls['tags'].value, scraperConfiguration, symbolMapping }; @@ -258,6 +277,24 @@ export class AssetProfileDialog implements OnDestroy, OnInit { }); } + public onRemoveTag(aTag: Tag) { + this.assetProfileForm.controls['tags'].setValue( + this.assetProfileForm.controls['tags'].value.filter(({ id }) => { + return id !== aTag.id; + }) + ); + } + + public onAddTag(event: MatAutocompleteSelectedEvent) { + this.assetProfileForm.controls['tags'].setValue([ + ...(this.assetProfileForm.controls['tags'].value ?? []), + this.HoldingTags.find(({ id }) => { + return id === event.option.value; + }) + ]); + this.tagInput.nativeElement.value = ''; + } + public ngOnDestroy() { this.unsubscribeSubject.next(); this.unsubscribeSubject.complete(); 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 4b521c929..27571a360 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 @@ -213,12 +213,32 @@
Tags - - - {{ tag.label }} + - + {{ tag.name }} + + + + + + + {{ tag.name }} + +
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 b836e4066..fcf918527 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 @@ -13,6 +13,8 @@ import { GfPortfolioProportionChartModule } from '@ghostfolio/ui/portfolio-propo import { GfValueModule } from '@ghostfolio/ui/value'; import { AssetProfileDialog } from './asset-profile-dialog.component'; +import { MatAutocompleteModule } from '@angular/material/autocomplete'; +import { MatChipsModule } from '@angular/material/chips'; @NgModule({ declarations: [AssetProfileDialog], @@ -21,6 +23,8 @@ import { AssetProfileDialog } from './asset-profile-dialog.component'; FormsModule, GfAdminMarketDataDetailModule, GfPortfolioProportionChartModule, + MatAutocompleteModule, + MatChipsModule, GfValueModule, MatButtonModule, MatCheckboxModule, diff --git a/apps/client/src/app/services/admin.service.ts b/apps/client/src/app/services/admin.service.ts index 042914ee4..b8ea0c70d 100644 --- a/apps/client/src/app/services/admin.service.ts +++ b/apps/client/src/app/services/admin.service.ts @@ -209,7 +209,8 @@ export class AdminService { name, scraperConfiguration, symbol, - symbolMapping + symbolMapping, + tags }: UniqueAsset & UpdateAssetProfileDto) { return this.http.patch( `/api/v1/admin/profile-data/${dataSource}/${symbol}`, @@ -219,7 +220,8 @@ export class AdminService { comment, name, scraperConfiguration, - symbolMapping + symbolMapping, + tags } ); } diff --git a/libs/common/src/lib/interfaces/admin-market-data.interface.ts b/libs/common/src/lib/interfaces/admin-market-data.interface.ts index 08838d4bc..791530b38 100644 --- a/libs/common/src/lib/interfaces/admin-market-data.interface.ts +++ b/libs/common/src/lib/interfaces/admin-market-data.interface.ts @@ -1,4 +1,4 @@ -import { AssetClass, AssetSubClass, DataSource } from '@prisma/client'; +import { AssetClass, AssetSubClass, DataSource, Tag } from '@prisma/client'; export interface AdminMarketData { count: number; @@ -16,4 +16,5 @@ export interface AdminMarketDataItem { name: string; sectorsCount: number; symbol: string; + tags: Tag[]; } diff --git a/libs/common/src/lib/interfaces/enhanced-symbol-profile.interface.ts b/libs/common/src/lib/interfaces/enhanced-symbol-profile.interface.ts index ba28babcf..f53d41354 100644 --- a/libs/common/src/lib/interfaces/enhanced-symbol-profile.interface.ts +++ b/libs/common/src/lib/interfaces/enhanced-symbol-profile.interface.ts @@ -1,4 +1,4 @@ -import { AssetClass, AssetSubClass, DataSource } from '@prisma/client'; +import { AssetClass, AssetSubClass, DataSource, Tag } from '@prisma/client'; import { Country } from './country.interface'; import { ScraperConfiguration } from './scraper-configuration.interface'; @@ -23,5 +23,5 @@ export interface EnhancedSymbolProfile { symbolMapping?: { [key: string]: string }; updatedAt: Date; url?: string; - tags?: string[]; + tags?: Tag[]; } diff --git a/prisma/migrations/20231106132716_added_tag_to_symbol_profile/migration.sql b/prisma/migrations/20231106132716_added_tag_to_symbol_profile/migration.sql new file mode 100644 index 000000000..b9fd176db --- /dev/null +++ b/prisma/migrations/20231106132716_added_tag_to_symbol_profile/migration.sql @@ -0,0 +1,20 @@ +-- AlterEnum +ALTER TYPE "Type" ADD VALUE 'STAKE'; + +-- CreateTable +CREATE TABLE "_SymbolProfileToTag" ( + "A" TEXT NOT NULL, + "B" TEXT NOT NULL +); + +-- CreateIndex +CREATE UNIQUE INDEX "_SymbolProfileToTag_AB_unique" ON "_SymbolProfileToTag"("A", "B"); + +-- CreateIndex +CREATE INDEX "_SymbolProfileToTag_B_index" ON "_SymbolProfileToTag"("B"); + +-- AddForeignKey +ALTER TABLE "_SymbolProfileToTag" ADD CONSTRAINT "_SymbolProfileToTag_A_fkey" FOREIGN KEY ("A") REFERENCES "SymbolProfile"("id") ON DELETE CASCADE ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "_SymbolProfileToTag" ADD CONSTRAINT "_SymbolProfileToTag_B_fkey" FOREIGN KEY ("B") REFERENCES "Tag"("id") ON DELETE CASCADE ON UPDATE CASCADE; diff --git a/prisma/schema.prisma b/prisma/schema.prisma index ea7b31a01..c7579b9cc 100644 --- a/prisma/schema.prisma +++ b/prisma/schema.prisma @@ -145,6 +145,7 @@ model SymbolProfile { symbolMapping Json? url String? Order Order[] + tags Tag[] SymbolProfileOverrides SymbolProfileOverrides? @@unique([dataSource, symbol]) @@ -176,6 +177,7 @@ model Tag { id String @id @default(uuid()) name String @unique orders Order[] + symbolProfile SymbolProfile[] } model User {