Browse Source

add symbol-autocomplete-comp, change assetProfileForm coverage, refactor endpoint

pull/4469/head
tobikugel 1 month ago
committed by Thomas Kaul
parent
commit
a437a1c722
  1. 11
      apps/api/src/app/admin/admin.controller.ts
  2. 59
      apps/api/src/app/admin/admin.service.ts
  3. 10
      apps/api/src/app/admin/update-asset-profile.dto.ts
  4. 22
      apps/api/src/services/market-data/market-data.service.ts
  5. 63
      apps/api/src/services/symbol-profile/symbol-profile.service.ts
  6. 20
      apps/client/src/app/components/admin-market-data/asset-profile-dialog/asset-profile-dialog.component.ts
  7. 86
      apps/client/src/app/components/admin-market-data/asset-profile-dialog/asset-profile-dialog.html
  8. 2
      apps/client/src/app/components/admin-market-data/asset-profile-dialog/asset-profile-dialog.module.ts

11
apps/api/src/app/admin/admin.controller.ts

@ -338,11 +338,12 @@ export class AdminController {
@Param('dataSource') dataSource: DataSource, @Param('dataSource') dataSource: DataSource,
@Param('symbol') symbol: string @Param('symbol') symbol: string
): Promise<EnhancedSymbolProfile> { ): Promise<EnhancedSymbolProfile> {
return this.adminService.patchAssetProfileData({ return this.adminService.patchAssetProfileData(
...assetProfileData, { dataSource, symbol },
dataSource, {
symbol ...assetProfileData
}); }
);
} }
@HasPermission(permissions.accessAdminControl) @HasPermission(permissions.accessAdminControl)

59
apps/api/src/app/admin/admin.service.ts

@ -463,7 +463,9 @@ export class AdminService {
return { count, users }; return { count, users };
} }
public async patchAssetProfileData({ public async patchAssetProfileData(
assetProfileIdentifier: AssetProfileIdentifier,
{
assetClass, assetClass,
assetSubClass, assetSubClass,
comment, comment,
@ -477,7 +479,41 @@ export class AdminService {
symbol, symbol,
symbolMapping, symbolMapping,
url url
}: AssetProfileIdentifier & Prisma.SymbolProfileUpdateInput) { }: 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
}
);
const [symbolProfile] = await this.symbolProfileService.getSymbolProfiles(
[
{
dataSource: dataSource as DataSource,
symbol: symbol as string
}
]
);
return symbolProfile;
} else {
const symbolProfileOverrides = { const symbolProfileOverrides = {
assetClass: assetClass as AssetClass, assetClass: assetClass as AssetClass,
assetSubClass: assetSubClass as AssetSubClass, assetSubClass: assetSubClass as AssetSubClass,
@ -485,8 +521,7 @@ export class AdminService {
url: url as string url: url as string
}; };
const updatedSymbolProfile: AssetProfileIdentifier & const updatedSymbolProfile: Prisma.SymbolProfileUpdateInput = {
Prisma.SymbolProfileUpdateInput = {
comment, comment,
countries, countries,
currency, currency,
@ -508,17 +543,23 @@ export class AdminService {
}) })
}; };
await this.symbolProfileService.updateSymbolProfile(updatedSymbolProfile); await this.symbolProfileService.updateSymbolProfile(
assetProfileIdentifier,
updatedSymbolProfile
);
const [symbolProfile] = await this.symbolProfileService.getSymbolProfiles([ const [symbolProfile] = await this.symbolProfileService.getSymbolProfiles(
[
{ {
dataSource, dataSource: dataSource as DataSource,
symbol symbol: symbol as string
} }
]); ]
);
return symbolProfile; return symbolProfile;
} }
}
public async putSetting(key: string, value: string) { public async putSetting(key: string, value: string) {
let response: Property; let response: Property;

10
apps/api/src/app/admin/update-asset-profile.dto.ts

@ -1,6 +1,6 @@
import { IsCurrencyCode } from '@ghostfolio/api/validators/is-currency-code'; 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 { import {
IsArray, IsArray,
IsEnum, IsEnum,
@ -35,6 +35,14 @@ export class UpdateAssetProfileDto {
@IsOptional() @IsOptional()
name?: string; name?: string;
@IsEnum(DataSource, { each: true })
@IsOptional()
dataSource?: DataSource;
@IsString()
@IsOptional()
symbol?: string;
@IsObject() @IsObject()
@IsOptional() @IsOptional()
scraperConfiguration?: Prisma.InputJsonObject; scraperConfiguration?: Prisma.InputJsonObject;

22
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: { public async updateMarketData(params: {
data: { data: {
state: MarketDataState; state: MarketDataState;

63
apps/api/src/services/symbol-profile/symbol-profile.service.ts

@ -126,23 +126,78 @@ export class SymbolProfileService {
}); });
} }
public updateSymbolProfile({ public async getSymbolProfilesByUserSubscription({
withUserSubscription = false
}: {
withUserSubscription?: boolean;
}) {
return this.prismaService.symbolProfile.findMany({
include: {
Order: {
include: {
User: true
}
}
},
orderBy: [{ symbol: 'asc' }],
where: {
Order: withUserSubscription
? {
some: {
User: {
Subscription: { some: { expiresAt: { gt: new Date() } } }
}
}
}
: {
every: {
User: {
Subscription: { none: { expiresAt: { gt: new Date() } } }
}
}
}
}
});
}
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, assetClass,
assetSubClass, assetSubClass,
comment, comment,
countries, countries,
currency, currency,
dataSource, //dataSource,
holdings, holdings,
isActive, isActive,
name, name,
scraperConfiguration, scraperConfiguration,
sectors, sectors,
symbol, //symbol,
symbolMapping, symbolMapping,
SymbolProfileOverrides, SymbolProfileOverrides,
url url
}: AssetProfileIdentifier & Prisma.SymbolProfileUpdateInput) { }: Prisma.SymbolProfileUpdateInput
) {
return this.prismaService.symbolProfile.update({ return this.prismaService.symbolProfile.update({
data: { data: {
assetClass, assetClass,

20
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) }; return { id: assetSubClass, label: translate(assetSubClass) };
}); });
public assetProfile: AdminMarketDataDetails['assetProfile']; public assetProfile: AdminMarketDataDetails['assetProfile'];
public assetProfileIdentifierForm;
public assetProfileForm = this.formBuilder.group({ public assetProfileForm = this.formBuilder.group({
assetClass: new FormControl<AssetClass>(undefined), assetClass: new FormControl<AssetClass>(undefined),
assetSubClass: new FormControl<AssetSubClass>(undefined), assetSubClass: new FormControl<AssetSubClass>(undefined),
@ -86,6 +87,7 @@ export class AssetProfileDialog implements OnDestroy, OnInit {
public ghostfolioScraperApiSymbolPrefix = ghostfolioScraperApiSymbolPrefix; public ghostfolioScraperApiSymbolPrefix = ghostfolioScraperApiSymbolPrefix;
public historicalDataItems: LineChartItem[]; public historicalDataItems: LineChartItem[];
public isBenchmark = false; public isBenchmark = false;
public isEditSymbolMode = false;
public marketDataItems: MarketData[] = []; public marketDataItems: MarketData[] = [];
public modeValues = [ 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 countries = [];
let scraperConfiguration = {}; let scraperConfiguration = {};
let sectors = []; let sectors = [];

86
apps/client/src/app/components/admin-market-data/asset-profile-dialog/asset-profile-dialog.html

@ -1,9 +1,4 @@
<form <div class="d-flex flex-column h-100">
class="d-flex flex-column h-100"
[formGroup]="assetProfileForm"
(keyup.enter)="assetProfileForm.valid && onSubmit()"
(ngSubmit)="onSubmit()"
>
<div class="d-flex mb-3"> <div class="d-flex mb-3">
<h1 class="flex-grow-1 m-0" mat-dialog-title> <h1 class="flex-grow-1 m-0" mat-dialog-title>
{{ assetProfile?.name ?? data.symbol }} {{ assetProfile?.name ?? data.symbol }}
@ -91,12 +86,34 @@
/> />
<div class="row"> <div class="row">
@if (isEditSymbolMode) {
<div class="col-12 mb-3">
<mat-form-field appearance="outline">
<mat-label i18n>Name, symbol or ISIN</mat-label>
<gf-symbol-autocomplete
formControlName="searchSymbol"
[includeIndices]="true"
/>
</mat-form-field>
<button class="mx-1 no-min-width px-2" mat-button type="button">
Apply
</button>
<button
class="mx-1 no-min-width px-2"
mat-button
type="button"
(click)="onCancelEditSymboleMode()"
>
Cancel
</button>
</div>
} @else {
<div class="col-6 mb-3"> <div class="col-6 mb-3">
<gf-value i18n size="medium" [value]="assetProfile?.symbol" <gf-value i18n size="medium" [value]="assetProfile?.symbol"
>Symbol</gf-value >Symbol</gf-value
> >
</div> </div>
<div class="col-6 mb-3"> <div class="col-4 mb-3">
<gf-value <gf-value
i18n i18n
size="medium" size="medium"
@ -106,6 +123,18 @@
>Data Source</gf-value >Data Source</gf-value
> >
</div> </div>
<div class="col-1 mb-3">
<button
class="mx-1 no-min-width px-2"
mat-button
type="button"
[disabled]="assetProfileForm.dirty"
(click)="onSetEditSymboleMode()"
>
<ion-icon name="create-outline"></ion-icon>
</button>
</div>
}
<div class="col-6 mb-3"> <div class="col-6 mb-3">
<gf-value i18n size="medium" [value]="assetProfile?.currency" <gf-value i18n size="medium" [value]="assetProfile?.currency"
>Currency</gf-value >Currency</gf-value
@ -202,6 +231,11 @@
} }
} }
</div> </div>
<form
[formGroup]="assetProfileForm"
(keyup.enter)="assetProfileForm.valid && onSubmitAssetProfileForm()"
(ngSubmit)="onSubmitAssetProfileForm()"
>
<div class="mt-3"> <div class="mt-3">
<mat-form-field appearance="outline" class="w-100 without-hint"> <mat-form-field appearance="outline" class="w-100 without-hint">
<mat-label i18n>Name</mat-label> <mat-label i18n>Name</mat-label>
@ -251,6 +285,7 @@
color="primary" color="primary"
i18n i18n
[checked]="isBenchmark" [checked]="isBenchmark"
[disabled]="isEditSymbolMode"
(change)=" (change)="
isBenchmark isBenchmark
? onUnsetBenchmark({ ? onUnsetBenchmark({
@ -296,7 +331,10 @@
</mat-expansion-panel-header> </mat-expansion-panel-header>
<div formGroupName="scraperConfiguration"> <div formGroupName="scraperConfiguration">
<div class="mt-3"> <div class="mt-3">
<mat-form-field appearance="outline" class="w-100 without-hint"> <mat-form-field
appearance="outline"
class="w-100 without-hint"
>
<mat-label i18n>Default Market Price</mat-label> <mat-label i18n>Default Market Price</mat-label>
<input <input
formControlName="defaultMarketPrice" formControlName="defaultMarketPrice"
@ -306,7 +344,10 @@
</mat-form-field> </mat-form-field>
</div> </div>
<div class="mt-3"> <div class="mt-3">
<mat-form-field appearance="outline" class="w-100 without-hint"> <mat-form-field
appearance="outline"
class="w-100 without-hint"
>
<mat-label i18n>HTTP Request Headers</mat-label> <mat-label i18n>HTTP Request Headers</mat-label>
<textarea <textarea
cdkTextareaAutosize cdkTextareaAutosize
@ -318,13 +359,19 @@
</mat-form-field> </mat-form-field>
</div> </div>
<div class="mt-3"> <div class="mt-3">
<mat-form-field appearance="outline" class="w-100 without-hint"> <mat-form-field
appearance="outline"
class="w-100 without-hint"
>
<mat-label i18n>Locale</mat-label> <mat-label i18n>Locale</mat-label>
<input formControlName="locale" matInput type="text" /> <input formControlName="locale" matInput type="text" />
</mat-form-field> </mat-form-field>
</div> </div>
<div class="mt-3"> <div class="mt-3">
<mat-form-field appearance="outline" class="w-100 without-hint"> <mat-form-field
appearance="outline"
class="w-100 without-hint"
>
<mat-label i18n>Mode</mat-label> <mat-label i18n>Mode</mat-label>
<mat-select formControlName="mode"> <mat-select formControlName="mode">
@for (modeValue of modeValues; track modeValue) { @for (modeValue of modeValues; track modeValue) {
@ -336,7 +383,10 @@
</mat-form-field> </mat-form-field>
</div> </div>
<div class="mt-3"> <div class="mt-3">
<mat-form-field appearance="outline" class="w-100 without-hint"> <mat-form-field
appearance="outline"
class="w-100 without-hint"
>
<mat-label> <mat-label>
<ng-container i18n>Selector</ng-container>* <ng-container i18n>Selector</ng-container>*
</mat-label> </mat-label>
@ -349,7 +399,10 @@
</mat-form-field> </mat-form-field>
</div> </div>
<div class="mt-3"> <div class="mt-3">
<mat-form-field appearance="outline" class="w-100 without-hint"> <mat-form-field
appearance="outline"
class="w-100 without-hint"
>
<mat-label> <mat-label>
<ng-container i18n>Url</ng-container>* <ng-container i18n>Url</ng-container>*
</mat-label> </mat-label>
@ -364,8 +417,8 @@
[disabled]=" [disabled]="
assetProfileForm.controls.scraperConfiguration.controls assetProfileForm.controls.scraperConfiguration.controls
.selector.value === '' || .selector.value === '' ||
assetProfileForm.controls.scraperConfiguration.controls.url assetProfileForm.controls.scraperConfiguration.controls
.value === '' .url.value === ''
" "
(click)="onTestMarketData()" (click)="onTestMarketData()"
> >
@ -426,6 +479,7 @@
></textarea> ></textarea>
</mat-form-field> </mat-form-field>
</div> </div>
</form>
</div> </div>
<div class="d-flex justify-content-end" mat-dialog-actions> <div class="d-flex justify-content-end" mat-dialog-actions>
@ -439,4 +493,4 @@
<ng-container i18n>Save</ng-container> <ng-container i18n>Save</ng-container>
</button> </button>
</div> </div>
</form> </div>

2
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 { GfHistoricalMarketDataEditorComponent } from '@ghostfolio/ui/historical-market-data-editor';
import { GfLineChartComponent } from '@ghostfolio/ui/line-chart'; import { GfLineChartComponent } from '@ghostfolio/ui/line-chart';
import { GfPortfolioProportionChartComponent } from '@ghostfolio/ui/portfolio-proportion-chart'; import { GfPortfolioProportionChartComponent } from '@ghostfolio/ui/portfolio-proportion-chart';
import { GfSymbolAutocompleteComponent } from '@ghostfolio/ui/symbol-autocomplete';
import { GfValueComponent } from '@ghostfolio/ui/value'; import { GfValueComponent } from '@ghostfolio/ui/value';
import { TextFieldModule } from '@angular/cdk/text-field'; import { TextFieldModule } from '@angular/cdk/text-field';
@ -30,6 +31,7 @@ import { AssetProfileDialog } from './asset-profile-dialog.component';
GfCurrencySelectorComponent, GfCurrencySelectorComponent,
GfHistoricalMarketDataEditorComponent, GfHistoricalMarketDataEditorComponent,
GfLineChartComponent, GfLineChartComponent,
GfSymbolAutocompleteComponent,
GfPortfolioProportionChartComponent, GfPortfolioProportionChartComponent,
GfValueComponent, GfValueComponent,
MatButtonModule, MatButtonModule,

Loading…
Cancel
Save