diff --git a/CHANGELOG.md b/CHANGELOG.md index d261f93ff..5638b923c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,6 +10,15 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Added - Supported enter key press to submit the form of the create or update account dialog +- Added the version to the admin control panel +- Added pagination parameters (`skip`, `take`) to the endpoint `GET api/v1/order` + +### Changed + +- Harmonized the settings icon of the user account page +- Improved the usability to set an asset profile as a benchmark +- Reload platforms after making a change in the admin control panel +- Reload tags after making a change in the admin control panel ### Fixed diff --git a/apps/api/src/app/benchmark/benchmark.controller.ts b/apps/api/src/app/benchmark/benchmark.controller.ts index d59a231ff..2230ff42b 100644 --- a/apps/api/src/app/benchmark/benchmark.controller.ts +++ b/apps/api/src/app/benchmark/benchmark.controller.ts @@ -10,6 +10,7 @@ import type { RequestWithUser } from '@ghostfolio/common/types'; import { Body, Controller, + Delete, Get, HttpException, Inject, @@ -32,35 +33,49 @@ export class BenchmarkController { @Inject(REQUEST) private readonly request: RequestWithUser ) {} - @Get() - @UseInterceptors(TransformDataSourceInRequestInterceptor) - @UseInterceptors(TransformDataSourceInResponseInterceptor) - public async getBenchmark(): Promise { - return { - benchmarks: await this.benchmarkService.getBenchmarks() - }; - } - - @Get(':dataSource/:symbol/:startDateString') + @Post() @UseGuards(AuthGuard('jwt')) - @UseInterceptors(TransformDataSourceInRequestInterceptor) - public async getBenchmarkMarketDataBySymbol( - @Param('dataSource') dataSource: DataSource, - @Param('startDateString') startDateString: string, - @Param('symbol') symbol: string - ): Promise { - const startDate = new Date(startDateString); + public async addBenchmark(@Body() { dataSource, symbol }: UniqueAsset) { + if ( + !hasPermission( + this.request.user.permissions, + permissions.accessAdminControl + ) + ) { + throw new HttpException( + getReasonPhrase(StatusCodes.FORBIDDEN), + StatusCodes.FORBIDDEN + ); + } - return this.benchmarkService.getMarketDataBySymbol({ - dataSource, - startDate, - symbol - }); + try { + const benchmark = await this.benchmarkService.addBenchmark({ + dataSource, + symbol + }); + + if (!benchmark) { + throw new HttpException( + getReasonPhrase(StatusCodes.NOT_FOUND), + StatusCodes.NOT_FOUND + ); + } + + return benchmark; + } catch { + throw new HttpException( + getReasonPhrase(StatusCodes.INTERNAL_SERVER_ERROR), + StatusCodes.INTERNAL_SERVER_ERROR + ); + } } - @Post() + @Delete(':dataSource/:symbol') @UseGuards(AuthGuard('jwt')) - public async addBenchmark(@Body() { dataSource, symbol }: UniqueAsset) { + public async deleteBenchmark( + @Param('dataSource') dataSource: DataSource, + @Param('symbol') symbol: string + ) { if ( !hasPermission( this.request.user.permissions, @@ -74,7 +89,7 @@ export class BenchmarkController { } try { - const benchmark = await this.benchmarkService.addBenchmark({ + const benchmark = await this.benchmarkService.deleteBenchmark({ dataSource, symbol }); @@ -94,4 +109,30 @@ export class BenchmarkController { ); } } + + @Get() + @UseInterceptors(TransformDataSourceInRequestInterceptor) + @UseInterceptors(TransformDataSourceInResponseInterceptor) + public async getBenchmark(): Promise { + return { + benchmarks: await this.benchmarkService.getBenchmarks() + }; + } + + @Get(':dataSource/:symbol/:startDateString') + @UseGuards(AuthGuard('jwt')) + @UseInterceptors(TransformDataSourceInRequestInterceptor) + public async getBenchmarkMarketDataBySymbol( + @Param('dataSource') dataSource: DataSource, + @Param('startDateString') startDateString: string, + @Param('symbol') symbol: string + ): Promise { + const startDate = new Date(startDateString); + + return this.benchmarkService.getMarketDataBySymbol({ + dataSource, + startDate, + symbol + }); + } } diff --git a/apps/api/src/app/benchmark/benchmark.service.ts b/apps/api/src/app/benchmark/benchmark.service.ts index 785c2801a..7fe1911a4 100644 --- a/apps/api/src/app/benchmark/benchmark.service.ts +++ b/apps/api/src/app/benchmark/benchmark.service.ts @@ -245,6 +245,43 @@ export class BenchmarkService { }; } + public async deleteBenchmark({ + dataSource, + symbol + }: UniqueAsset): Promise> { + const assetProfile = await this.prismaService.symbolProfile.findFirst({ + where: { + dataSource, + symbol + } + }); + + if (!assetProfile) { + return null; + } + + let benchmarks = + ((await this.propertyService.getByKey( + PROPERTY_BENCHMARKS + )) as BenchmarkProperty[]) ?? []; + + benchmarks = benchmarks.filter(({ symbolProfileId }) => { + return symbolProfileId !== assetProfile.id; + }); + + await this.propertyService.put({ + key: PROPERTY_BENCHMARKS, + value: JSON.stringify(benchmarks) + }); + + return { + dataSource, + symbol, + id: assetProfile.id, + name: assetProfile.name + }; + } + private getMarketCondition(aPerformanceInPercent: number) { return aPerformanceInPercent <= -0.2 ? 'BEAR_MARKET' : 'NEUTRAL_MARKET'; } diff --git a/apps/api/src/app/order/order.controller.ts b/apps/api/src/app/order/order.controller.ts index be6a6bead..8c8e3e27a 100644 --- a/apps/api/src/app/order/order.controller.ts +++ b/apps/api/src/app/order/order.controller.ts @@ -89,7 +89,9 @@ export class OrderController { @Headers(HEADER_KEY_IMPERSONATION.toLowerCase()) impersonationId, @Query('accounts') filterByAccounts?: string, @Query('assetClasses') filterByAssetClasses?: string, - @Query('tags') filterByTags?: string + @Query('skip') skip?: number, + @Query('tags') filterByTags?: string, + @Query('take') take?: number ): Promise { const filters = this.apiService.buildFiltersFromQueryParams({ filterByAccounts, @@ -105,6 +107,8 @@ export class OrderController { filters, userCurrency, includeDrafts: true, + skip: isNaN(skip) ? undefined : skip, + take: isNaN(take) ? undefined : take, userId: impersonationUserId || this.request.user.id, withExcludedAccounts: true }); diff --git a/apps/api/src/app/order/order.service.ts b/apps/api/src/app/order/order.service.ts index f518e0bb3..10515018c 100644 --- a/apps/api/src/app/order/order.service.ts +++ b/apps/api/src/app/order/order.service.ts @@ -230,6 +230,8 @@ export class OrderService { public async getOrders({ filters, includeDrafts = false, + skip, + take = Number.MAX_SAFE_INTEGER, types, userCurrency, userId, @@ -237,6 +239,8 @@ export class OrderService { }: { filters?: Filter[]; includeDrafts?: boolean; + skip?: number; + take?: number; types?: TypeOfOrder[]; userCurrency: string; userId: string; @@ -315,6 +319,8 @@ export class OrderService { return ( await this.orders({ + skip, + take, where, include: { // eslint-disable-next-line @typescript-eslint/naming-convention diff --git a/apps/client/project.json b/apps/client/project.json index 9da89ea9e..2e36f7144 100644 --- a/apps/client/project.json +++ b/apps/client/project.json @@ -104,40 +104,40 @@ "options": { "commands": [ { - "command": "mkdir -p dist/apps/client" + "command": "shx mkdir -p dist/apps/client" }, { - "command": "cp -r apps/client/src/assets dist/apps/client" + "command": "shx cp -r apps/client/src/assets dist/apps/client" }, { - "command": "cp -r apps/client/src/assets/.well-known dist/apps/client" + "command": "shx cp -r apps/client/src/assets/.well-known dist/apps/client" }, { - "command": "cp apps/client/src/assets/favicon.ico dist/apps/client" + "command": "shx cp apps/client/src/assets/favicon.ico dist/apps/client" }, { - "command": "cp apps/client/src/assets/index.html dist/apps/client" + "command": "shx cp apps/client/src/assets/index.html dist/apps/client" }, { - "command": "cp apps/client/src/assets/robots.txt dist/apps/client" + "command": "shx cp apps/client/src/assets/robots.txt dist/apps/client" }, { - "command": "cp apps/client/src/assets/site.webmanifest dist/apps/client" + "command": "shx cp apps/client/src/assets/site.webmanifest dist/apps/client" }, { - "command": "cp node_modules/ionicons/dist/index.js dist/apps/client" + "command": "shx cp node_modules/ionicons/dist/index.js dist/apps/client" }, { - "command": "cp node_modules/ionicons/dist/ionicons.js dist/apps/client" + "command": "shx cp node_modules/ionicons/dist/ionicons.js dist/apps/client" }, { - "command": "cp -r node_modules/ionicons/dist/ionicons dist/apps/client/ionicons" + "command": "shx cp -r node_modules/ionicons/dist/ionicons dist/apps/client/ionicons" }, { - "command": "cp CHANGELOG.md dist/apps/client/assets" + "command": "shx cp CHANGELOG.md dist/apps/client/assets" }, { - "command": "cp LICENSE dist/apps/client/assets" + "command": "shx cp LICENSE dist/apps/client/assets" } ] } 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 be1892e91..bef984729 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 @@ -146,9 +146,11 @@ export class AssetProfileDialog implements OnDestroy, OnInit { .postBenchmark({ dataSource, symbol }) .pipe(takeUntil(this.unsubscribeSubject)) .subscribe(() => { - setTimeout(() => { - window.location.reload(); - }, 300); + this.dataService.updateInfo(); + + this.isBenchmark = true; + + this.changeDetectorRef.markForCheck(); }); } @@ -185,6 +187,19 @@ export class AssetProfileDialog implements OnDestroy, OnInit { }); } + public onUnsetBenchmark({ dataSource, symbol }: UniqueAsset) { + this.dataService + .deleteBenchmark({ dataSource, symbol }) + .pipe(takeUntil(this.unsubscribeSubject)) + .subscribe(() => { + this.dataService.updateInfo(); + + this.isBenchmark = false; + + this.changeDetectorRef.markForCheck(); + }); + } + 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 be99df7cb..6682d004d 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 @@ -37,13 +37,6 @@ > Gather Profile Data - @@ -151,6 +144,17 @@ +
+
+ Benchmark +
+
Symbol Mapping 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 8672342b0..1911f5a47 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 @@ -3,6 +3,7 @@ import { CommonModule } from '@angular/common'; import { CUSTOM_ELEMENTS_SCHEMA, NgModule } from '@angular/core'; import { FormsModule, ReactiveFormsModule } from '@angular/forms'; import { MatButtonModule } from '@angular/material/button'; +import { MatCheckboxModule } from '@angular/material/checkbox'; import { MatDialogModule } from '@angular/material/dialog'; import { MatInputModule } from '@angular/material/input'; import { MatMenuModule } from '@angular/material/menu'; @@ -21,6 +22,7 @@ import { AssetProfileDialog } from './asset-profile-dialog.component'; GfPortfolioProportionChartModule, GfValueModule, MatButtonModule, + MatCheckboxModule, MatDialogModule, MatInputModule, MatMenuModule, diff --git a/apps/client/src/app/components/admin-overview/admin-overview.component.ts b/apps/client/src/app/components/admin-overview/admin-overview.component.ts index 2053c4298..2ca3f0724 100644 --- a/apps/client/src/app/components/admin-overview/admin-overview.component.ts +++ b/apps/client/src/app/components/admin-overview/admin-overview.component.ts @@ -1,5 +1,6 @@ import { ChangeDetectorRef, Component, OnDestroy, OnInit } from '@angular/core'; import { MatCheckboxChange } from '@angular/material/checkbox'; +import { environment } from '@ghostfolio/client/../environments/environment'; import { AdminService } from '@ghostfolio/client/services/admin.service'; import { CacheService } from '@ghostfolio/client/services/cache.service'; import { DataService } from '@ghostfolio/client/services/data.service'; @@ -42,6 +43,7 @@ export class AdminOverviewComponent implements OnDestroy, OnInit { public transactionCount: number; public userCount: number; public user: User; + public version = environment.version; private unsubscribeSubject = new Subject(); diff --git a/apps/client/src/app/components/admin-overview/admin-overview.html b/apps/client/src/app/components/admin-overview/admin-overview.html index 6d8245cb7..2739547b5 100644 --- a/apps/client/src/app/components/admin-overview/admin-overview.html +++ b/apps/client/src/app/components/admin-overview/admin-overview.html @@ -3,6 +3,10 @@
+
+
Version
+
{{ version }}
+
User Count
diff --git a/apps/client/src/app/components/admin-platform/admin-platform.component.ts b/apps/client/src/app/components/admin-platform/admin-platform.component.ts index c8fce18ad..ffc5810b3 100644 --- a/apps/client/src/app/components/admin-platform/admin-platform.component.ts +++ b/apps/client/src/app/components/admin-platform/admin-platform.component.ts @@ -13,6 +13,7 @@ import { ActivatedRoute, Router } from '@angular/router'; import { CreatePlatformDto } from '@ghostfolio/api/app/platform/create-platform.dto'; import { UpdatePlatformDto } from '@ghostfolio/api/app/platform/update-platform.dto'; import { AdminService } from '@ghostfolio/client/services/admin.service'; +import { DataService } from '@ghostfolio/client/services/data.service'; import { UserService } from '@ghostfolio/client/services/user/user.service'; import { Platform } from '@prisma/client'; import { get } from 'lodash'; @@ -40,6 +41,7 @@ export class AdminPlatformComponent implements OnInit, OnDestroy { public constructor( private adminService: AdminService, private changeDetectorRef: ChangeDetectorRef, + private dataService: DataService, private deviceService: DeviceDetectorService, private dialog: MatDialog, private route: ActivatedRoute, @@ -119,6 +121,8 @@ export class AdminPlatformComponent implements OnInit, OnDestroy { this.dataSource.sort = this.sort; this.dataSource.sortingDataAccessor = get; + this.dataService.updateInfo(); + this.changeDetectorRef.markForCheck(); }); } diff --git a/apps/client/src/app/components/admin-tag/admin-tag.component.ts b/apps/client/src/app/components/admin-tag/admin-tag.component.ts index 48eb25554..e0dce2477 100644 --- a/apps/client/src/app/components/admin-tag/admin-tag.component.ts +++ b/apps/client/src/app/components/admin-tag/admin-tag.component.ts @@ -13,6 +13,7 @@ import { ActivatedRoute, Router } from '@angular/router'; import { CreateTagDto } from '@ghostfolio/api/app/tag/create-tag.dto'; import { UpdateTagDto } from '@ghostfolio/api/app/tag/update-tag.dto'; import { AdminService } from '@ghostfolio/client/services/admin.service'; +import { DataService } from '@ghostfolio/client/services/data.service'; import { UserService } from '@ghostfolio/client/services/user/user.service'; import { Tag } from '@prisma/client'; import { get } from 'lodash'; @@ -40,6 +41,7 @@ export class AdminTagComponent implements OnInit, OnDestroy { public constructor( private adminService: AdminService, private changeDetectorRef: ChangeDetectorRef, + private dataService: DataService, private deviceService: DeviceDetectorService, private dialog: MatDialog, private route: ActivatedRoute, @@ -114,10 +116,13 @@ export class AdminTagComponent implements OnInit, OnDestroy { .pipe(takeUntil(this.unsubscribeSubject)) .subscribe((tags) => { this.tags = tags; + this.dataSource = new MatTableDataSource(this.tags); this.dataSource.sort = this.sort; this.dataSource.sortingDataAccessor = get; + this.dataService.updateInfo(); + this.changeDetectorRef.markForCheck(); }); } diff --git a/apps/client/src/app/pages/user-account/user-account-page.component.ts b/apps/client/src/app/pages/user-account/user-account-page.component.ts index 970dadd6a..80c4a8f72 100644 --- a/apps/client/src/app/pages/user-account/user-account-page.component.ts +++ b/apps/client/src/app/pages/user-account/user-account-page.component.ts @@ -30,7 +30,7 @@ export class UserAccountPageComponent implements OnDestroy, OnInit { this.tabs = [ { - iconName: 'cog-outline', + iconName: 'settings-outline', label: $localize`Settings`, path: ['/account'] }, diff --git a/apps/client/src/app/services/data.service.ts b/apps/client/src/app/services/data.service.ts index 82b4acca0..5cc955af2 100644 --- a/apps/client/src/app/services/data.service.ts +++ b/apps/client/src/app/services/data.service.ts @@ -204,6 +204,10 @@ export class DataService { return this.http.delete(`/api/v1/order/`); } + public deleteBenchmark({ dataSource, symbol }: UniqueAsset) { + return this.http.delete(`/api/v1/benchmark/${dataSource}/${symbol}`); + } + public deleteOrder(aId: string) { return this.http.delete(`/api/v1/order/${aId}`); } @@ -496,4 +500,19 @@ export class DataService { couponCode }); } + + public updateInfo() { + this.http.get('/api/v1/info').subscribe((info) => { + const utmSource = <'ios' | 'trusted-web-activity'>( + window.localStorage.getItem('utm_source') + ); + + info.globalPermissions = filterGlobalPermissions( + info.globalPermissions, + utmSource + ); + + (window as any).info = info; + }); + } } diff --git a/package.json b/package.json index 56192852a..f303e5f51 100644 --- a/package.json +++ b/package.json @@ -194,6 +194,7 @@ "react": "18.2.0", "react-dom": "18.2.0", "replace-in-file": "7.0.1", + "shx": "0.3.4", "storybook": "7.0.9", "ts-jest": "29.1.0", "ts-node": "10.9.1", diff --git a/yarn.lock b/yarn.lock index 43b5deddd..69f995b5d 100644 --- a/yarn.lock +++ b/yarn.lock @@ -14264,7 +14264,7 @@ minimatch@^9.0.0, minimatch@^9.0.1: dependencies: brace-expansion "^2.0.1" -minimist@^1.2.0, minimist@^1.2.5, minimist@^1.2.6: +minimist@^1.2.0, minimist@^1.2.3, minimist@^1.2.5, minimist@^1.2.6: version "1.2.8" resolved "https://registry.yarnpkg.com/minimist/-/minimist-1.2.8.tgz#c1a464e7693302e082a075cee0c057741ac4772c" integrity sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA== @@ -16984,6 +16984,14 @@ shelljs@^0.8.5: interpret "^1.0.0" rechoir "^0.6.2" +shx@0.3.4: + version "0.3.4" + resolved "https://registry.yarnpkg.com/shx/-/shx-0.3.4.tgz#74289230b4b663979167f94e1935901406e40f02" + integrity sha512-N6A9MLVqjxZYcVn8hLmtneQWIJtp8IKzMP4eMnx+nqkvXoqinUPCbUFLp2UcWTEIUONhlk0ewxr/jaVGlc+J+g== + dependencies: + minimist "^1.2.3" + shelljs "^0.8.5" + side-channel@^1.0.4: version "1.0.4" resolved "https://registry.yarnpkg.com/side-channel/-/side-channel-1.0.4.tgz#efce5c8fdc104ee751b25c58d4290011fa5ea2cf"