diff --git a/CHANGELOG.md b/CHANGELOG.md index e27515571..6be164980 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,36 @@ All notable changes to this project will be documented in this file. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). +## 1.112.1 - 06.02.2022 + +### Fixed + +- Fixed the creation of the user account (missing access token) + +## 1.112.0 - 06.02.2022 + +### Added + +- Added the export functionality to the position detail dialog + +### Changed + +- Improved the export functionality for activities (respect filtering) +- Removed the _Admin_ user from the database seeding +- Assigned the role `ADMIN` on sign up (only if there is no admin yet) +- Upgraded `prisma` from version `3.8.1` to `3.9.1` + +### Fixed + +- Fixed an issue with the performance calculation in connection with a sell activity in the new calculation engine +- Fixed the horizontal overflow in the accounts table +- Fixed the horizontal overflow in the activities table +- Fixed the total value of the activities table in the position detail dialog (absolute value) + +### Todo + +- Apply data migration (`yarn database:migrate`) + ## 1.111.0 - 03.02.2022 ### Added diff --git a/README.md b/README.md index 260aca9be..64c8cfd3a 100644 --- a/README.md +++ b/README.md @@ -124,16 +124,10 @@ docker-compose -f docker/docker-compose.build.yml exec ghostfolio yarn database: Open http://localhost:3333 in your browser and accomplish these steps: -1. Login as _Admin_ with the following _Security Token_: `ae76872ae8f3419c6d6f64bf51888ecbcc703927a342d815fafe486acdb938da07d0cf44fca211a0be74a423238f535362d390a41e81e633a9ce668a6e31cdf9` +1. Create a new user via _Get Started_ (this first user will get the role `ADMIN`) 1. Go to the _Admin Control Panel_ and click _Gather All Data_ to fetch historical data 1. Click _Sign out_ and check out the _Live Demo_ -### Finalization - -1. Create a new user via _Get Started_ -1. Assign the role `ADMIN` to this user (directly in the database) -1. Delete the original _Admin_ (directly in the database) - ### Migrate Database With the following command you can keep your database schema in sync after a Ghostfolio version update: @@ -155,8 +149,8 @@ docker-compose -f docker/docker-compose-build-local.yml exec ghostfolio yarn dat 1. Run `yarn install` 1. Run `docker-compose -f docker/docker-compose.dev.yml up -d` to start [PostgreSQL](https://www.postgresql.org) and [Redis](https://redis.io) 1. Run `yarn database:setup` to initialize the database schema and populate your database with (example) data -1. Start server and client (see [_Development_](#Development)) -1. Login as _Admin_ with the following _Security Token_: `ae76872ae8f3419c6d6f64bf51888ecbcc703927a342d815fafe486acdb938da07d0cf44fca211a0be74a423238f535362d390a41e81e633a9ce668a6e31cdf9` +1. Start the server and the client (see [_Development_](#Development)) +1. Create a new user via _Get Started_ (this first user will get the role `ADMIN`) 1. Go to the _Admin Control Panel_ and click _Gather All Data_ to fetch historical data 1. Click _Sign out_ and check out the _Live Demo_ diff --git a/apps/api/src/app/export/export.controller.ts b/apps/api/src/app/export/export.controller.ts index ca318ce81..3617ebe24 100644 --- a/apps/api/src/app/export/export.controller.ts +++ b/apps/api/src/app/export/export.controller.ts @@ -1,6 +1,13 @@ import { Export } from '@ghostfolio/common/interfaces'; import type { RequestWithUser } from '@ghostfolio/common/types'; -import { Controller, Get, Inject, UseGuards } from '@nestjs/common'; +import { + Controller, + Get, + Headers, + Inject, + Query, + UseGuards +} from '@nestjs/common'; import { REQUEST } from '@nestjs/core'; import { AuthGuard } from '@nestjs/passport'; @@ -15,8 +22,11 @@ export class ExportController { @Get() @UseGuards(AuthGuard('jwt')) - public async export(): Promise { - return await this.exportService.export({ + public async export( + @Query('activityIds') activityIds?: string[] + ): Promise { + return this.exportService.export({ + activityIds, userId: this.request.user.id }); } diff --git a/apps/api/src/app/export/export.service.ts b/apps/api/src/app/export/export.service.ts index 30b1ed082..301f13cea 100644 --- a/apps/api/src/app/export/export.service.ts +++ b/apps/api/src/app/export/export.service.ts @@ -7,8 +7,14 @@ import { Injectable } from '@nestjs/common'; export class ExportService { public constructor(private readonly prismaService: PrismaService) {} - public async export({ userId }: { userId: string }): Promise { - const orders = await this.prismaService.order.findMany({ + public async export({ + activityIds, + userId + }: { + activityIds?: string[]; + userId: string; + }): Promise { + let orders = await this.prismaService.order.findMany({ orderBy: { date: 'desc' }, select: { accountId: true, @@ -16,6 +22,7 @@ export class ExportService { dataSource: true, date: true, fee: true, + id: true, quantity: true, SymbolProfile: true, type: true, @@ -24,6 +31,12 @@ export class ExportService { where: { userId } }); + if (activityIds) { + orders = orders.filter((order) => { + return activityIds.includes(order.id); + }); + } + return { meta: { date: new Date().toISOString(), version: environment.version }, orders: orders.map( diff --git a/apps/api/src/app/user/interfaces/user-item.interface.ts b/apps/api/src/app/user/interfaces/user-item.interface.ts index 338888c15..32230b69e 100644 --- a/apps/api/src/app/user/interfaces/user-item.interface.ts +++ b/apps/api/src/app/user/interfaces/user-item.interface.ts @@ -1,4 +1,7 @@ +import { Role } from '@prisma/client'; + export interface UserItem { accessToken?: string; authToken: string; + role: Role; } diff --git a/apps/api/src/app/user/user.controller.ts b/apps/api/src/app/user/user.controller.ts index adb059412..a615d2f2b 100644 --- a/apps/api/src/app/user/user.controller.ts +++ b/apps/api/src/app/user/user.controller.ts @@ -23,7 +23,7 @@ import { import { REQUEST } from '@nestjs/core'; import { JwtService } from '@nestjs/jwt'; import { AuthGuard } from '@nestjs/passport'; -import { Provider } from '@prisma/client'; +import { Provider, Role } from '@prisma/client'; import { User as UserModel } from '@prisma/client'; import { StatusCodes, getReasonPhrase } from 'http-status-codes'; @@ -83,12 +83,15 @@ export class UserController { } } - const { accessToken, id } = await this.userService.createUser({ - provider: Provider.ANONYMOUS + const hasAdmin = await this.userService.hasAdmin(); + + const { accessToken, id, role } = await this.userService.createUser({ + role: hasAdmin ? 'USER' : 'ADMIN' }); return { accessToken, + role, authToken: this.jwtService.sign({ id }) diff --git a/apps/api/src/app/user/user.service.ts b/apps/api/src/app/user/user.service.ts index 13a5b7d1d..5a21e9303 100644 --- a/apps/api/src/app/user/user.service.ts +++ b/apps/api/src/app/user/user.service.ts @@ -70,6 +70,18 @@ export class UserService { }; } + public async hasAdmin() { + const usersWithAdminRole = await this.users({ + where: { + role: { + equals: 'ADMIN' + } + } + }); + + return usersWithAdminRole.length > 0; + } + public isRestrictedView(aUser: UserWithSettings) { return (aUser.Settings.settings as UserSettings)?.isRestrictedView ?? false; } @@ -168,7 +180,11 @@ export class UserService { return hash.digest('hex'); } - public async createUser(data?: Prisma.UserCreateInput): Promise { + public async createUser(data: Prisma.UserCreateInput): Promise { + if (!data?.provider) { + data.provider = 'ANONYMOUS'; + } + let user = await this.prismaService.user.create({ data: { ...data, @@ -187,7 +203,7 @@ export class UserService { } }); - if (data.provider === Provider.ANONYMOUS) { + if (data.provider === 'ANONYMOUS') { const accessToken = this.createAccessToken( user.id, this.getRandomString(10) diff --git a/apps/client/src/app/components/accounts-table/accounts-table.component.ts b/apps/client/src/app/components/accounts-table/accounts-table.component.ts index ed0b9b298..eda979eaa 100644 --- a/apps/client/src/app/components/accounts-table/accounts-table.component.ts +++ b/apps/client/src/app/components/accounts-table/accounts-table.component.ts @@ -46,9 +46,9 @@ export class AccountsTableComponent implements OnChanges, OnDestroy, OnInit { public ngOnChanges() { this.displayedColumns = [ 'account', - 'currency', 'platform', 'transactions', + 'currency', 'balance', 'value' ]; diff --git a/apps/client/src/app/components/home-holdings/home-holdings.component.ts b/apps/client/src/app/components/home-holdings/home-holdings.component.ts index c7e964372..50c452ea8 100644 --- a/apps/client/src/app/components/home-holdings/home-holdings.component.ts +++ b/apps/client/src/app/components/home-holdings/home-holdings.component.ts @@ -3,6 +3,7 @@ import { MatDialog } from '@angular/material/dialog'; import { ActivatedRoute, Router } from '@angular/router'; import { PositionDetailDialog } from '@ghostfolio/client/components/position/position-detail-dialog/position-detail-dialog.component'; import { DataService } from '@ghostfolio/client/services/data.service'; +import { ImpersonationStorageService } from '@ghostfolio/client/services/impersonation-storage.service'; import { RANGE, SettingsStorageService @@ -26,6 +27,7 @@ export class HomeHoldingsComponent implements OnDestroy, OnInit { public dateRange: DateRange; public dateRangeOptions = defaultDateRangeOptions; public deviceType: string; + public hasImpersonationId: boolean; public hasPermissionToCreateOrder: boolean; public positions: Position[]; public user: User; @@ -40,6 +42,7 @@ export class HomeHoldingsComponent implements OnDestroy, OnInit { private dataService: DataService, private deviceService: DeviceDetectorService, private dialog: MatDialog, + private impersonationStorageService: ImpersonationStorageService, private route: ActivatedRoute, private router: Router, private settingsStorageService: SettingsStorageService, @@ -82,6 +85,13 @@ export class HomeHoldingsComponent implements OnDestroy, OnInit { public ngOnInit() { this.deviceType = this.deviceService.getDeviceInfo().deviceType; + this.impersonationStorageService + .onChangeHasImpersonation() + .pipe(takeUntil(this.unsubscribeSubject)) + .subscribe((aId) => { + this.hasImpersonationId = !!aId; + }); + this.dateRange = this.settingsStorageService.getSetting(RANGE) || 'max'; @@ -119,6 +129,7 @@ export class HomeHoldingsComponent implements OnDestroy, OnInit { symbol, baseCurrency: this.user?.settings?.baseCurrency, deviceType: this.deviceType, + hasImpersonationId: this.hasImpersonationId, locale: this.user?.settings?.locale }, height: this.deviceType === 'mobile' ? '97.5vh' : '80vh', diff --git a/apps/client/src/app/components/position/position-detail-dialog/interfaces/interfaces.ts b/apps/client/src/app/components/position/position-detail-dialog/interfaces/interfaces.ts index daac4065a..791c2b46a 100644 --- a/apps/client/src/app/components/position/position-detail-dialog/interfaces/interfaces.ts +++ b/apps/client/src/app/components/position/position-detail-dialog/interfaces/interfaces.ts @@ -4,6 +4,7 @@ export interface PositionDetailDialogParams { baseCurrency: string; dataSource: DataSource; deviceType: string; + hasImpersonationId: boolean; locale: string; symbol: string; } diff --git a/apps/client/src/app/components/position/position-detail-dialog/position-detail-dialog.component.ts b/apps/client/src/app/components/position/position-detail-dialog/position-detail-dialog.component.ts index 1802619a0..02563afba 100644 --- a/apps/client/src/app/components/position/position-detail-dialog/position-detail-dialog.component.ts +++ b/apps/client/src/app/components/position/position-detail-dialog/position-detail-dialog.component.ts @@ -8,7 +8,7 @@ import { } from '@angular/core'; import { MAT_DIALOG_DATA, MatDialogRef } from '@angular/material/dialog'; import { DataService } from '@ghostfolio/client/services/data.service'; -import { DATE_FORMAT } from '@ghostfolio/common/helper'; +import { DATE_FORMAT, downloadAsFile } from '@ghostfolio/common/helper'; import { OrderWithAccount } from '@ghostfolio/common/types'; import { LineChartItem } from '@ghostfolio/ui/line-chart/interfaces/line-chart.interface'; import { AssetSubClass } from '@prisma/client'; @@ -185,6 +185,26 @@ export class PositionDetailDialog implements OnDestroy, OnInit { this.dialogRef.close(); } + public onExport() { + this.dataService + .fetchExport( + this.orders.map((order) => { + return order.id; + }) + ) + .pipe(takeUntil(this.unsubscribeSubject)) + .subscribe((data) => { + downloadAsFile( + data, + `ghostfolio-export-${this.symbol}-${format( + parseISO(data.meta.date), + 'yyyyMMddHHmm' + )}.json`, + 'text/plain' + ); + }); + } + public ngOnDestroy() { this.unsubscribeSubject.next(); this.unsubscribeSubject.complete(); diff --git a/apps/client/src/app/components/position/position-detail-dialog/position-detail-dialog.html b/apps/client/src/app/components/position/position-detail-dialog/position-detail-dialog.html index 00d949263..db8f78bc3 100644 --- a/apps/client/src/app/components/position/position-detail-dialog/position-detail-dialog.html +++ b/apps/client/src/app/components/position/position-detail-dialog/position-detail-dialog.html @@ -131,12 +131,14 @@ [baseCurrency]="data.baseCurrency" [deviceType]="data.deviceType" [hasPermissionToCreateActivity]="false" + [hasPermissionToExportActivities]="!hasImpersonationId" [hasPermissionToFilter]="false" [hasPermissionToImportActivities]="false" [hasPermissionToOpenDetails]="false" [locale]="data.locale" [showActions]="false" [showSymbolColumn]="false" + (export)="onExport()" > diff --git a/apps/client/src/app/pages/about/about-page.component.ts b/apps/client/src/app/pages/about/about-page.component.ts index 2abc6ec8a..ab4fdb57e 100644 --- a/apps/client/src/app/pages/about/about-page.component.ts +++ b/apps/client/src/app/pages/about/about-page.component.ts @@ -11,7 +11,7 @@ import { takeUntil } from 'rxjs/operators'; import { environment } from '../../../environments/environment'; @Component({ - host: { class: 'mb-5' }, + host: { class: 'page' }, selector: 'gf-about-page', styleUrls: ['./about-page.scss'], templateUrl: './about-page.html' diff --git a/apps/client/src/app/pages/about/changelog/changelog-page.component.ts b/apps/client/src/app/pages/about/changelog/changelog-page.component.ts index a9f383b87..74f39c9a6 100644 --- a/apps/client/src/app/pages/about/changelog/changelog-page.component.ts +++ b/apps/client/src/app/pages/about/changelog/changelog-page.component.ts @@ -2,7 +2,7 @@ import { Component, OnDestroy } from '@angular/core'; import { Subject } from 'rxjs'; @Component({ - host: { class: 'mb-5' }, + host: { class: 'page' }, selector: 'gf-changelog-page', styleUrls: ['./changelog-page.scss'], templateUrl: './changelog-page.html' diff --git a/apps/client/src/app/pages/account/account-page.component.ts b/apps/client/src/app/pages/account/account-page.component.ts index df7f81beb..7aed7479d 100644 --- a/apps/client/src/app/pages/account/account-page.component.ts +++ b/apps/client/src/app/pages/account/account-page.component.ts @@ -31,7 +31,7 @@ import { catchError, switchMap, takeUntil } from 'rxjs/operators'; import { CreateOrUpdateAccessDialog } from './create-or-update-access-dialog/create-or-update-access-dialog.component'; @Component({ - host: { class: 'mb-5' }, + host: { class: 'page' }, selector: 'gf-account-page', styleUrls: ['./account-page.scss'], templateUrl: './account-page.html' diff --git a/apps/client/src/app/pages/accounts/accounts-page.component.ts b/apps/client/src/app/pages/accounts/accounts-page.component.ts index 53191835c..8c3e145aa 100644 --- a/apps/client/src/app/pages/accounts/accounts-page.component.ts +++ b/apps/client/src/app/pages/accounts/accounts-page.component.ts @@ -16,7 +16,7 @@ import { takeUntil } from 'rxjs/operators'; import { CreateOrUpdateAccountDialog } from './create-or-update-account-dialog/create-or-update-account-dialog.component'; @Component({ - host: { class: 'mb-5' }, + host: { class: 'page' }, selector: 'gf-accounts-page', styleUrls: ['./accounts-page.scss'], templateUrl: './accounts-page.html' diff --git a/apps/client/src/app/pages/accounts/accounts-page.html b/apps/client/src/app/pages/accounts/accounts-page.html index 2afbfc959..117a1f5d5 100644 --- a/apps/client/src/app/pages/accounts/accounts-page.html +++ b/apps/client/src/app/pages/accounts/accounts-page.html @@ -1,19 +1,21 @@
-
+

Accounts

- +
+ +
diff --git a/apps/client/src/app/pages/accounts/accounts-page.scss b/apps/client/src/app/pages/accounts/accounts-page.scss index c10640da3..307bf0f32 100644 --- a/apps/client/src/app/pages/accounts/accounts-page.scss +++ b/apps/client/src/app/pages/accounts/accounts-page.scss @@ -1,6 +1,10 @@ :host { display: block; + .accounts { + overflow-x: auto; + } + .fab-container { position: fixed; right: 2rem; diff --git a/apps/client/src/app/pages/blog/2021/07/hallo-ghostfolio/hallo-ghostfolio-page.component.ts b/apps/client/src/app/pages/blog/2021/07/hallo-ghostfolio/hallo-ghostfolio-page.component.ts index f5282bc2c..04b4f79f1 100644 --- a/apps/client/src/app/pages/blog/2021/07/hallo-ghostfolio/hallo-ghostfolio-page.component.ts +++ b/apps/client/src/app/pages/blog/2021/07/hallo-ghostfolio/hallo-ghostfolio-page.component.ts @@ -1,7 +1,7 @@ import { Component } from '@angular/core'; @Component({ - host: { class: 'mb-5' }, + host: { class: 'page' }, selector: 'gf-hallo-ghostfolio-page', styleUrls: ['./hallo-ghostfolio-page.scss'], templateUrl: './hallo-ghostfolio-page.html' diff --git a/apps/client/src/app/pages/blog/2021/07/hello-ghostfolio/hello-ghostfolio-page.component.ts b/apps/client/src/app/pages/blog/2021/07/hello-ghostfolio/hello-ghostfolio-page.component.ts index 1a5cd413a..e9a0a7382 100644 --- a/apps/client/src/app/pages/blog/2021/07/hello-ghostfolio/hello-ghostfolio-page.component.ts +++ b/apps/client/src/app/pages/blog/2021/07/hello-ghostfolio/hello-ghostfolio-page.component.ts @@ -1,7 +1,7 @@ import { Component } from '@angular/core'; @Component({ - host: { class: 'mb-5' }, + host: { class: 'page' }, selector: 'gf-hello-ghostfolio-page', styleUrls: ['./hello-ghostfolio-page.scss'], templateUrl: './hello-ghostfolio-page.html' diff --git a/apps/client/src/app/pages/blog/2022/01/first-months-in-open-source/first-months-in-open-source-page.component.ts b/apps/client/src/app/pages/blog/2022/01/first-months-in-open-source/first-months-in-open-source-page.component.ts index 84e5aae45..1fe69b79c 100644 --- a/apps/client/src/app/pages/blog/2022/01/first-months-in-open-source/first-months-in-open-source-page.component.ts +++ b/apps/client/src/app/pages/blog/2022/01/first-months-in-open-source/first-months-in-open-source-page.component.ts @@ -1,7 +1,7 @@ import { Component } from '@angular/core'; @Component({ - host: { class: 'mb-5' }, + host: { class: 'page' }, selector: 'gf-first-months-in-open-source-page', styleUrls: ['./first-months-in-open-source-page.scss'], templateUrl: './first-months-in-open-source-page.html' diff --git a/apps/client/src/app/pages/blog/blog-page.component.ts b/apps/client/src/app/pages/blog/blog-page.component.ts index c07c91ee6..c9176625f 100644 --- a/apps/client/src/app/pages/blog/blog-page.component.ts +++ b/apps/client/src/app/pages/blog/blog-page.component.ts @@ -2,7 +2,7 @@ import { Component, OnDestroy } from '@angular/core'; import { Subject } from 'rxjs'; @Component({ - host: { class: 'mb-5' }, + host: { class: 'page' }, selector: 'gf-blog-page', styleUrls: ['./blog-page.scss'], templateUrl: './blog-page.html' diff --git a/apps/client/src/app/pages/landing/landing-page.component.ts b/apps/client/src/app/pages/landing/landing-page.component.ts index ed7ca9b7d..9e853c183 100644 --- a/apps/client/src/app/pages/landing/landing-page.component.ts +++ b/apps/client/src/app/pages/landing/landing-page.component.ts @@ -6,7 +6,7 @@ import { format } from 'date-fns'; import { Subject } from 'rxjs'; @Component({ - host: { class: 'mb-5' }, + host: { class: 'page' }, selector: 'gf-landing-page', styleUrls: ['./landing-page.scss'], templateUrl: './landing-page.html' diff --git a/apps/client/src/app/pages/portfolio/allocations/allocations-page.component.ts b/apps/client/src/app/pages/portfolio/allocations/allocations-page.component.ts index f89297403..f170a541e 100644 --- a/apps/client/src/app/pages/portfolio/allocations/allocations-page.component.ts +++ b/apps/client/src/app/pages/portfolio/allocations/allocations-page.component.ts @@ -20,7 +20,7 @@ import { Subject, Subscription } from 'rxjs'; import { takeUntil } from 'rxjs/operators'; @Component({ - host: { class: 'mb-5' }, + host: { class: 'page' }, selector: 'gf-allocations-page', styleUrls: ['./allocations-page.scss'], templateUrl: './allocations-page.html' @@ -316,6 +316,7 @@ export class AllocationsPageComponent implements OnDestroy, OnInit { symbol, baseCurrency: this.user?.settings?.baseCurrency, deviceType: this.deviceType, + hasImpersonationId: this.hasImpersonationId, locale: this.user?.settings?.locale }, height: this.deviceType === 'mobile' ? '97.5vh' : '80vh', diff --git a/apps/client/src/app/pages/portfolio/analysis/analysis-page.component.ts b/apps/client/src/app/pages/portfolio/analysis/analysis-page.component.ts index 9cf798a18..8f771e71b 100644 --- a/apps/client/src/app/pages/portfolio/analysis/analysis-page.component.ts +++ b/apps/client/src/app/pages/portfolio/analysis/analysis-page.component.ts @@ -11,7 +11,7 @@ import { Subject } from 'rxjs'; import { takeUntil } from 'rxjs/operators'; @Component({ - host: { class: 'mb-5' }, + host: { class: 'page' }, selector: 'gf-analysis-page', styleUrls: ['./analysis-page.scss'], templateUrl: './analysis-page.html' diff --git a/apps/client/src/app/pages/portfolio/portfolio-page.component.ts b/apps/client/src/app/pages/portfolio/portfolio-page.component.ts index a29472190..cacdc93a8 100644 --- a/apps/client/src/app/pages/portfolio/portfolio-page.component.ts +++ b/apps/client/src/app/pages/portfolio/portfolio-page.component.ts @@ -7,7 +7,7 @@ import { Subject } from 'rxjs'; import { takeUntil } from 'rxjs/operators'; @Component({ - host: { class: 'mb-5' }, + host: { class: 'page' }, selector: 'gf-portfolio-page', styleUrls: ['./portfolio-page.scss'], templateUrl: './portfolio-page.html' diff --git a/apps/client/src/app/pages/portfolio/report/report-page.component.ts b/apps/client/src/app/pages/portfolio/report/report-page.component.ts index 057923d09..17bca2bcb 100644 --- a/apps/client/src/app/pages/portfolio/report/report-page.component.ts +++ b/apps/client/src/app/pages/portfolio/report/report-page.component.ts @@ -7,7 +7,7 @@ import { Subject } from 'rxjs'; import { takeUntil } from 'rxjs/operators'; @Component({ - host: { class: 'mb-5' }, + host: { class: 'page' }, selector: 'gf-report-page', styleUrls: ['./report-page.scss'], templateUrl: './report-page.html' diff --git a/apps/client/src/app/pages/portfolio/transactions/transactions-page.component.ts b/apps/client/src/app/pages/portfolio/transactions/transactions-page.component.ts index 8172ed080..5d165c40f 100644 --- a/apps/client/src/app/pages/portfolio/transactions/transactions-page.component.ts +++ b/apps/client/src/app/pages/portfolio/transactions/transactions-page.component.ts @@ -10,6 +10,7 @@ import { DataService } from '@ghostfolio/client/services/data.service'; import { ImpersonationStorageService } from '@ghostfolio/client/services/impersonation-storage.service'; import { ImportTransactionsService } from '@ghostfolio/client/services/import-transactions.service'; import { UserService } from '@ghostfolio/client/services/user/user.service'; +import { downloadAsFile } from '@ghostfolio/common/helper'; import { User } from '@ghostfolio/common/interfaces'; import { hasPermission, permissions } from '@ghostfolio/common/permissions'; import { DataSource, Order as OrderModel } from '@prisma/client'; @@ -23,7 +24,7 @@ import { CreateOrUpdateTransactionDialog } from './create-or-update-transaction- import { ImportTransactionDialog } from './import-transaction-dialog/import-transaction-dialog.component'; @Component({ - host: { class: 'mb-5' }, + host: { class: 'page' }, selector: 'gf-transactions-page', styleUrls: ['./transactions-page.scss'], templateUrl: './transactions-page.html' @@ -90,11 +91,6 @@ export class TransactionsPageComponent implements OnDestroy, OnInit { public ngOnInit() { const { globalPermissions } = this.dataService.fetchInfo(); - this.hasPermissionToImportOrders = hasPermission( - globalPermissions, - permissions.enableImport - ); - this.deviceType = this.deviceService.getDeviceInfo().deviceType; this.impersonationStorageService @@ -102,6 +98,10 @@ export class TransactionsPageComponent implements OnDestroy, OnInit { .pipe(takeUntil(this.unsubscribeSubject)) .subscribe((aId) => { this.hasImpersonationId = !!aId; + + this.hasPermissionToImportOrders = + hasPermission(globalPermissions, permissions.enableImport) && + !this.hasImpersonationId; }); this.userService.stateChanged @@ -147,12 +147,12 @@ export class TransactionsPageComponent implements OnDestroy, OnInit { }); } - public onExport() { + public onExport(activityIds?: string[]) { this.dataService - .fetchExport() + .fetchExport(activityIds) .pipe(takeUntil(this.unsubscribeSubject)) .subscribe((data) => { - this.downloadAsFile( + downloadAsFile( data, `ghostfolio-export-${format( parseISO(data.meta.date), @@ -303,20 +303,6 @@ export class TransactionsPageComponent implements OnDestroy, OnInit { this.unsubscribeSubject.complete(); } - private downloadAsFile( - aContent: unknown, - aFileName: string, - aContentType: string - ) { - const a = document.createElement('a'); - const file = new Blob([JSON.stringify(aContent, undefined, ' ')], { - type: aContentType - }); - a.href = URL.createObjectURL(file); - a.download = aFileName; - a.click(); - } - private handleImportError({ error, orders }: { error: any; orders: any[] }) { this.snackBar.dismiss(); @@ -406,6 +392,7 @@ export class TransactionsPageComponent implements OnDestroy, OnInit { symbol, baseCurrency: this.user?.settings?.baseCurrency, deviceType: this.deviceType, + hasImpersonationId: this.hasImpersonationId, locale: this.user?.settings?.locale }, height: this.deviceType === 'mobile' ? '97.5vh' : '80vh', diff --git a/apps/client/src/app/pages/portfolio/transactions/transactions-page.html b/apps/client/src/app/pages/portfolio/transactions/transactions-page.html index db365c2d9..0df0171b9 100644 --- a/apps/client/src/app/pages/portfolio/transactions/transactions-page.html +++ b/apps/client/src/app/pages/portfolio/transactions/transactions-page.html @@ -7,13 +7,14 @@ [baseCurrency]="user?.settings?.baseCurrency" [deviceType]="deviceType" [hasPermissionToCreateActivity]="hasPermissionToCreateOrder" + [hasPermissionToExportActivities]="!hasImpersonationId" [hasPermissionToImportActivities]="hasPermissionToImportOrders" [locale]="user?.settings?.locale" [showActions]="!hasImpersonationId && hasPermissionToDeleteOrder && !user.settings.isRestrictedView" (activityDeleted)="onDeleteTransaction($event)" (activityToClone)="onCloneTransaction($event)" (activityToUpdate)="onUpdateTransaction($event)" - (export)="onExport()" + (export)="onExport($event)" (import)="onImport()" >
diff --git a/apps/client/src/app/pages/pricing/pricing-page.component.ts b/apps/client/src/app/pages/pricing/pricing-page.component.ts index 1124b674b..eee7be440 100644 --- a/apps/client/src/app/pages/pricing/pricing-page.component.ts +++ b/apps/client/src/app/pages/pricing/pricing-page.component.ts @@ -7,7 +7,7 @@ import { Subject } from 'rxjs'; import { takeUntil } from 'rxjs/operators'; @Component({ - host: { class: 'mb-5' }, + host: { class: 'page' }, selector: 'gf-pricing-page', styleUrls: ['./pricing-page.scss'], templateUrl: './pricing-page.html' diff --git a/apps/client/src/app/pages/public/public-page.component.ts b/apps/client/src/app/pages/public/public-page.component.ts index 3a9ad920f..a3ac43721 100644 --- a/apps/client/src/app/pages/public/public-page.component.ts +++ b/apps/client/src/app/pages/public/public-page.component.ts @@ -13,7 +13,7 @@ import { EMPTY, Subject } from 'rxjs'; import { catchError, takeUntil } from 'rxjs/operators'; @Component({ - host: { class: 'mb-5' }, + host: { class: 'page' }, selector: 'gf-public-page', styleUrls: ['./public-page.scss'], templateUrl: './public-page.html' diff --git a/apps/client/src/app/pages/register/register-page.component.ts b/apps/client/src/app/pages/register/register-page.component.ts index f7da483d1..70cb17bdb 100644 --- a/apps/client/src/app/pages/register/register-page.component.ts +++ b/apps/client/src/app/pages/register/register-page.component.ts @@ -6,6 +6,7 @@ import { TokenStorageService } from '@ghostfolio/client/services/token-storage.s import { InfoItem } from '@ghostfolio/common/interfaces'; import { hasPermission, permissions } from '@ghostfolio/common/permissions'; import { LineChartItem } from '@ghostfolio/ui/line-chart/interfaces/line-chart.interface'; +import { Role } from '@prisma/client'; import { format } from 'date-fns'; import { DeviceDetectorService } from 'ngx-device-detector'; import { Subject } from 'rxjs'; @@ -14,7 +15,7 @@ import { takeUntil } from 'rxjs/operators'; import { ShowAccessTokenDialog } from './show-access-token-dialog/show-access-token-dialog.component'; @Component({ - host: { class: 'mb-5' }, + host: { class: 'page' }, selector: 'gf-register-page', styleUrls: ['./register-page.scss'], templateUrl: './register-page.html' @@ -62,19 +63,21 @@ export class RegisterPageComponent implements OnDestroy, OnInit { this.dataService .postUser() .pipe(takeUntil(this.unsubscribeSubject)) - .subscribe(({ accessToken, authToken }) => { - this.openShowAccessTokenDialog(accessToken, authToken); + .subscribe(({ accessToken, authToken, role }) => { + this.openShowAccessTokenDialog(accessToken, authToken, role); }); } public openShowAccessTokenDialog( accessToken: string, - authToken: string + authToken: string, + role: Role ): void { const dialogRef = this.dialog.open(ShowAccessTokenDialog, { data: { accessToken, - authToken + authToken, + role }, disableClose: true, width: '30rem' diff --git a/apps/client/src/app/pages/register/show-access-token-dialog/show-access-token-dialog.html b/apps/client/src/app/pages/register/show-access-token-dialog/show-access-token-dialog.html index c47e2740c..a742bdbf6 100644 --- a/apps/client/src/app/pages/register/show-access-token-dialog/show-access-token-dialog.html +++ b/apps/client/src/app/pages/register/show-access-token-dialog/show-access-token-dialog.html @@ -1,4 +1,9 @@ -

Create Account

+

+ Create Account{{ data.role }} +

diff --git a/apps/client/src/app/pages/resources/resources-page.component.ts b/apps/client/src/app/pages/resources/resources-page.component.ts index 71bc3e314..a771e3ae3 100644 --- a/apps/client/src/app/pages/resources/resources-page.component.ts +++ b/apps/client/src/app/pages/resources/resources-page.component.ts @@ -2,7 +2,7 @@ import { Component, OnInit } from '@angular/core'; import { Subject } from 'rxjs'; @Component({ - host: { class: 'mb-5' }, + host: { class: 'page' }, selector: 'gf-resources-page', styleUrls: ['./resources-page.scss'], templateUrl: './resources-page.html' diff --git a/apps/client/src/app/pages/webauthn/webauthn-page.component.ts b/apps/client/src/app/pages/webauthn/webauthn-page.component.ts index 3a63eed50..de3b29d61 100644 --- a/apps/client/src/app/pages/webauthn/webauthn-page.component.ts +++ b/apps/client/src/app/pages/webauthn/webauthn-page.component.ts @@ -6,7 +6,7 @@ import { Subject } from 'rxjs'; import { takeUntil } from 'rxjs/operators'; @Component({ - host: { class: 'mb-5' }, + host: { class: 'page' }, selector: 'gf-webauthn-page', styleUrls: ['./webauthn-page.scss'], templateUrl: './webauthn-page.html' diff --git a/apps/client/src/app/services/data.service.ts b/apps/client/src/app/services/data.service.ts index bf03b3f5f..fac56a1f2 100644 --- a/apps/client/src/app/services/data.service.ts +++ b/apps/client/src/app/services/data.service.ts @@ -94,8 +94,16 @@ export class DataService { }); } - public fetchExport() { - return this.http.get('/api/export'); + public fetchExport(activityIds?: string[]) { + let params = new HttpParams(); + + if (activityIds) { + params = params.append('activityIds', activityIds.join(',')); + } + + return this.http.get('/api/export', { + params + }); } public fetchInfo(): InfoItem { diff --git a/apps/client/src/styles.scss b/apps/client/src/styles.scss index eb162f9fa..12f0f3a2b 100644 --- a/apps/client/src/styles.scss +++ b/apps/client/src/styles.scss @@ -164,6 +164,10 @@ ngx-skeleton-loader { min-width: unset !important; } +.page { + padding-bottom: 5rem; +} + .svgMap-tooltip { border-bottom: none; diff --git a/libs/common/src/lib/helper.ts b/libs/common/src/lib/helper.ts index 4fac26654..dbfc787f3 100644 --- a/libs/common/src/lib/helper.ts +++ b/libs/common/src/lib/helper.ts @@ -12,6 +12,20 @@ export function decodeDataSource(encodedDataSource: string) { return Buffer.from(encodedDataSource, 'hex').toString(); } +export function downloadAsFile( + aContent: unknown, + aFileName: string, + aContentType: string +) { + const a = document.createElement('a'); + const file = new Blob([JSON.stringify(aContent, undefined, ' ')], { + type: aContentType + }); + a.href = URL.createObjectURL(file); + a.download = aFileName; + a.click(); +} + export function encodeDataSource(aDataSource: DataSource) { return Buffer.from(aDataSource, 'utf-8').toString('hex'); } diff --git a/libs/ui/src/lib/activities-table/activities-table.component.html b/libs/ui/src/lib/activities-table/activities-table.component.html index 5af6013bb..5fd343a0a 100644 --- a/libs/ui/src/lib/activities-table/activities-table.component.html +++ b/libs/ui/src/lib/activities-table/activities-table.component.html @@ -36,309 +36,335 @@ - - - - - - - - - - - - - - - + + +
- {{ dataSource.data.length - i }} - - Date - -
- {{ element.date | date: defaultDateFormat }} -
-
Total - Type - -
+ + + + - - + {{ dataSource.data.length - i }} + + + + + + + + - - - + - - - - - - - - + + {{ element.type }} + + + + - - - - - + + + + + - - - - - + + + + + - - - - - + + + + + - - - - - + + + + + - - - - - + + + + + - - + + + + + + + + + + + + + - - + + + + + + + + - - - -
- - {{ element.type }} - - + Date + +
+ {{ element.date | date: defaultDateFormat }} +
+
Total - Symbol - -
- {{ element.symbol | gfSymbol }} - Draft +
+ Type + +
-
-
- Currency - - {{ element.currency }} - - {{ baseCurrency }} - - Quantity - -
- -
-
+ Symbol + +
+ {{ element.symbol | gfSymbol }} + Draft +
+
- Unit Price - -
- -
-
+ Currency + + {{ element.currency }} + + {{ baseCurrency }} + - Fee - -
- -
-
-
- -
-
+ Quantity + +
+ +
+
- Value - -
- -
-
-
- -
-
+ Unit Price + +
+ +
+
- Account - -
- - {{ element.Account?.name }} -
-
+ Fee + +
+ +
+
+
+ +
+
- - - - + Value + +
+ +
+
+
+ +
+
+ Account + +
+ + {{ element.Account?.name }} +
+
+ + + + + - - - - - - - - - -
+
+
(); @Output() activityToClone = new EventEmitter(); @Output() activityToUpdate = new EventEmitter(); - @Output() export = new EventEmitter(); + @Output() export = new EventEmitter(); @Output() import = new EventEmitter(); @ViewChild('autocomplete') matAutocomplete: MatAutocomplete; @@ -132,18 +133,15 @@ export class ActivitiesTableComponent implements OnChanges, OnDestroy { 'date', 'type', 'symbol', - 'currency', 'quantity', + 'currency', 'unitPrice', 'fee', 'value', - 'account' + 'account', + 'actions' ]; - if (this.showActions) { - this.displayedColumns.push('actions'); - } - if (!this.showSymbolColumn) { this.displayedColumns = this.displayedColumns.filter((column) => { return column !== 'symbol'; @@ -184,7 +182,15 @@ export class ActivitiesTableComponent implements OnChanges, OnDestroy { } public onExport() { - this.export.emit(); + if (this.searchKeywords.length > 0) { + this.export.emit( + this.dataSource.filteredData.map((activity) => { + return activity.id; + }) + ); + } else { + this.export.emit(); + } } public onImport() { diff --git a/libs/ui/src/lib/value/value.component.ts b/libs/ui/src/lib/value/value.component.ts index 9c805e859..07fc2b136 100644 --- a/libs/ui/src/lib/value/value.component.ts +++ b/libs/ui/src/lib/value/value.component.ts @@ -17,6 +17,7 @@ import { isNumber } from 'lodash'; export class ValueComponent implements OnChanges { @Input() colorizeSign = false; @Input() currency = ''; + @Input() isAbsolute = false; @Input() isCurrency = false; @Input() isPercent = false; @Input() label = ''; @@ -91,6 +92,11 @@ export class ValueComponent implements OnChanges { } else { this.formattedValue = this.value?.toString(); } + + if (this.isAbsolute) { + // Remove algebraic sign + this.formattedValue = this.formattedValue.replace(/^-/, ''); + } } else { try { if (isDate(new Date(this.value))) { diff --git a/package.json b/package.json index 033097f9b..c07f2f875 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "ghostfolio", - "version": "1.111.0", + "version": "1.112.1", "homepage": "https://ghostfol.io", "license": "AGPL-3.0", "scripts": { @@ -70,7 +70,7 @@ "@nestjs/schedule": "1.0.2", "@nestjs/serve-static": "2.2.2", "@nrwl/angular": "13.4.1", - "@prisma/client": "3.8.1", + "@prisma/client": "3.9.1", "@simplewebauthn/browser": "4.1.0", "@simplewebauthn/server": "4.1.0", "@simplewebauthn/typescript-types": "4.0.0", @@ -107,7 +107,7 @@ "passport": "0.4.1", "passport-google-oauth20": "2.0.0", "passport-jwt": "4.0.0", - "prisma": "3.8.1", + "prisma": "3.9.1", "reflect-metadata": "0.1.13", "round-to": "5.0.0", "rxjs": "7.4.0", diff --git a/prisma/migrations/20220205195653_added_default_value_for_provider_in_user/migration.sql b/prisma/migrations/20220205195653_added_default_value_for_provider_in_user/migration.sql new file mode 100644 index 000000000..eae68b571 --- /dev/null +++ b/prisma/migrations/20220205195653_added_default_value_for_provider_in_user/migration.sql @@ -0,0 +1,6 @@ +-- Set default value +UPDATE "User" SET "provider" = 'ANONYMOUS' WHERE "provider" IS NULL; + +-- AlterTable +ALTER TABLE "User" ALTER COLUMN "provider" SET NOT NULL, +ALTER COLUMN "provider" SET DEFAULT E'ANONYMOUS'; diff --git a/prisma/schema.prisma b/prisma/schema.prisma index 2625c0fcd..53467d7b7 100644 --- a/prisma/schema.prisma +++ b/prisma/schema.prisma @@ -156,7 +156,7 @@ model User { createdAt DateTime @default(now()) id String @id @default(uuid()) Order Order[] - provider Provider? + provider Provider @default(ANONYMOUS) role Role @default(USER) Settings Settings? Subscription Subscription[] diff --git a/prisma/seed.js b/prisma/seed.js index b355a22f0..3e4996a1e 100644 --- a/prisma/seed.js +++ b/prisma/seed.js @@ -78,30 +78,6 @@ async function main() { where: { id: '1377d9df-0d25-42c2-9d9b-e4c63156291f' } }); - const userAdmin = await prisma.user.upsert({ - create: { - accessToken: - 'c689bcc894e4a420cb609ee34271f3e07f200594f7d199c50d75add7102889eb60061a04cd2792ebc853c54e37308271271e7bf588657c9e0c37faacbc28c3c6', - Account: { - create: [ - { - accountType: AccountType.SECURITIES, - balance: 0, - currency: 'USD', - id: 'f4425b66-9ba9-4ac4-93d7-fdf9a145e8cb', - isDefault: true, - name: 'Default Account' - } - ] - }, - alias: 'Admin', - id: '4e1af723-95f6-44f8-92a7-464df17f6ec3', - role: Role.ADMIN - }, - update: {}, - where: { id: '4e1af723-95f6-44f8-92a7-464df17f6ec3' } - }); - const userDemo = await prisma.user.upsert({ create: { accessToken: @@ -345,7 +321,6 @@ async function main() { platformInteractiveBrokers, platformPostFinance, platformSwissquote, - userAdmin, userDemo }); } diff --git a/yarn.lock b/yarn.lock index 814879352..6d75f42a8 100644 --- a/yarn.lock +++ b/yarn.lock @@ -3349,22 +3349,22 @@ resolved "https://registry.yarnpkg.com/@popperjs/core/-/core-2.10.1.tgz#728ecd95ab207aab8a9a4e421f0422db329232be" integrity sha512-HnUhk1Sy9IuKrxEMdIRCxpIqPw6BFsbYSEUO9p/hNw5sMld/+3OLMWQP80F8/db9qsv3qUjs7ZR5bS/R+iinXw== -"@prisma/client@3.8.1": - version "3.8.1" - resolved "https://registry.yarnpkg.com/@prisma/client/-/client-3.8.1.tgz#c11eda8e84760867552ffde4de7b48fb2cf1e1c0" - integrity sha512-NxD1Xbkx1eT1mxSwo1RwZe665mqBETs0VxohuwNfFIxMqcp0g6d4TgugPxwZ4Jb4e5wCu8mQ9quMedhNWIWcZQ== +"@prisma/client@3.9.1": + version "3.9.1" + resolved "https://registry.yarnpkg.com/@prisma/client/-/client-3.9.1.tgz#565c8121f1220637bcab4a1d1f106b8c1334406c" + integrity sha512-aLwfXKLvL+loQ0IuPPCXkcq8cXBg1IeoHHa5lqQu3dJHdj45wnislA/Ny4UxRQjD5FXqrfAb8sWtF+jhdmjFTg== dependencies: - "@prisma/engines-version" "3.8.0-43.34df67547cf5598f5a6cd3eb45f14ee70c3fb86f" + "@prisma/engines-version" "3.9.0-58.bcc2ff906db47790ee902e7bbc76d7ffb1893009" -"@prisma/engines-version@3.8.0-43.34df67547cf5598f5a6cd3eb45f14ee70c3fb86f": - version "3.8.0-43.34df67547cf5598f5a6cd3eb45f14ee70c3fb86f" - resolved "https://registry.yarnpkg.com/@prisma/engines-version/-/engines-version-3.8.0-43.34df67547cf5598f5a6cd3eb45f14ee70c3fb86f.tgz#4c8d9744b5e54650a8ba5fde0a711399d6adba24" - integrity sha512-G2JH6yWt6ixGKmsRmVgaQYahfwMopim0u/XLIZUo2o/mZ5jdu7+BL+2V5lZr7XiG1axhyrpvlyqE/c0OgYSl3g== +"@prisma/engines-version@3.9.0-58.bcc2ff906db47790ee902e7bbc76d7ffb1893009": + version "3.9.0-58.bcc2ff906db47790ee902e7bbc76d7ffb1893009" + resolved "https://registry.yarnpkg.com/@prisma/engines-version/-/engines-version-3.9.0-58.bcc2ff906db47790ee902e7bbc76d7ffb1893009.tgz#ea03ffa723382a526dc6625ce6eae9b6ad984400" + integrity sha512-5Dh+qTDhpPR66w6NNAnPs+/W/Qt4r1DSd+qhfPFcDThUK4uxoZKGlPb2IYQn5LL+18aIGnmteDf7BnVMmvBNSQ== -"@prisma/engines@3.8.0-43.34df67547cf5598f5a6cd3eb45f14ee70c3fb86f": - version "3.8.0-43.34df67547cf5598f5a6cd3eb45f14ee70c3fb86f" - resolved "https://registry.yarnpkg.com/@prisma/engines/-/engines-3.8.0-43.34df67547cf5598f5a6cd3eb45f14ee70c3fb86f.tgz#4479099b99f6a082ce5843ee7208943ccedd127f" - integrity sha512-bHYubuItSN/DGYo36aDu7xJiJmK52JOSHs4MK+KbceAtwS20BCWadRgtpQ3iZ2EXfN/B1T0iCXlNraaNwnpU2w== +"@prisma/engines@3.9.0-58.bcc2ff906db47790ee902e7bbc76d7ffb1893009": + version "3.9.0-58.bcc2ff906db47790ee902e7bbc76d7ffb1893009" + resolved "https://registry.yarnpkg.com/@prisma/engines/-/engines-3.9.0-58.bcc2ff906db47790ee902e7bbc76d7ffb1893009.tgz#e5c345cdedb7be83d11c1e0c5ab61d866b411256" + integrity sha512-qM+uJbkelB21bnK44gYE049YTHIjHysOuj0mj5U2gDGyNLfmiazlggzFPCgEjgme4U5YB2tYs6Z5Hq08Kl8pjA== "@samverschueren/stream-to-observable@^0.3.0": version "0.3.1" @@ -15025,12 +15025,12 @@ pretty-hrtime@^1.0.3: resolved "https://registry.yarnpkg.com/pretty-hrtime/-/pretty-hrtime-1.0.3.tgz#b7e3ea42435a4c9b2759d99e0f201eb195802ee1" integrity sha1-t+PqQkNaTJsnWdmeDyAesZWALuE= -prisma@3.8.1: - version "3.8.1" - resolved "https://registry.yarnpkg.com/prisma/-/prisma-3.8.1.tgz#44395cef7cbb1ea86216cb84ee02f856c08a7873" - integrity sha512-Q8zHwS9m70TaD7qI8u+8hTAmiTpK+IpvRYF3Rgb/OeWGQJOMgZCFFvNCiSfoLEQ95wilK7ctW3KOpc9AuYnRUA== +prisma@3.9.1: + version "3.9.1" + resolved "https://registry.yarnpkg.com/prisma/-/prisma-3.9.1.tgz#7510a8bf06018a5313b9427b1127ce4750b1ce5c" + integrity sha512-IGcJAu5LzlFv+i+NNhOEh1J1xVVttsVdRBxmrMN7eIH+7mRN6L89Hz1npUAiz4jOpNlHC7n9QwaOYZGxTqlwQw== dependencies: - "@prisma/engines" "3.8.0-43.34df67547cf5598f5a6cd3eb45f14ee70c3fb86f" + "@prisma/engines" "3.9.0-58.bcc2ff906db47790ee902e7bbc76d7ffb1893009" prismjs@^1.21.0, prismjs@~1.24.0: version "1.24.1"