Browse Source

Merge branch 'main' into bugfix/fix-state-of-currency-selector-component

pull/3429/head
Thomas Kaul 1 year ago
committed by GitHub
parent
commit
b6181a5be5
No known key found for this signature in database GPG Key ID: B5690EEEBB952194
  1. 28
      CHANGELOG.md
  2. 2
      README.md
  3. 15
      apps/api/src/app/auth/google.strategy.ts
  4. 2
      apps/api/src/app/portfolio/interfaces/portfolio-holding-detail.interface.ts
  5. 4
      apps/api/src/app/portfolio/portfolio.controller.ts
  6. 6
      apps/api/src/app/portfolio/portfolio.service.ts
  7. 2448
      apps/api/src/assets/cryptocurrencies/cryptocurrencies.json
  8. 4
      apps/api/src/main.ts
  9. 50
      apps/client/src/app/app-routing.module.ts
  10. 8
      apps/client/src/app/app.component.ts
  11. 0
      apps/client/src/app/components/holding-detail-dialog/holding-detail-dialog.component.scss
  12. 56
      apps/client/src/app/components/holding-detail-dialog/holding-detail-dialog.component.ts
  13. 0
      apps/client/src/app/components/holding-detail-dialog/holding-detail-dialog.html
  14. 2
      apps/client/src/app/components/holding-detail-dialog/interfaces/interfaces.ts
  15. 40
      apps/client/src/app/components/position-detail-dialog/position-detail-dialog.module.ts
  16. 3
      apps/client/src/app/core/auth.guard.ts
  17. 11
      apps/client/src/app/core/paths.ts
  18. 2
      apps/client/src/app/pages/about/about-page-routing.module.ts
  19. 16
      apps/client/src/app/pages/demo/demo-page-routing.module.ts
  20. 5
      apps/client/src/app/pages/demo/demo-page.component.ts
  21. 12
      apps/client/src/app/pages/demo/demo-page.module.ts
  22. 21
      apps/client/src/app/pages/features/features-page-routing.module.ts
  23. 15
      apps/client/src/app/pages/features/features-page.component.ts
  24. 22
      apps/client/src/app/pages/features/features-page.module.ts
  25. 20
      apps/client/src/app/pages/i18n/i18n-page-routing.module.ts
  26. 5
      apps/client/src/app/pages/i18n/i18n-page.component.ts
  27. 12
      apps/client/src/app/pages/i18n/i18n-page.module.ts
  28. 7
      apps/client/src/app/pages/portfolio/activities/create-or-update-activity-dialog/create-or-update-activity-dialog.component.ts
  29. 14
      apps/client/src/app/pages/webauthn/webauthn-page-routing.module.ts
  30. 13
      apps/client/src/app/pages/webauthn/webauthn-page.component.ts
  31. 21
      apps/client/src/app/pages/webauthn/webauthn-page.module.ts
  32. 52
      apps/client/src/app/services/data.service.ts
  33. 15
      libs/ui/src/lib/assistant/assistant.component.ts
  34. 34
      libs/ui/src/lib/fire-calculator/fire-calculator.component.ts
  35. 6
      libs/ui/src/lib/holdings-table/holdings-table.component.ts
  36. 94
      package.json
  37. 1072
      yarn.lock

28
CHANGELOG.md

@ -9,8 +9,36 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
### Fixed
- Fixed an issue with the initial annual interest rate in the _FIRE_ calculator
- Fixed the state handling in the currency selector
## 2.83.0 - 2024-05-30
### Changed
- Upgraded `@nestjs/passport` from version `10.0.0` to `10.0.3`
- Upgraded `angular` from version `17.3.5` to `17.3.10`
- Upgraded `class-validator` from version `0.14.0` to `0.14.1`
- Upgraded `countup.js` from version `2.3.2` to `2.8.0`
- Upgraded `Nx` from version `19.0.2` to `19.0.5`
- Upgraded `passport` from version `0.6.0` to `0.7.0`
- Upgraded `passport-jwt` from version `4.0.0` to `4.0.1`
- Upgraded `prisma` from version `5.13.0` to `5.14.0`
- Upgraded `yahoo-finance2` from version `2.11.2` to `2.11.3`
## 2.82.0 - 2024-05-22
### Changed
- Improved the usability of the create or update activity dialog by preselecting the (only) account
- Improved the usability of the date range selector in the assistant
- Refactored the holding detail dialog to a standalone component
- Refreshed the cryptocurrencies list
- Refactored various pages to standalone components
- Upgraded `@internationalized/number` from version `3.5.0` to `3.5.2`
- Upgraded `body-parser` from version `1.20.1` to `1.20.2`
- Upgraded `zone.js` from version `0.14.4` to `0.14.5`
## 2.81.0 - 2024-05-12
### Added

2
README.md

@ -275,7 +275,7 @@ Are you building your own project? Add the `ghostfolio` topic to your _GitHub_ r
Ghostfolio is **100% free** and **open source**. We encourage and support an active and healthy community that accepts contributions from the public - including you.
Not sure what to work on? We have got some ideas. Please join the Ghostfolio [Slack](https://join.slack.com/t/ghostfolio/shared_invite/zt-vsaan64h-F_I0fEo5M0P88lP9ibCxFg) channel or post to [@ghostfolio\_](https://twitter.com/ghostfolio_) on _X_. We would love to hear from you.
Not sure what to work on? We have [some ideas](https://github.com/ghostfolio/ghostfolio/issues?q=is%3Aissue+is%3Aopen+label%3A%22help+wanted%22), even for [newcomers](https://github.com/ghostfolio/ghostfolio/issues?q=is%3Aissue+is%3Aopen+label%3A%22good+first+issue%22). Please join the Ghostfolio [Slack](https://join.slack.com/t/ghostfolio/shared_invite/zt-vsaan64h-F_I0fEo5M0P88lP9ibCxFg) channel or post to [@ghostfolio\_](https://twitter.com/ghostfolio_) on _X_. We would love to hear from you.
If you like to support this project, get [**Ghostfolio Premium**](https://ghostfol.io/en/pricing) or [**Buy me a coffee**](https://www.buymeacoffee.com/ghostfolio).

15
apps/api/src/app/auth/google.strategy.ts

@ -3,7 +3,7 @@ import { ConfigurationService } from '@ghostfolio/api/services/configuration/con
import { Injectable, Logger } from '@nestjs/common';
import { PassportStrategy } from '@nestjs/passport';
import { Provider } from '@prisma/client';
import { Strategy } from 'passport-google-oauth20';
import { Profile, Strategy } from 'passport-google-oauth20';
import { AuthService } from './auth.service';
@ -11,7 +11,7 @@ import { AuthService } from './auth.service';
export class GoogleStrategy extends PassportStrategy(Strategy, 'google') {
public constructor(
private readonly authService: AuthService,
readonly configurationService: ConfigurationService
private readonly configurationService: ConfigurationService
) {
super({
callbackURL: `${configurationService.get(
@ -20,7 +20,7 @@ export class GoogleStrategy extends PassportStrategy(Strategy, 'google') {
clientID: configurationService.get('GOOGLE_CLIENT_ID'),
clientSecret: configurationService.get('GOOGLE_SECRET'),
passReqToCallback: true,
scope: ['email', 'profile']
scope: ['profile']
});
}
@ -28,20 +28,17 @@ export class GoogleStrategy extends PassportStrategy(Strategy, 'google') {
request: any,
token: string,
refreshToken: string,
profile,
profile: Profile,
done: Function,
done2: Function
) {
try {
const jwt: string = await this.authService.validateOAuthLogin({
const jwt = await this.authService.validateOAuthLogin({
provider: Provider.GOOGLE,
thirdPartyId: profile.id
});
const user = {
jwt
};
done(null, user);
done(null, { jwt });
} catch (error) {
Logger.error(error, 'GoogleStrategy');
done(error, false);

2
apps/api/src/app/portfolio/interfaces/portfolio-position-detail.interface.ts → apps/api/src/app/portfolio/interfaces/portfolio-holding-detail.interface.ts

@ -7,7 +7,7 @@ import {
import { Account, Tag } from '@prisma/client';
export interface PortfolioPositionDetail {
export interface PortfolioHoldingDetail {
accounts: Account[];
averagePrice: number;
dataProviderInfo: DataProviderInfo;

4
apps/api/src/app/portfolio/portfolio.controller.ts

@ -51,7 +51,7 @@ import { AssetClass, AssetSubClass } from '@prisma/client';
import { Big } from 'big.js';
import { StatusCodes, getReasonPhrase } from 'http-status-codes';
import { PortfolioPositionDetail } from './interfaces/portfolio-position-detail.interface';
import { PortfolioHoldingDetail } from './interfaces/portfolio-holding-detail.interface';
import { PortfolioService } from './portfolio.service';
@Controller('portfolio')
@ -569,7 +569,7 @@ export class PortfolioController {
@Headers(HEADER_KEY_IMPERSONATION.toLowerCase()) impersonationId: string,
@Param('dataSource') dataSource,
@Param('symbol') symbol
): Promise<PortfolioPositionDetail> {
): Promise<PortfolioHoldingDetail> {
const position = await this.portfolioService.getPosition(
dataSource,
impersonationId,

6
apps/api/src/app/portfolio/portfolio.service.ts

@ -77,7 +77,7 @@ import {
PerformanceCalculationType,
PortfolioCalculatorFactory
} from './calculator/portfolio-calculator.factory';
import { PortfolioPositionDetail } from './interfaces/portfolio-position-detail.interface';
import { PortfolioHoldingDetail } from './interfaces/portfolio-holding-detail.interface';
import { RulesService } from './rules.service';
const asiaPacificMarkets = require('../../assets/countries/asia-pacific-markets.json');
@ -602,7 +602,7 @@ export class PortfolioService {
aDataSource: DataSource,
aImpersonationId: string,
aSymbol: string
): Promise<PortfolioPositionDetail> {
): Promise<PortfolioHoldingDetail> {
const userId = await this.getUserId(aImpersonationId, this.request.user.id);
const user = await this.userService.user({ id: userId });
const userCurrency = this.getUserCurrency(user);
@ -693,7 +693,7 @@ export class PortfolioService {
transactionCount
} = position;
const accounts: PortfolioPositionDetail['accounts'] = uniqBy(
const accounts: PortfolioHoldingDetail['accounts'] = uniqBy(
orders.filter(({ Account }) => {
return Account;
}),

2448
apps/api/src/assets/cryptocurrencies/cryptocurrencies.json

File diff suppressed because it is too large

4
apps/api/src/main.ts

@ -2,7 +2,7 @@ import { Logger, ValidationPipe, VersioningType } from '@nestjs/common';
import { ConfigService } from '@nestjs/config';
import { NestFactory } from '@nestjs/core';
import type { NestExpressApplication } from '@nestjs/platform-express';
import * as bodyParser from 'body-parser';
import { json } from 'body-parser';
import helmet from 'helmet';
import { AppModule } from './app/app.module';
@ -34,7 +34,7 @@ async function bootstrap() {
);
// Support 10mb csv/json files for importing activities
app.use(bodyParser.json({ limit: '10mb' }));
app.use(json({ limit: '10mb' }));
if (configService.get<string>('ENABLE_FEATURE_SUBSCRIPTION') === 'true') {
app.use(

50
apps/client/src/app/app-routing.module.ts

@ -1,3 +1,5 @@
import { AuthGuard } from '@ghostfolio/client/core/auth.guard';
import { paths } from '@ghostfolio/client/core/paths';
import { PageTitleStrategy } from '@ghostfolio/client/services/page-title.strategy';
import { NgModule } from '@angular/core';
@ -5,18 +7,6 @@ import { RouterModule, Routes, TitleStrategy } from '@angular/router';
import { ModulePreloadService } from './core/module-preload.service';
export const paths = {
about: $localize`about`,
faq: $localize`faq`,
features: $localize`features`,
license: $localize`license`,
markets: $localize`markets`,
pricing: $localize`pricing`,
privacyPolicy: $localize`privacy-policy`,
register: $localize`register`,
resources: $localize`resources`
};
const routes: Routes = [
{
path: paths.about,
@ -53,9 +43,12 @@ const routes: Routes = [
import('./pages/blog/blog-page.module').then((m) => m.BlogPageModule)
},
{
path: 'demo',
loadChildren: () =>
import('./pages/demo/demo-page.module').then((m) => m.DemoPageModule)
canActivate: [AuthGuard],
loadComponent: () =>
import('./pages/demo/demo-page.component').then(
(c) => c.GfDemoPageComponent
),
path: 'demo'
},
{
path: paths.faq,
@ -63,11 +56,13 @@ const routes: Routes = [
import('./pages/faq/faq-page.module').then((m) => m.FaqPageModule)
},
{
canActivate: [AuthGuard],
loadComponent: () =>
import('./pages/features/features-page.component').then(
(c) => c.GfFeaturesPageComponent
),
path: paths.features,
loadChildren: () =>
import('./pages/features/features-page.module').then(
(m) => m.FeaturesPageModule
)
title: $localize`Features`
},
{
path: 'home',
@ -75,9 +70,13 @@ const routes: Routes = [
import('./pages/home/home-page.module').then((m) => m.HomePageModule)
},
{
canActivate: [AuthGuard],
loadComponent: () =>
import('./pages/i18n/i18n-page.component').then(
(c) => c.GfI18nPageComponent
),
path: 'i18n',
loadChildren: () =>
import('./pages/i18n/i18n-page.module').then((m) => m.I18nPageModule)
title: $localize`Internationalization`
},
{
path: paths.markets,
@ -134,11 +133,12 @@ const routes: Routes = [
)
},
{
loadComponent: () =>
import('./pages/webauthn/webauthn-page.component').then(
(c) => c.GfWebauthnPageComponent
),
path: 'webauthn',
loadChildren: () =>
import('./pages/webauthn/webauthn-page.module').then(
(m) => m.WebauthnPageModule
)
title: $localize`Sign in`
},
{
path: 'zen',

8
apps/client/src/app/app.component.ts

@ -1,3 +1,5 @@
import { GfHoldingDetailDialogComponent } from '@ghostfolio/client/components/holding-detail-dialog/holding-detail-dialog.component';
import { HoldingDetailDialogParams } from '@ghostfolio/client/components/holding-detail-dialog/interfaces/interfaces';
import { getCssVariable } from '@ghostfolio/common/helper';
import { InfoItem, User } from '@ghostfolio/common/interfaces';
import { hasPermission, permissions } from '@ghostfolio/common/permissions';
@ -26,8 +28,6 @@ import { DeviceDetectorService } from 'ngx-device-detector';
import { Subject } from 'rxjs';
import { filter, takeUntil } from 'rxjs/operators';
import { PositionDetailDialogParams } from './components/position-detail-dialog/interfaces/interfaces';
import { PositionDetailDialog } from './components/position-detail-dialog/position-detail-dialog.component';
import { DataService } from './services/data.service';
import { ImpersonationStorageService } from './services/impersonation-storage.service';
import { TokenStorageService } from './services/token-storage.service';
@ -246,9 +246,9 @@ export class AppComponent implements OnDestroy, OnInit {
.subscribe((user) => {
this.user = user;
const dialogRef = this.dialog.open(PositionDetailDialog, {
const dialogRef = this.dialog.open(GfHoldingDetailDialogComponent, {
autoFocus: false,
data: <PositionDetailDialogParams>{
data: <HoldingDetailDialogParams>{
dataSource,
symbol,
baseCurrency: this.user?.settings?.baseCurrency,

0
apps/client/src/app/components/position-detail-dialog/position-detail-dialog.component.scss → apps/client/src/app/components/holding-detail-dialog/holding-detail-dialog.component.scss

56
apps/client/src/app/components/position-detail-dialog/position-detail-dialog.component.ts → apps/client/src/app/components/holding-detail-dialog/holding-detail-dialog.component.ts

@ -1,4 +1,7 @@
import { Activity } from '@ghostfolio/api/app/order/interfaces/activities.interface';
import { GfAccountsTableModule } from '@ghostfolio/client/components/accounts-table/accounts-table.module';
import { GfDialogFooterModule } from '@ghostfolio/client/components/dialog-footer/dialog-footer.module';
import { GfDialogHeaderModule } from '@ghostfolio/client/components/dialog-header/dialog-header.module';
import { DataService } from '@ghostfolio/client/services/data.service';
import { UserService } from '@ghostfolio/client/services/user/user.service';
import { DATE_FORMAT, downloadAsFile } from '@ghostfolio/common/helper';
@ -8,9 +11,16 @@ import {
LineChartItem,
User
} from '@ghostfolio/common/interfaces';
import { GfActivitiesTableComponent } from '@ghostfolio/ui/activities-table';
import { GfDataProviderCreditsComponent } from '@ghostfolio/ui/data-provider-credits';
import { translate } from '@ghostfolio/ui/i18n';
import { GfLineChartComponent } from '@ghostfolio/ui/line-chart';
import { GfPortfolioProportionChartComponent } from '@ghostfolio/ui/portfolio-proportion-chart';
import { GfValueComponent } from '@ghostfolio/ui/value';
import { CommonModule } from '@angular/common';
import {
CUSTOM_ELEMENTS_SCHEMA,
ChangeDetectionStrategy,
ChangeDetectorRef,
Component,
@ -18,24 +28,50 @@ import {
OnDestroy,
OnInit
} from '@angular/core';
import { MAT_DIALOG_DATA, MatDialogRef } from '@angular/material/dialog';
import { MatButtonModule } from '@angular/material/button';
import { MatChipsModule } from '@angular/material/chips';
import {
MAT_DIALOG_DATA,
MatDialogModule,
MatDialogRef
} from '@angular/material/dialog';
import { SortDirection } from '@angular/material/sort';
import { MatTableDataSource } from '@angular/material/table';
import { MatTabsModule } from '@angular/material/tabs';
import { Account, Tag } from '@prisma/client';
import { format, isSameMonth, isToday, parseISO } from 'date-fns';
import { NgxSkeletonLoaderModule } from 'ngx-skeleton-loader';
import { Subject } from 'rxjs';
import { takeUntil } from 'rxjs/operators';
import { PositionDetailDialogParams } from './interfaces/interfaces';
import { HoldingDetailDialogParams } from './interfaces/interfaces';
@Component({
host: { class: 'd-flex flex-column h-100' },
selector: 'gf-position-detail-dialog',
changeDetection: ChangeDetectionStrategy.OnPush,
templateUrl: 'position-detail-dialog.html',
styleUrls: ['./position-detail-dialog.component.scss']
host: { class: 'd-flex flex-column h-100' },
imports: [
CommonModule,
GfAccountsTableModule,
GfActivitiesTableComponent,
GfDataProviderCreditsComponent,
GfDialogFooterModule,
GfDialogHeaderModule,
GfLineChartComponent,
GfPortfolioProportionChartComponent,
GfValueComponent,
MatButtonModule,
MatChipsModule,
MatDialogModule,
MatTabsModule,
NgxSkeletonLoaderModule
],
schemas: [CUSTOM_ELEMENTS_SCHEMA],
selector: 'gf-holding-detail-dialog',
standalone: true,
styleUrls: ['./holding-detail-dialog.component.scss'],
templateUrl: 'holding-detail-dialog.html'
})
export class PositionDetailDialog implements OnDestroy, OnInit {
export class GfHoldingDetailDialogComponent implements OnDestroy, OnInit {
public accounts: Account[];
public activities: Activity[];
public assetClass: string;
@ -80,14 +116,14 @@ export class PositionDetailDialog implements OnDestroy, OnInit {
public constructor(
private changeDetectorRef: ChangeDetectorRef,
private dataService: DataService,
public dialogRef: MatDialogRef<PositionDetailDialog>,
@Inject(MAT_DIALOG_DATA) public data: PositionDetailDialogParams,
public dialogRef: MatDialogRef<GfHoldingDetailDialogComponent>,
@Inject(MAT_DIALOG_DATA) public data: HoldingDetailDialogParams,
private userService: UserService
) {}
public ngOnInit() {
this.dataService
.fetchPositionDetail({
.fetchHoldingDetail({
dataSource: this.data.dataSource,
symbol: this.data.symbol
})

0
apps/client/src/app/components/position-detail-dialog/position-detail-dialog.html → apps/client/src/app/components/holding-detail-dialog/holding-detail-dialog.html

2
apps/client/src/app/components/position-detail-dialog/interfaces/interfaces.ts → apps/client/src/app/components/holding-detail-dialog/interfaces/interfaces.ts

@ -2,7 +2,7 @@ import { ColorScheme } from '@ghostfolio/common/types';
import { DataSource } from '@prisma/client';
export interface PositionDetailDialogParams {
export interface HoldingDetailDialogParams {
baseCurrency: string;
colorScheme: ColorScheme;
dataSource: DataSource;

40
apps/client/src/app/components/position-detail-dialog/position-detail-dialog.module.ts

@ -1,40 +0,0 @@
import { GfAccountsTableModule } from '@ghostfolio/client/components/accounts-table/accounts-table.module';
import { GfDialogFooterModule } from '@ghostfolio/client/components/dialog-footer/dialog-footer.module';
import { GfDialogHeaderModule } from '@ghostfolio/client/components/dialog-header/dialog-header.module';
import { GfActivitiesTableComponent } from '@ghostfolio/ui/activities-table';
import { GfDataProviderCreditsComponent } from '@ghostfolio/ui/data-provider-credits';
import { GfLineChartComponent } from '@ghostfolio/ui/line-chart';
import { GfPortfolioProportionChartComponent } from '@ghostfolio/ui/portfolio-proportion-chart';
import { GfValueComponent } from '@ghostfolio/ui/value';
import { CommonModule } from '@angular/common';
import { CUSTOM_ELEMENTS_SCHEMA, NgModule } from '@angular/core';
import { MatButtonModule } from '@angular/material/button';
import { MatChipsModule } from '@angular/material/chips';
import { MatDialogModule } from '@angular/material/dialog';
import { MatTabsModule } from '@angular/material/tabs';
import { NgxSkeletonLoaderModule } from 'ngx-skeleton-loader';
import { PositionDetailDialog } from './position-detail-dialog.component';
@NgModule({
declarations: [PositionDetailDialog],
imports: [
CommonModule,
GfAccountsTableModule,
GfActivitiesTableComponent,
GfDataProviderCreditsComponent,
GfDialogFooterModule,
GfDialogHeaderModule,
GfLineChartComponent,
GfPortfolioProportionChartComponent,
GfValueComponent,
MatButtonModule,
MatChipsModule,
MatDialogModule,
MatTabsModule,
NgxSkeletonLoaderModule
],
schemas: [CUSTOM_ELEMENTS_SCHEMA]
})
export class GfPositionDetailDialogModule {}

3
apps/client/src/app/core/auth.guard.ts

@ -1,4 +1,3 @@
import { paths } from '@ghostfolio/client/app-routing.module';
import { DataService } from '@ghostfolio/client/services/data.service';
import { SettingsStorageService } from '@ghostfolio/client/services/settings-storage.service';
import { UserService } from '@ghostfolio/client/services/user/user.service';
@ -12,6 +11,8 @@ import {
import { EMPTY } from 'rxjs';
import { catchError } from 'rxjs/operators';
import { paths } from './paths';
@Injectable({ providedIn: 'root' })
export class AuthGuard {
private static PUBLIC_PAGE_ROUTES = [

11
apps/client/src/app/core/paths.ts

@ -0,0 +1,11 @@
export const paths = {
about: $localize`about`,
faq: $localize`faq`,
features: $localize`features`,
license: $localize`license`,
markets: $localize`markets`,
pricing: $localize`pricing`,
privacyPolicy: $localize`privacy-policy`,
register: $localize`register`,
resources: $localize`resources`
};

2
apps/client/src/app/pages/about/about-page-routing.module.ts

@ -1,5 +1,5 @@
import { paths } from '@ghostfolio/client/app-routing.module';
import { AuthGuard } from '@ghostfolio/client/core/auth.guard';
import { paths } from '@ghostfolio/client/core/paths';
import { NgModule } from '@angular/core';
import { RouterModule, Routes } from '@angular/router';

16
apps/client/src/app/pages/demo/demo-page-routing.module.ts

@ -1,16 +0,0 @@
import { AuthGuard } from '@ghostfolio/client/core/auth.guard';
import { NgModule } from '@angular/core';
import { RouterModule, Routes } from '@angular/router';
import { DemoPageComponent } from './demo-page.component';
const routes: Routes = [
{ path: '', component: DemoPageComponent, canActivate: [AuthGuard] }
];
@NgModule({
imports: [RouterModule.forChild(routes)],
exports: [RouterModule]
})
export class DemoPageRoutingModule {}

5
apps/client/src/app/pages/demo/demo-page.component.ts

@ -2,16 +2,19 @@ import { DataService } from '@ghostfolio/client/services/data.service';
import { TokenStorageService } from '@ghostfolio/client/services/token-storage.service';
import { InfoItem } from '@ghostfolio/common/interfaces';
import { CommonModule } from '@angular/common';
import { Component, OnDestroy } from '@angular/core';
import { Router } from '@angular/router';
import { Subject } from 'rxjs';
@Component({
host: { class: 'page' },
imports: [CommonModule],
selector: 'gf-demo-page',
standalone: true,
templateUrl: './demo-page.html'
})
export class DemoPageComponent implements OnDestroy {
export class GfDemoPageComponent implements OnDestroy {
public info: InfoItem;
private unsubscribeSubject = new Subject<void>();

12
apps/client/src/app/pages/demo/demo-page.module.ts

@ -1,12 +0,0 @@
import { CommonModule } from '@angular/common';
import { CUSTOM_ELEMENTS_SCHEMA, NgModule } from '@angular/core';
import { DemoPageRoutingModule } from './demo-page-routing.module';
import { DemoPageComponent } from './demo-page.component';
@NgModule({
declarations: [DemoPageComponent],
imports: [CommonModule, DemoPageRoutingModule],
schemas: [CUSTOM_ELEMENTS_SCHEMA]
})
export class DemoPageModule {}

21
apps/client/src/app/pages/features/features-page-routing.module.ts

@ -1,21 +0,0 @@
import { AuthGuard } from '@ghostfolio/client/core/auth.guard';
import { NgModule } from '@angular/core';
import { RouterModule, Routes } from '@angular/router';
import { FeaturesPageComponent } from './features-page.component';
const routes: Routes = [
{
canActivate: [AuthGuard],
component: FeaturesPageComponent,
path: '',
title: $localize`Features`
}
];
@NgModule({
imports: [RouterModule.forChild(routes)],
exports: [RouterModule]
})
export class FeaturesPageRoutingModule {}

15
apps/client/src/app/pages/features/features-page.component.ts

@ -2,17 +2,30 @@ import { DataService } from '@ghostfolio/client/services/data.service';
import { UserService } from '@ghostfolio/client/services/user/user.service';
import { InfoItem, User } from '@ghostfolio/common/interfaces';
import { hasPermission, permissions } from '@ghostfolio/common/permissions';
import { GfPremiumIndicatorComponent } from '@ghostfolio/ui/premium-indicator';
import { CommonModule } from '@angular/common';
import { ChangeDetectorRef, Component, OnDestroy } from '@angular/core';
import { MatButtonModule } from '@angular/material/button';
import { MatCardModule } from '@angular/material/card';
import { RouterModule } from '@angular/router';
import { Subject, takeUntil } from 'rxjs';
@Component({
host: { class: 'page' },
imports: [
CommonModule,
GfPremiumIndicatorComponent,
MatButtonModule,
MatCardModule,
RouterModule
],
selector: 'gf-features-page',
standalone: true,
styleUrls: ['./features-page.scss'],
templateUrl: './features-page.html'
})
export class FeaturesPageComponent implements OnDestroy {
export class GfFeaturesPageComponent implements OnDestroy {
public hasPermissionForSubscription: boolean;
public info: InfoItem;
public routerLinkRegister = ['/' + $localize`register`];

22
apps/client/src/app/pages/features/features-page.module.ts

@ -1,22 +0,0 @@
import { GfPremiumIndicatorComponent } from '@ghostfolio/ui/premium-indicator';
import { CommonModule } from '@angular/common';
import { CUSTOM_ELEMENTS_SCHEMA, NgModule } from '@angular/core';
import { MatButtonModule } from '@angular/material/button';
import { MatCardModule } from '@angular/material/card';
import { FeaturesPageRoutingModule } from './features-page-routing.module';
import { FeaturesPageComponent } from './features-page.component';
@NgModule({
declarations: [FeaturesPageComponent],
imports: [
CommonModule,
FeaturesPageRoutingModule,
GfPremiumIndicatorComponent,
MatButtonModule,
MatCardModule
],
schemas: [CUSTOM_ELEMENTS_SCHEMA]
})
export class FeaturesPageModule {}

20
apps/client/src/app/pages/i18n/i18n-page-routing.module.ts

@ -1,20 +0,0 @@
import { AuthGuard } from '@ghostfolio/client/core/auth.guard';
import { NgModule } from '@angular/core';
import { RouterModule, Routes } from '@angular/router';
import { I18nPageComponent } from './i18n-page.component';
const routes: Routes = [
{
canActivate: [AuthGuard],
component: I18nPageComponent,
path: ''
}
];
@NgModule({
imports: [RouterModule.forChild(routes)],
exports: [RouterModule]
})
export class I18nPageRoutingModule {}

5
apps/client/src/app/pages/i18n/i18n-page.component.ts

@ -1,13 +1,16 @@
import { CommonModule } from '@angular/common';
import { Component, OnInit } from '@angular/core';
import { Subject } from 'rxjs';
@Component({
host: { class: 'page' },
imports: [CommonModule],
selector: 'gf-i18n-page',
standalone: true,
styleUrls: ['./i18n-page.scss'],
templateUrl: './i18n-page.html'
})
export class I18nPageComponent implements OnInit {
export class GfI18nPageComponent implements OnInit {
private unsubscribeSubject = new Subject<void>();
public constructor() {}

12
apps/client/src/app/pages/i18n/i18n-page.module.ts

@ -1,12 +0,0 @@
import { CommonModule } from '@angular/common';
import { CUSTOM_ELEMENTS_SCHEMA, NgModule } from '@angular/core';
import { I18nPageRoutingModule } from './i18n-page-routing.module';
import { I18nPageComponent } from './i18n-page.component';
@NgModule({
declarations: [I18nPageComponent],
imports: [CommonModule, I18nPageRoutingModule],
schemas: [CUSTOM_ELEMENTS_SCHEMA]
})
export class I18nPageModule {}

7
apps/client/src/app/pages/portfolio/activities/create-or-update-activity-dialog/create-or-update-activity-dialog.component.ts

@ -91,7 +91,12 @@ export class CreateOrUpdateActivityDialog implements OnDestroy {
});
this.activityForm = this.formBuilder.group({
accountId: [this.data.activity?.accountId, Validators.required],
accountId: [
this.data.accounts.length === 1 && !this.data.activity?.accountId
? this.data.accounts[0].id
: this.data.activity?.accountId,
Validators.required
],
assetClass: [this.data.activity?.SymbolProfile?.assetClass],
assetSubClass: [this.data.activity?.SymbolProfile?.assetSubClass],
comment: [this.data.activity?.comment],

14
apps/client/src/app/pages/webauthn/webauthn-page-routing.module.ts

@ -1,14 +0,0 @@
import { WebauthnPageComponent } from '@ghostfolio/client/pages/webauthn/webauthn-page.component';
import { NgModule } from '@angular/core';
import { RouterModule, Routes } from '@angular/router';
const routes: Routes = [
{ component: WebauthnPageComponent, path: '', title: $localize`Sign in` }
];
@NgModule({
imports: [RouterModule.forChild(routes)],
exports: [RouterModule]
})
export class WebauthnPageRoutingModule {}

13
apps/client/src/app/pages/webauthn/webauthn-page.component.ts

@ -1,18 +1,29 @@
import { TokenStorageService } from '@ghostfolio/client/services/token-storage.service';
import { WebAuthnService } from '@ghostfolio/client/services/web-authn.service';
import { GfLogoComponent } from '@ghostfolio/ui/logo';
import { CommonModule } from '@angular/common';
import { ChangeDetectorRef, Component, OnDestroy, OnInit } from '@angular/core';
import { MatButtonModule } from '@angular/material/button';
import { MatProgressSpinnerModule } from '@angular/material/progress-spinner';
import { Router } from '@angular/router';
import { Subject } from 'rxjs';
import { takeUntil } from 'rxjs/operators';
@Component({
host: { class: 'page' },
imports: [
CommonModule,
GfLogoComponent,
MatButtonModule,
MatProgressSpinnerModule
],
selector: 'gf-webauthn-page',
standalone: true,
styleUrls: ['./webauthn-page.scss'],
templateUrl: './webauthn-page.html'
})
export class WebauthnPageComponent implements OnDestroy, OnInit {
export class GfWebauthnPageComponent implements OnDestroy, OnInit {
public hasError = false;
private unsubscribeSubject = new Subject<void>();

21
apps/client/src/app/pages/webauthn/webauthn-page.module.ts

@ -1,21 +0,0 @@
import { WebauthnPageComponent } from '@ghostfolio/client/pages/webauthn/webauthn-page.component';
import { GfLogoComponent } from '@ghostfolio/ui/logo';
import { CommonModule } from '@angular/common';
import { NgModule } from '@angular/core';
import { MatButtonModule } from '@angular/material/button';
import { MatProgressSpinnerModule } from '@angular/material/progress-spinner';
import { WebauthnPageRoutingModule } from './webauthn-page-routing.module';
@NgModule({
declarations: [WebauthnPageComponent],
imports: [
CommonModule,
GfLogoComponent,
MatButtonModule,
MatProgressSpinnerModule,
WebauthnPageRoutingModule
]
})
export class WebauthnPageModule {}

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

@ -6,7 +6,7 @@ import { UpdateAccountDto } from '@ghostfolio/api/app/account/update-account.dto
import { CreateOrderDto } from '@ghostfolio/api/app/order/create-order.dto';
import { Activities } from '@ghostfolio/api/app/order/interfaces/activities.interface';
import { UpdateOrderDto } from '@ghostfolio/api/app/order/update-order.dto';
import { PortfolioPositionDetail } from '@ghostfolio/api/app/portfolio/interfaces/portfolio-position-detail.interface';
import { PortfolioHoldingDetail } from '@ghostfolio/api/app/portfolio/interfaces/portfolio-holding-detail.interface';
import { LookupItem } from '@ghostfolio/api/app/symbol/interfaces/lookup-item.interface';
import { SymbolItem } from '@ghostfolio/api/app/symbol/interfaces/symbol-item.interface';
import { UserItem } from '@ghostfolio/api/app/user/interfaces/user-item.interface';
@ -325,6 +325,31 @@ export class DataService {
});
}
public fetchHoldingDetail({
dataSource,
symbol
}: {
dataSource: DataSource;
symbol: string;
}) {
return this.http
.get<PortfolioHoldingDetail>(
`/api/v1/portfolio/position/${dataSource}/${symbol}`
)
.pipe(
map((data) => {
if (data.orders) {
for (const order of data.orders) {
order.createdAt = parseISO(<string>(<unknown>order.createdAt));
order.date = parseISO(<string>(<unknown>order.date));
}
}
return data;
})
);
}
public fetchInfo(): InfoItem {
const info = cloneDeep((window as any).info);
const utmSource = <'ios' | 'trusted-web-activity'>(
@ -563,31 +588,6 @@ export class DataService {
return this.http.get<PortfolioReport>('/api/v1/portfolio/report');
}
public fetchPositionDetail({
dataSource,
symbol
}: {
dataSource: DataSource;
symbol: string;
}) {
return this.http
.get<PortfolioPositionDetail>(
`/api/v1/portfolio/position/${dataSource}/${symbol}`
)
.pipe(
map((data) => {
if (data.orders) {
for (const order of data.orders) {
order.createdAt = parseISO(<string>(<unknown>order.createdAt));
order.date = parseISO(<string>(<unknown>order.date));
}
}
return data;
})
);
}
public loginAnonymous(accessToken: string) {
return this.http.post<OAuthResponse>(`/api/v1/auth/anonymous`, {
accessToken

15
libs/ui/src/lib/assistant/assistant.component.ts

@ -228,12 +228,7 @@ export class GfAssistantComponent implements OnChanges, OnDestroy, OnInit {
{
label: '1 ' + $localize`year` + ' (' + $localize`1Y` + ')',
value: '1y'
},
{
label: '5 ' + $localize`years` + ' (' + $localize`5Y` + ')',
value: '5y'
},
{ label: $localize`Max`, value: 'max' }
}
];
if (this.user?.settings?.isExperimentalFeatures) {
@ -250,6 +245,14 @@ export class GfAssistantComponent implements OnChanges, OnDestroy, OnInit {
);
}
this.dateRangeOptions = this.dateRangeOptions.concat([
{
label: '5 ' + $localize`years` + ' (' + $localize`5Y` + ')',
value: '5y'
},
{ label: $localize`Max`, value: 'max' }
]);
this.dateRangeFormControl.setValue(this.user?.settings?.dateRange ?? null);
this.filterForm.setValue(

34
libs/ui/src/lib/fire-calculator/fire-calculator.component.ts

@ -123,19 +123,6 @@ export class GfFireCalculatorComponent implements OnChanges, OnDestroy {
Tooltip
);
this.calculatorForm.setValue(
{
annualInterestRate: this.annualInterestRate,
paymentPerPeriod: this.savingsRate,
principalInvestmentAmount: 0,
projectedTotalAmount: this.projectedTotalAmount,
retirementDate: this.retirementDate ?? this.DEFAULT_RETIREMENT_DATE
},
{
emitEvent: false
}
);
this.calculatorForm.valueChanges
.pipe(takeUntil(this.unsubscribeSubject))
.subscribe(() => {
@ -169,9 +156,22 @@ export class GfFireCalculatorComponent implements OnChanges, OnDestroy {
}
public ngOnChanges() {
this.periodsToRetire = this.getPeriodsToRetire();
if (isNumber(this.fireWealth) && this.fireWealth >= 0) {
this.calculatorForm.setValue(
{
annualInterestRate: this.annualInterestRate,
paymentPerPeriod: this.savingsRate,
principalInvestmentAmount: 0,
projectedTotalAmount: this.projectedTotalAmount,
retirementDate: this.retirementDate ?? this.DEFAULT_RETIREMENT_DATE
},
{
emitEvent: false
}
);
this.periodsToRetire = this.getPeriodsToRetire();
setTimeout(() => {
// Wait for the chartCanvas
this.calculatorForm.patchValue(
@ -409,9 +409,9 @@ export class GfFireCalculatorComponent implements OnChanges, OnDestroy {
if (this.projectedTotalAmount) {
const periods = this.fireCalculatorService.calculatePeriodsToRetire({
P: this.getP(),
totalAmount: this.projectedTotalAmount,
PMT: this.getPMT(),
r: this.getR()
r: this.getR(),
totalAmount: this.projectedTotalAmount
});
return periods;

6
libs/ui/src/lib/holdings-table/holdings-table.component.ts

@ -1,5 +1,5 @@
import { GfAssetProfileIconComponent } from '@ghostfolio/client/components/asset-profile-icon/asset-profile-icon.component';
import { GfPositionDetailDialogModule } from '@ghostfolio/client/components/position-detail-dialog/position-detail-dialog.module';
import { GfHoldingDetailDialogComponent } from '@ghostfolio/client/components/holding-detail-dialog/holding-detail-dialog.component';
import { GfSymbolModule } from '@ghostfolio/client/pipes/symbol/symbol.module';
import { getLocale } from '@ghostfolio/common/helper';
import { PortfolioPosition, UniqueAsset } from '@ghostfolio/common/interfaces';
@ -23,7 +23,7 @@ import { MatPaginator, MatPaginatorModule } from '@angular/material/paginator';
import { MatSort, MatSortModule } from '@angular/material/sort';
import { MatTableDataSource, MatTableModule } from '@angular/material/table';
import { Router, RouterModule } from '@angular/router';
import { AssetClass, AssetSubClass } from '@prisma/client';
import { AssetSubClass } from '@prisma/client';
import { NgxSkeletonLoaderModule } from 'ngx-skeleton-loader';
import { Subject, Subscription } from 'rxjs';
@ -32,8 +32,8 @@ import { Subject, Subscription } from 'rxjs';
imports: [
CommonModule,
GfAssetProfileIconComponent,
GfHoldingDetailDialogComponent,
GfNoTransactionsInfoComponent,
GfPositionDetailDialogModule,
GfSymbolModule,
GfValueComponent,
MatButtonModule,

94
package.json

@ -1,6 +1,6 @@
{
"name": "ghostfolio",
"version": "2.81.0",
"version": "2.83.0",
"homepage": "https://ghostfol.io",
"license": "AGPL-3.0",
"repository": "https://github.com/ghostfolio/ghostfolio",
@ -54,17 +54,17 @@
"workspace-generator": "nx workspace-generator"
},
"dependencies": {
"@angular/animations": "17.3.5",
"@angular/cdk": "17.3.5",
"@angular/common": "17.3.5",
"@angular/compiler": "17.3.5",
"@angular/core": "17.3.5",
"@angular/forms": "17.3.5",
"@angular/material": "17.3.5",
"@angular/platform-browser": "17.3.5",
"@angular/platform-browser-dynamic": "17.3.5",
"@angular/router": "17.3.5",
"@angular/service-worker": "17.3.5",
"@angular/animations": "17.3.10",
"@angular/cdk": "17.3.10",
"@angular/common": "17.3.10",
"@angular/compiler": "17.3.10",
"@angular/core": "17.3.10",
"@angular/forms": "17.3.10",
"@angular/material": "17.3.10",
"@angular/platform-browser": "17.3.10",
"@angular/platform-browser-dynamic": "17.3.10",
"@angular/router": "17.3.10",
"@angular/service-worker": "17.3.10",
"@codewithdan/observable-store": "2.2.15",
"@dfinity/agent": "0.15.7",
"@dfinity/auth-client": "0.15.7",
@ -72,7 +72,7 @@
"@dfinity/identity": "0.15.7",
"@dfinity/principal": "0.15.7",
"@dinero.js/currencies": "2.0.0-alpha.8",
"@internationalized/number": "3.5.0",
"@internationalized/number": "3.5.2",
"@nestjs/bull": "10.0.1",
"@nestjs/cache-manager": "2.1.0",
"@nestjs/common": "10.1.3",
@ -80,17 +80,17 @@
"@nestjs/core": "10.1.3",
"@nestjs/event-emitter": "2.0.4",
"@nestjs/jwt": "10.1.0",
"@nestjs/passport": "10.0.0",
"@nestjs/passport": "10.0.3",
"@nestjs/platform-express": "10.1.3",
"@nestjs/schedule": "3.0.2",
"@nestjs/serve-static": "4.0.0",
"@prisma/client": "5.13.0",
"@prisma/client": "5.14.0",
"@simplewebauthn/browser": "9.0.1",
"@simplewebauthn/server": "9.0.3",
"@stripe/stripe-js": "1.47.0",
"alphavantage": "2.2.0",
"big.js": "6.2.1",
"body-parser": "1.20.1",
"body-parser": "1.20.2",
"bootstrap": "4.6.0",
"bull": "4.10.4",
"cache-manager": "3.4.3",
@ -101,11 +101,11 @@
"chartjs-plugin-datalabels": "2.2.0",
"cheerio": "1.0.0-rc.12",
"class-transformer": "0.5.1",
"class-validator": "0.14.0",
"class-validator": "0.14.1",
"color": "4.2.3",
"countries-and-timezones": "3.4.1",
"countries-list": "3.1.0",
"countup.js": "2.3.2",
"countup.js": "2.8.0",
"date-fns": "2.29.3",
"envalid": "7.3.1",
"google-spreadsheet": "3.2.0",
@ -123,43 +123,43 @@
"ngx-skeleton-loader": "7.0.0",
"ngx-stripe": "15.5.0",
"papaparse": "5.3.1",
"passport": "0.6.0",
"passport": "0.7.0",
"passport-google-oauth20": "2.0.0",
"passport-jwt": "4.0.0",
"prisma": "5.13.0",
"passport-jwt": "4.0.1",
"prisma": "5.14.0",
"reflect-metadata": "0.1.13",
"rxjs": "7.5.6",
"stripe": "11.12.0",
"svgmap": "2.6.0",
"twitter-api-v2": "1.14.2",
"uuid": "9.0.1",
"yahoo-finance2": "2.11.2",
"zone.js": "0.14.4"
"yahoo-finance2": "2.11.3",
"zone.js": "0.14.5"
},
"devDependencies": {
"@angular-devkit/build-angular": "17.3.5",
"@angular-devkit/core": "17.3.5",
"@angular-devkit/schematics": "17.3.5",
"@angular-eslint/eslint-plugin": "17.3.0",
"@angular-eslint/eslint-plugin-template": "17.3.0",
"@angular-eslint/template-parser": "17.3.0",
"@angular/cli": "17.3.5",
"@angular/compiler-cli": "17.3.5",
"@angular/language-service": "17.3.5",
"@angular/localize": "17.3.5",
"@angular/pwa": "17.3.5",
"@angular-devkit/build-angular": "17.3.8",
"@angular-devkit/core": "17.3.8",
"@angular-devkit/schematics": "17.3.8",
"@angular-eslint/eslint-plugin": "17.5.1",
"@angular-eslint/eslint-plugin-template": "17.5.1",
"@angular-eslint/template-parser": "17.5.1",
"@angular/cli": "17.3.8",
"@angular/compiler-cli": "17.3.10",
"@angular/language-service": "17.3.10",
"@angular/localize": "17.3.10",
"@angular/pwa": "17.3.8",
"@nestjs/schematics": "10.0.1",
"@nestjs/testing": "10.1.3",
"@nx/angular": "19.0.2",
"@nx/cypress": "19.0.2",
"@nx/eslint-plugin": "19.0.2",
"@nx/jest": "19.0.2",
"@nx/js": "19.0.2",
"@nx/nest": "19.0.2",
"@nx/node": "19.0.2",
"@nx/storybook": "19.0.2",
"@nx/web": "19.0.2",
"@nx/workspace": "19.0.2",
"@nx/angular": "19.0.5",
"@nx/cypress": "19.0.5",
"@nx/eslint-plugin": "19.0.5",
"@nx/jest": "19.0.5",
"@nx/js": "19.0.5",
"@nx/nest": "19.0.5",
"@nx/node": "19.0.5",
"@nx/storybook": "19.0.5",
"@nx/web": "19.0.5",
"@nx/workspace": "19.0.5",
"@schematics/angular": "17.3.3",
"@simplewebauthn/types": "9.0.1",
"@storybook/addon-essentials": "7.6.5",
@ -167,7 +167,7 @@
"@storybook/core-server": "7.6.5",
"@trivago/prettier-plugin-sort-imports": "4.3.0",
"@types/big.js": "6.2.2",
"@types/body-parser": "1.19.2",
"@types/body-parser": "1.19.5",
"@types/cache-manager": "3.4.2",
"@types/color": "3.0.3",
"@types/google-spreadsheet": "3.1.5",
@ -175,7 +175,7 @@
"@types/lodash": "4.17.0",
"@types/node": "18.16.9",
"@types/papaparse": "5.3.7",
"@types/passport-google-oauth20": "2.0.11",
"@types/passport-google-oauth20": "2.0.16",
"@typescript-eslint/eslint-plugin": "6.21.0",
"@typescript-eslint/parser": "6.21.0",
"codelyzer": "6.0.1",
@ -188,7 +188,7 @@
"jest": "29.4.3",
"jest-environment-jsdom": "29.4.3",
"jest-preset-angular": "14.0.3",
"nx": "19.0.2",
"nx": "19.0.5",
"prettier": "3.2.5",
"prettier-plugin-organize-attributes": "1.0.0",
"react": "18.2.0",

1072
yarn.lock

File diff suppressed because it is too large
Loading…
Cancel
Save