Browse Source

Merge branch 'main' into feature/fix-twr-performance-2

pull/684/head
gizmodus 3 years ago
committed by GitHub
parent
commit
3a2cbdf5e8
No known key found for this signature in database GPG Key ID: 4AEE18F83AFDEB23
  1. 30
      CHANGELOG.md
  2. 12
      README.md
  3. 16
      apps/api/src/app/export/export.controller.ts
  4. 17
      apps/api/src/app/export/export.service.ts
  5. 3
      apps/api/src/app/user/interfaces/user-item.interface.ts
  6. 9
      apps/api/src/app/user/user.controller.ts
  7. 20
      apps/api/src/app/user/user.service.ts
  8. 2
      apps/client/src/app/components/accounts-table/accounts-table.component.ts
  9. 11
      apps/client/src/app/components/home-holdings/home-holdings.component.ts
  10. 1
      apps/client/src/app/components/position/position-detail-dialog/interfaces/interfaces.ts
  11. 22
      apps/client/src/app/components/position/position-detail-dialog/position-detail-dialog.component.ts
  12. 2
      apps/client/src/app/components/position/position-detail-dialog/position-detail-dialog.html
  13. 2
      apps/client/src/app/pages/about/about-page.component.ts
  14. 2
      apps/client/src/app/pages/about/changelog/changelog-page.component.ts
  15. 2
      apps/client/src/app/pages/account/account-page.component.ts
  16. 2
      apps/client/src/app/pages/accounts/accounts-page.component.ts
  17. 28
      apps/client/src/app/pages/accounts/accounts-page.html
  18. 4
      apps/client/src/app/pages/accounts/accounts-page.scss
  19. 2
      apps/client/src/app/pages/blog/2021/07/hallo-ghostfolio/hallo-ghostfolio-page.component.ts
  20. 2
      apps/client/src/app/pages/blog/2021/07/hello-ghostfolio/hello-ghostfolio-page.component.ts
  21. 2
      apps/client/src/app/pages/blog/2022/01/first-months-in-open-source/first-months-in-open-source-page.component.ts
  22. 2
      apps/client/src/app/pages/blog/blog-page.component.ts
  23. 2
      apps/client/src/app/pages/landing/landing-page.component.ts
  24. 3
      apps/client/src/app/pages/portfolio/allocations/allocations-page.component.ts
  25. 2
      apps/client/src/app/pages/portfolio/analysis/analysis-page.component.ts
  26. 2
      apps/client/src/app/pages/portfolio/portfolio-page.component.ts
  27. 2
      apps/client/src/app/pages/portfolio/report/report-page.component.ts
  28. 33
      apps/client/src/app/pages/portfolio/transactions/transactions-page.component.ts
  29. 3
      apps/client/src/app/pages/portfolio/transactions/transactions-page.html
  30. 2
      apps/client/src/app/pages/pricing/pricing-page.component.ts
  31. 2
      apps/client/src/app/pages/public/public-page.component.ts
  32. 13
      apps/client/src/app/pages/register/register-page.component.ts
  33. 7
      apps/client/src/app/pages/register/show-access-token-dialog/show-access-token-dialog.html
  34. 2
      apps/client/src/app/pages/resources/resources-page.component.ts
  35. 2
      apps/client/src/app/pages/webauthn/webauthn-page.component.ts
  36. 12
      apps/client/src/app/services/data.service.ts
  37. 4
      apps/client/src/styles.scss
  38. 14
      libs/common/src/lib/helper.ts
  39. 598
      libs/ui/src/lib/activities-table/activities-table.component.html
  40. 64
      libs/ui/src/lib/activities-table/activities-table.component.scss
  41. 22
      libs/ui/src/lib/activities-table/activities-table.component.ts
  42. 6
      libs/ui/src/lib/value/value.component.ts
  43. 6
      package.json
  44. 6
      prisma/migrations/20220205195653_added_default_value_for_provider_in_user/migration.sql
  45. 2
      prisma/schema.prisma
  46. 25
      prisma/seed.js
  47. 36
      yarn.lock

30
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

12
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_

16
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<Export> {
return await this.exportService.export({
public async export(
@Query('activityIds') activityIds?: string[]
): Promise<Export> {
return this.exportService.export({
activityIds,
userId: this.request.user.id
});
}

17
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<Export> {
const orders = await this.prismaService.order.findMany({
public async export({
activityIds,
userId
}: {
activityIds?: string[];
userId: string;
}): Promise<Export> {
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(

3
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;
}

9
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
})

20
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<User> {
public async createUser(data: Prisma.UserCreateInput): Promise<User> {
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)

2
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'
];

11
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 =
<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',

1
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;
}

22
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();

2
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()"
></gf-activities-table>
</div>

2
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'

2
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'

2
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'

2
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'

28
apps/client/src/app/pages/accounts/accounts-page.html

@ -1,19 +1,21 @@
<div class="container">
<div class="row mb-3">
<div class="row">
<div class="col">
<h3 class="d-flex justify-content-center mb-3" i18n>Accounts</h3>
<gf-accounts-table
[accounts]="accounts"
[baseCurrency]="user?.settings?.baseCurrency"
[deviceType]="deviceType"
[locale]="user?.settings?.locale"
[showActions]="!hasImpersonationId && hasPermissionToDeleteAccount && !user.settings.isRestrictedView"
[totalBalance]="totalBalance"
[totalValue]="totalValue"
[transactionCount]="transactionCount"
(accountDeleted)="onDeleteAccount($event)"
(accountToUpdate)="onUpdateAccount($event)"
></gf-accounts-table>
<div class="accounts">
<gf-accounts-table
[accounts]="accounts"
[baseCurrency]="user?.settings?.baseCurrency"
[deviceType]="deviceType"
[locale]="user?.settings?.locale"
[showActions]="!hasImpersonationId && hasPermissionToDeleteAccount && !user.settings.isRestrictedView"
[totalBalance]="totalBalance"
[totalValue]="totalValue"
[transactionCount]="transactionCount"
(accountDeleted)="onDeleteAccount($event)"
(accountToUpdate)="onUpdateAccount($event)"
></gf-accounts-table>
</div>
</div>
</div>

4
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;

2
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'

2
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'

2
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'

2
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'

2
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'

3
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',

2
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'

2
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'

2
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'

33
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',

3
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()"
></gf-activities-table>
</div>

2
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'

2
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'

13
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'

7
apps/client/src/app/pages/register/show-access-token-dialog/show-access-token-dialog.html

@ -1,4 +1,9 @@
<h1 mat-dialog-title i18n>Create Account</h1>
<h1 mat-dialog-title>
<span i18n>Create Account</span
><span *ngIf="data.role === 'ADMIN'" class="badge badge-light ml-2"
>{{ data.role }}</span
>
</h1>
<div mat-dialog-content>
<div>
<mat-form-field appearance="outline" class="w-100">

2
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'

2
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'

12
apps/client/src/app/services/data.service.ts

@ -94,8 +94,16 @@ export class DataService {
});
}
public fetchExport() {
return this.http.get<Export>('/api/export');
public fetchExport(activityIds?: string[]) {
let params = new HttpParams();
if (activityIds) {
params = params.append('activityIds', activityIds.join(','));
}
return this.http.get<Export>('/api/export', {
params
});
}
public fetchInfo(): InfoItem {

4
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;

14
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');
}

598
libs/ui/src/lib/activities-table/activities-table.component.html

@ -36,309 +36,335 @@
</mat-autocomplete>
</mat-form-field>
<table
class="gf-table w-100"
matSort
matSortActive="date"
matSortDirection="desc"
mat-table
[dataSource]="dataSource"
>
<ng-container matColumnDef="count">
<th
*matHeaderCellDef
class="d-none d-lg-table-cell px-1 text-right"
i18n
mat-header-cell
></th>
<td
*matCellDef="let element; let i = index"
class="d-none d-lg-table-cell px-1 text-right"
mat-cell
>
{{ dataSource.data.length - i }}
</td>
<td
*matFooterCellDef
class="d-none d-lg-table-cell px-1"
mat-footer-cell
></td>
</ng-container>
<ng-container matColumnDef="date">
<th *matHeaderCellDef class="px-1" i18n mat-header-cell mat-sort-header>
Date
</th>
<td *matCellDef="let element" class="px-1" mat-cell>
<div class="d-flex">
{{ element.date | date: defaultDateFormat }}
</div>
</td>
<td *matFooterCellDef class="px-1" i18n mat-footer-cell>Total</td>
</ng-container>
<ng-container matColumnDef="type">
<th *matHeaderCellDef class="px-1" i18n mat-header-cell mat-sort-header>
Type
</th>
<td *matCellDef="let element" mat-cell class="px-1">
<div
class="d-inline-flex p-1 type-badge"
[ngClass]="{
buy: element.type === 'BUY',
dividend: element.type === 'DIVIDEND',
sell: element.type === 'SELL'
}"
<div class="activities">
<table
class="gf-table w-100"
matSort
matSortActive="date"
matSortDirection="desc"
mat-table
[dataSource]="dataSource"
>
<ng-container matColumnDef="count">
<th
*matHeaderCellDef
class="d-none d-lg-table-cell px-1 text-right"
i18n
mat-header-cell
></th>
<td
*matCellDef="let element; let i = index"
class="d-none d-lg-table-cell px-1 text-right"
mat-cell
>
<ion-icon
[name]="
element.type === 'BUY' || element.type === 'DIVIDEND'
? 'arrow-forward-circle-outline'
: 'arrow-back-circle-outline'
"
></ion-icon>
<span class="d-none d-lg-block mx-1">{{ element.type }}</span>
</div>
</td>
<td *matFooterCellDef class="px-1" mat-footer-cell></td>
</ng-container>
{{ dataSource.data.length - i }}
</td>
<td
*matFooterCellDef
class="d-none d-lg-table-cell px-1"
mat-footer-cell
></td>
</ng-container>
<ng-container matColumnDef="date">
<th *matHeaderCellDef class="px-1" i18n mat-header-cell mat-sort-header>
Date
</th>
<td *matCellDef="let element" class="px-1" mat-cell>
<div class="d-flex">
{{ element.date | date: defaultDateFormat }}
</div>
</td>
<td *matFooterCellDef class="px-1" i18n mat-footer-cell>Total</td>
</ng-container>
<ng-container matColumnDef="symbol">
<th *matHeaderCellDef class="px-1" i18n mat-header-cell mat-sort-header>
Symbol
</th>
<td *matCellDef="let element" class="px-1" mat-cell>
<div class="d-flex align-items-center">
{{ element.symbol | gfSymbol }}
<span *ngIf="element.isDraft" class="badge badge-secondary ml-1" i18n
>Draft</span
<ng-container matColumnDef="type">
<th *matHeaderCellDef class="px-1" i18n mat-header-cell mat-sort-header>
Type
</th>
<td *matCellDef="let element" mat-cell class="px-1">
<div
class="d-inline-flex p-1 type-badge"
[ngClass]="{
buy: element.type === 'BUY',
dividend: element.type === 'DIVIDEND',
sell: element.type === 'SELL'
}"
>
</div>
</td>
<td *matFooterCellDef class="px-1" mat-footer-cell></td>
</ng-container>
<ng-container matColumnDef="currency">
<th
*matHeaderCellDef
class="d-none d-lg-table-cell px-1"
i18n
mat-header-cell
mat-sort-header
>
Currency
</th>
<td *matCellDef="let element" class="d-none d-lg-table-cell px-1" mat-cell>
{{ element.currency }}
</td>
<td *matFooterCellDef class="d-none d-lg-table-cell px-1" mat-footer-cell>
{{ baseCurrency }}
</td>
</ng-container>
<ion-icon
[name]="
element.type === 'BUY' || element.type === 'DIVIDEND'
? 'arrow-forward-circle-outline'
: 'arrow-back-circle-outline'
"
></ion-icon>
<span class="d-none d-lg-block mx-1">{{ element.type }}</span>
</div>
</td>
<td *matFooterCellDef class="px-1" mat-footer-cell></td>
</ng-container>
<ng-container matColumnDef="quantity">
<th
*matHeaderCellDef
class="d-none d-lg-table-cell justify-content-end px-1"
i18n
mat-header-cell
mat-sort-header
>
Quantity
</th>
<td *matCellDef="let element" class="d-none d-lg-table-cell px-1" mat-cell>
<div class="d-flex justify-content-end">
<gf-value
[isCurrency]="true"
[locale]="locale"
[value]="isLoading ? undefined : element.quantity"
></gf-value>
</div>
</td>
<td
*matFooterCellDef
class="d-none d-lg-table-cell px-1"
mat-footer-cell
></td>
</ng-container>
<ng-container matColumnDef="symbol">
<th *matHeaderCellDef class="px-1" i18n mat-header-cell mat-sort-header>
Symbol
</th>
<td *matCellDef="let element" class="px-1" mat-cell>
<div class="d-flex align-items-center">
{{ element.symbol | gfSymbol }}
<span *ngIf="element.isDraft" class="badge badge-secondary ml-1" i18n
>Draft</span
>
</div>
</td>
<td *matFooterCellDef class="px-1" mat-footer-cell></td>
</ng-container>
<ng-container matColumnDef="unitPrice">
<th
*matHeaderCellDef
class="d-none d-lg-table-cell justify-content-end px-1"
i18n
mat-header-cell
mat-sort-header
>
Unit Price
</th>
<td *matCellDef="let element" class="d-none d-lg-table-cell px-1" mat-cell>
<div class="d-flex justify-content-end">
<gf-value
[isCurrency]="true"
[locale]="locale"
[value]="isLoading ? undefined : element.unitPrice"
></gf-value>
</div>
</td>
<td
*matFooterCellDef
class="d-none d-lg-table-cell px-1"
mat-footer-cell
></td>
</ng-container>
<ng-container matColumnDef="currency">
<th
*matHeaderCellDef
class="d-none d-lg-table-cell px-1"
i18n
mat-header-cell
mat-sort-header
>
Currency
</th>
<td
*matCellDef="let element"
class="d-none d-lg-table-cell px-1"
mat-cell
>
{{ element.currency }}
</td>
<td *matFooterCellDef class="d-none d-lg-table-cell px-1" mat-footer-cell>
{{ baseCurrency }}
</td>
</ng-container>
<ng-container matColumnDef="fee">
<th
*matHeaderCellDef
class="d-none d-lg-table-cell justify-content-end px-1"
i18n
mat-header-cell
mat-sort-header
>
Fee
</th>
<td *matCellDef="let element" class="d-none d-lg-table-cell px-1" mat-cell>
<div class="d-flex justify-content-end">
<gf-value
[isCurrency]="true"
[locale]="locale"
[value]="isLoading ? undefined : element.fee"
></gf-value>
</div>
</td>
<td *matFooterCellDef class="d-none d-lg-table-cell px-1" mat-footer-cell>
<div class="d-flex justify-content-end">
<gf-value
[isCurrency]="true"
[locale]="locale"
[value]="isLoading ? undefined : totalFees"
></gf-value>
</div>
</td>
</ng-container>
<ng-container matColumnDef="quantity">
<th
*matHeaderCellDef
class="d-none d-lg-table-cell justify-content-end px-1"
i18n
mat-header-cell
mat-sort-header
>
Quantity
</th>
<td
*matCellDef="let element"
class="d-none d-lg-table-cell px-1"
mat-cell
>
<div class="d-flex justify-content-end">
<gf-value
[isCurrency]="true"
[locale]="locale"
[value]="isLoading ? undefined : element.quantity"
></gf-value>
</div>
</td>
<td
*matFooterCellDef
class="d-none d-lg-table-cell px-1"
mat-footer-cell
></td>
</ng-container>
<ng-container matColumnDef="value">
<th
*matHeaderCellDef
class="justify-content-end px-1"
i18n
mat-header-cell
mat-sort-header
>
Value
</th>
<td *matCellDef="let element" class="px-1" mat-cell>
<div class="d-flex justify-content-end">
<gf-value
[isCurrency]="true"
[locale]="locale"
[value]="isLoading ? undefined : element.value"
></gf-value>
</div>
</td>
<td *matFooterCellDef class="px-1" mat-footer-cell>
<div class="d-flex justify-content-end">
<gf-value
[isCurrency]="true"
[locale]="locale"
[value]="isLoading ? undefined : totalValue"
></gf-value>
</div>
</td>
</ng-container>
<ng-container matColumnDef="unitPrice">
<th
*matHeaderCellDef
class="d-none d-lg-table-cell justify-content-end px-1"
i18n
mat-header-cell
mat-sort-header
>
Unit Price
</th>
<td
*matCellDef="let element"
class="d-none d-lg-table-cell px-1"
mat-cell
>
<div class="d-flex justify-content-end">
<gf-value
[isCurrency]="true"
[locale]="locale"
[value]="isLoading ? undefined : element.unitPrice"
></gf-value>
</div>
</td>
<td
*matFooterCellDef
class="d-none d-lg-table-cell px-1"
mat-footer-cell
></td>
</ng-container>
<ng-container matColumnDef="account">
<th *matHeaderCellDef class="px-1" mat-header-cell>
<span class="d-none d-lg-block" i18n>Account</span>
</th>
<td *matCellDef="let element" class="px-1" mat-cell>
<div class="d-flex">
<gf-symbol-icon
*ngIf="element.Account?.Platform?.url"
class="mr-1"
[tooltip]="element.Account?.Platform?.name"
[url]="element.Account?.Platform?.url"
></gf-symbol-icon>
<span class="d-none d-lg-block">{{ element.Account?.name }}</span>
</div>
</td>
<td *matFooterCellDef class="px-1" mat-footer-cell></td>
</ng-container>
<ng-container matColumnDef="fee">
<th
*matHeaderCellDef
class="d-none d-lg-table-cell justify-content-end px-1"
i18n
mat-header-cell
mat-sort-header
>
Fee
</th>
<td
*matCellDef="let element"
class="d-none d-lg-table-cell px-1"
mat-cell
>
<div class="d-flex justify-content-end">
<gf-value
[isCurrency]="true"
[locale]="locale"
[value]="isLoading ? undefined : element.fee"
></gf-value>
</div>
</td>
<td *matFooterCellDef class="d-none d-lg-table-cell px-1" mat-footer-cell>
<div class="d-flex justify-content-end">
<gf-value
[isCurrency]="true"
[locale]="locale"
[value]="isLoading ? undefined : totalFees"
></gf-value>
</div>
</td>
</ng-container>
<ng-container matColumnDef="actions">
<th *matHeaderCellDef class="px-1 text-center" mat-header-cell>
<button
class="mx-1 no-min-width px-2"
mat-button
[matMenuTriggerFor]="activitiesMenu"
(click)="$event.stopPropagation()"
<ng-container matColumnDef="value">
<th
*matHeaderCellDef
class="justify-content-end px-1"
i18n
mat-header-cell
mat-sort-header
>
<ion-icon name="ellipsis-vertical"></ion-icon>
</button>
<mat-menu #activitiesMenu="matMenu" xPosition="before">
Value
</th>
<td *matCellDef="let element" class="px-1" mat-cell>
<div class="d-flex justify-content-end">
<gf-value
[isCurrency]="true"
[locale]="locale"
[value]="isLoading ? undefined : element.value"
></gf-value>
</div>
</td>
<td *matFooterCellDef class="px-1" mat-footer-cell>
<div class="d-flex justify-content-end">
<gf-value
[isAbsolute]="true"
[isCurrency]="true"
[locale]="locale"
[value]="isLoading ? undefined : totalValue"
></gf-value>
</div>
</td>
</ng-container>
<ng-container matColumnDef="account">
<th *matHeaderCellDef class="px-1" mat-header-cell>
<span class="d-none d-lg-block" i18n>Account</span>
</th>
<td *matCellDef="let element" class="px-1" mat-cell>
<div class="d-flex">
<gf-symbol-icon
*ngIf="element.Account?.Platform?.url"
class="mr-1"
[tooltip]="element.Account?.Platform?.name"
[url]="element.Account?.Platform?.url"
></gf-symbol-icon>
<span class="d-none d-lg-block">{{ element.Account?.name }}</span>
</div>
</td>
<td *matFooterCellDef class="px-1" mat-footer-cell></td>
</ng-container>
<ng-container matColumnDef="actions">
<th *matHeaderCellDef class="px-1 text-center" mat-header-cell>
<button
*ngIf="hasPermissionToImportActivities"
class="align-items-center d-flex"
mat-menu-item
(click)="onImport()"
*ngIf="
hasPermissionToExportActivities || hasPermissionToImportActivities
"
class="mx-1 no-min-width px-2"
mat-button
[matMenuTriggerFor]="activitiesMenu"
(click)="$event.stopPropagation()"
>
<ion-icon class="mr-2" name="cloud-upload-outline"></ion-icon>
<span i18n>Import</span>
<ion-icon name="ellipsis-vertical"></ion-icon>
</button>
<mat-menu #activitiesMenu="matMenu" xPosition="before">
<button
*ngIf="hasPermissionToImportActivities"
class="align-items-center d-flex"
mat-menu-item
(click)="onImport()"
>
<ion-icon class="mr-2" name="cloud-upload-outline"></ion-icon>
<span i18n>Import</span>
</button>
<button
*ngIf="hasPermissionToExportActivities"
class="align-items-center d-flex"
mat-menu-item
(click)="onExport()"
>
<ion-icon class="mr-2" name="cloud-download-outline"></ion-icon>
<span i18n>Export</span>
</button>
</mat-menu>
</th>
<td *matCellDef="let element" class="px-1 text-center" mat-cell>
<button
class="align-items-center d-flex"
mat-menu-item
(click)="onExport()"
*ngIf="this.showActions"
class="mx-1 no-min-width px-2"
mat-button
[matMenuTriggerFor]="activityMenu"
(click)="$event.stopPropagation()"
>
<ion-icon class="mr-2" name="cloud-download-outline"></ion-icon>
<span i18n>Export</span>
</button>
</mat-menu>
</th>
<td *matCellDef="let element" class="px-1 text-center" mat-cell>
<button
class="mx-1 no-min-width px-2"
mat-button
[matMenuTriggerFor]="activityMenu"
(click)="$event.stopPropagation()"
>
<ion-icon name="ellipsis-vertical"></ion-icon>
</button>
<mat-menu #activityMenu="matMenu" xPosition="before">
<button i18n mat-menu-item (click)="onUpdateActivity(element)">
Edit
<ion-icon name="ellipsis-vertical"></ion-icon>
</button>
<button i18n mat-menu-item (click)="onCloneActivity(element)">
Clone
</button>
<button i18n mat-menu-item (click)="onDeleteActivity(element.id)">
Delete
</button>
</mat-menu>
</td>
<td *matFooterCellDef class="px-1" mat-footer-cell></td>
</ng-container>
<mat-menu #activityMenu="matMenu" xPosition="before">
<button i18n mat-menu-item (click)="onUpdateActivity(element)">
Edit
</button>
<button i18n mat-menu-item (click)="onCloneActivity(element)">
Clone
</button>
<button i18n mat-menu-item (click)="onDeleteActivity(element.id)">
Delete
</button>
</mat-menu>
</td>
<td *matFooterCellDef class="px-1" mat-footer-cell></td>
</ng-container>
<tr *matHeaderRowDef="displayedColumns" mat-header-row></tr>
<tr
*matRowDef="let row; columns: displayedColumns"
mat-row
(click)="
hasPermissionToOpenDetails &&
!row.isDraft &&
onOpenPositionDialog({
dataSource: row.dataSource,
symbol: row.symbol
})
"
[ngClass]="{ 'cursor-pointer': hasPermissionToOpenDetails && !row.isDraft }"
></tr>
<tr
*matFooterRowDef="displayedColumns"
mat-footer-row
[ngClass]="{ 'd-none': isLoading || dataSource.data.length === 0 }"
></tr>
</table>
<tr *matHeaderRowDef="displayedColumns" mat-header-row></tr>
<tr
*matRowDef="let row; columns: displayedColumns"
mat-row
(click)="
hasPermissionToOpenDetails &&
!row.isDraft &&
onOpenPositionDialog({
dataSource: row.dataSource,
symbol: row.symbol
})
"
[ngClass]="{
'cursor-pointer': hasPermissionToOpenDetails && !row.isDraft
}"
></tr>
<tr
*matFooterRowDef="displayedColumns"
mat-footer-row
[ngClass]="{ 'd-none': isLoading || dataSource.data.length === 0 }"
></tr>
</table>
</div>
<ngx-skeleton-loader
*ngIf="isLoading"

64
libs/ui/src/lib/activities-table/activities-table.component.scss

@ -14,45 +14,49 @@
min-height: 1.5rem !important;
}
.mat-table {
td {
&.mat-footer-cell {
border-top: 1px solid
rgba(
var(--palette-foreground-divider),
var(--palette-foreground-divider-alpha)
);
.activities {
overflow-x: auto;
.mat-table {
td {
&.mat-footer-cell {
border-top: 1px solid
rgba(
var(--palette-foreground-divider),
var(--palette-foreground-divider-alpha)
);
}
}
}
th {
::ng-deep {
.mat-sort-header-container {
justify-content: inherit;
th {
::ng-deep {
.mat-sort-header-container {
justify-content: inherit;
}
}
}
}
.mat-row {
.type-badge {
background-color: rgba(var(--palette-foreground-text), 0.05);
border-radius: 1rem;
line-height: 1em;
.mat-row {
.type-badge {
background-color: rgba(var(--palette-foreground-text), 0.05);
border-radius: 1rem;
line-height: 1em;
ion-icon {
font-size: 1rem;
}
ion-icon {
font-size: 1rem;
}
&.buy {
color: var(--green);
}
&.buy {
color: var(--green);
}
&.dividend {
color: var(--blue);
}
&.dividend {
color: var(--blue);
}
&.sell {
color: var(--orange);
&.sell {
color: var(--orange);
}
}
}
}

22
libs/ui/src/lib/activities-table/activities-table.component.ts

@ -43,6 +43,7 @@ export class ActivitiesTableComponent implements OnChanges, OnDestroy {
@Input() baseCurrency: string;
@Input() deviceType: string;
@Input() hasPermissionToCreateActivity: boolean;
@Input() hasPermissionToExportActivities: boolean;
@Input() hasPermissionToFilter = true;
@Input() hasPermissionToImportActivities: boolean;
@Input() hasPermissionToOpenDetails = true;
@ -53,7 +54,7 @@ export class ActivitiesTableComponent implements OnChanges, OnDestroy {
@Output() activityDeleted = new EventEmitter<string>();
@Output() activityToClone = new EventEmitter<OrderWithAccount>();
@Output() activityToUpdate = new EventEmitter<OrderWithAccount>();
@Output() export = new EventEmitter<void>();
@Output() export = new EventEmitter<string[]>();
@Output() import = new EventEmitter<void>();
@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() {

6
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))) {

6
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",

6
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';

2
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[]

25
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
});
}

36
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"

Loading…
Cancel
Save