Compare commits

..

63 Commits

Author SHA1 Message Date
f2d206262e Release 1.254.0 (#1856) 2023-04-14 19:58:49 +02:00
1ed5690b33 Feature/improve queue jobs implementation (#1855)
* Improve queue jobs implementation

* Update changelog
2023-04-14 19:57:23 +02:00
4451514ec5 Release 1.253.0 (#1854) 2023-04-14 07:01:34 +02:00
8f73f85276 Bugfix/fix background color of dialogs in dark mode (#1853)
* Fix background color

* Update changelog
2023-04-13 08:47:19 +02:00
9a5d7b664b Release 1.252.2 (#1852) 2023-04-11 18:06:34 +02:00
7d2d1d971a Feature/deprecate get auth endpoint (#1851)
* Deprecate GET auth endpoint

* Update documentation

* Update changelog
2023-04-11 18:04:18 +02:00
d111493eed Release 1.252.1 (#1849) 2023-04-10 20:52:34 +02:00
e975f92a96 Release 1.252.0 (#1848) 2023-04-10 18:03:02 +02:00
739cb4242d Feature/decrease density of theme (#1846)
* Decrease density

* Update changelog
2023-04-10 17:59:29 +02:00
a37eebc9f1 Feature/upgrade nestjs from version 9.1.4 to 9.4.0 (#1843)
* Upgrade nestjs from version 9.1.4 to 9.4.0

* Update changelog
2023-04-10 13:50:27 +02:00
e92730879e Feature/migrate dialog components to angular material 15 (#1844)
* Migrate MatDialog

* Update changelog
2023-04-10 13:49:53 +02:00
464973f9b0 Feature/migrate form components to angular material 15 (#1842)
* Upgrade @angular/cdk

* Upgrade form components to Angular Material 15

* Update changelog
2023-04-10 10:59:44 +02:00
f6228c099f Migrate MatRadio (#1841) 2023-04-10 09:23:16 +02:00
a57fdfb2bb Feature/migrate slide toggle components to angular material 15 (#1840)
* Upgrade @angular/material

* Change MatSlideToggle to MatCheckbox

* Update changelog
2023-04-09 09:33:36 +02:00
24716f0561 Migrate checkbox and chips components to Angular Material 15 (#1839) 2023-04-09 08:55:09 +02:00
3453100afd Feature/migrate various components to angular material 15 part 2 (#1838)
* Migrate tooltips to Angular Material 15

* Migrate tabs to Angular Material 15
2023-04-09 08:54:32 +02:00
84de2c0c68 Migrate card components to Angular Material 15 (#1837) 2023-04-08 17:08:30 +02:00
1b7b082003 Feature/migrate various components to angular material 15 (#1836)
* Migrate components to Angular Material 15

* Update changelog
2023-04-08 15:33:27 +02:00
1928c2c2cc Release 1.251.0 (#1834) 2023-04-07 19:33:26 +02:00
52e7a7886d Feature/migrate libs components to angular material 15 (#1833)
* Migrate to Angular Material 15

* Update changelog
2023-04-07 17:10:03 +02:00
36298b217e Feature/improve tick abbreviation function (#1828)
* Keep the value if value smaller than 1000

* Update changelog
2023-04-07 17:09:34 +02:00
9bce57894e Feature/increase historical market data gathering to 10 years (#1830)
* Increase historical market data gathering of currency pairs to 10+ years

* Update changelog
2023-04-07 16:35:37 +02:00
9d6bb325cd Improve initialization (#1832) 2023-04-07 16:33:55 +02:00
a5f833c612 Feature/upgrade angular and nx 20230405 (#1826)
* Upgrade angular and Nx

* Update changelog
2023-04-06 19:18:23 +02:00
732b14c6ab Feature/improve activities import for csv files of ibkr (#1824)
* Improve import for csv files by Interactive Brokers

* Update changelog
2023-04-05 20:09:00 +02:00
b74a042da8 Feature/change auth endpoint from get to post (#1823)
* Change auth endpoint from GET to POST
  * Login with security token
  * Login with Internet Identity

* Update changelog
2023-04-05 18:10:29 +02:00
d55c052f57 Feature/improve content of pricing and faq pages (#1822)
* Improve content

* Update changelog
2023-04-03 17:39:32 +02:00
864f585efa Release/1.250.0 (#1821) 2023-04-02 09:47:13 +02:00
6d56146054 Feature/add support for multiple subscription offers (#1818)
* Setup for multiple subscription offers

* Update changelog
2023-04-02 09:44:13 +02:00
2c9f29a3c6 Feature/improve handling of platforms in accounts import (#1820)
* Improve handling of platforms

* Fix issue with pagination

* Update changelog
2023-04-01 10:51:55 +02:00
9bef2e960c Feature/ignore first item in portfolio evolution chart (#1816)
* Ignore first item in portfolio evolution chart

* Update changelog
2023-04-01 10:29:39 +02:00
17b8c41673 Add test file (#1810) 2023-03-30 08:24:44 +02:00
f0afbd7346 Release 1.249.0 (#1815) 2023-03-27 20:38:46 +02:00
5dc7429f6a Feature/add testimonials (#1814)
* Add testimonials

* Update changelog
2023-03-27 20:36:49 +02:00
7b39b32293 Feature/improve allocations page (#1813)
* Improve loading state

* Update changelog
2023-03-26 17:18:48 +02:00
e5b5a9e7e9 Feature/improve language localization for german (#1809)
* Improve language localization

* Update changelog
2023-03-26 17:17:48 +02:00
1f3511368a Feature/always show label in value component (#1812)
* Always show label while loading

* Update changelog
2023-03-26 16:59:52 +02:00
b37df2c84f Bugfix/fix algebraic sign in value component (#1811)
* Fix algebraic sign by resetting member variables

* Update changelog
2023-03-26 16:16:24 +02:00
f92ba54060 Release/1.248.0 (#1808) 2023-03-25 17:45:51 +01:00
a3bbd4030e Improve blog post and add images (#1807) 2023-03-25 17:42:41 +01:00
4b30da2d92 Feature/conditionally hide platform selector (#1805)
* Hide platform selector conditionally

* Update changelog
2023-03-25 16:46:46 +01:00
93d082afbb Feature/add blog post 1000 stars on GitHub (#1804)
* Add blog post: 1000 Stars on GitHub

* Add breadcrumb navigation

* Update changelog
2023-03-25 14:28:06 +01:00
0c85380dbf Feature/refactor portfolio calculator (#1803)
* Refactor chart calculation in portfolio calculator

* Update changelog
2023-03-25 12:20:42 +01:00
fb576376dc Feature/upgrade ng extract i18n merge (#1802)
* Upgrade ng-extract-i18n-merge

* Extract locales

* Update changelog
2023-03-24 17:34:27 +01:00
ff111d4c6c Release 1.247.0 (#1801) 2023-03-23 19:26:01 +01:00
bc6e9a8b68 Bugfix/fix total amount calculation in portfolio evolution chart (#1799)
* Fix total amount calculation

* Update changelog
2023-03-23 19:23:31 +01:00
bd1963ec26 Feature/add asset and asset sub class to search endpoint (#1795)
* Add asset and asset sub class to search endpoint

* Update changelog
2023-03-23 19:11:38 +01:00
a0bec9e97f Feature/remove mail address part 2 (#1800)
* Remove mail address and update Slack url

* Update changelog
2023-03-23 14:14:31 +01:00
c45df20d88 Sort imports (#1797) 2023-03-21 19:34:40 +01:00
fa1d669633 Feature/upgrade prisma to version 4.11.0 (#1798)
* Upgrade prisma to version 4.11.0

* Update changelog
2023-03-20 20:08:08 +01:00
1009b462e9 Feature/add subscription expiration dates to the admin control panel (#1796)
* Add expiration date

* Update changelog
2023-03-19 12:03:47 +01:00
b404858904 Release 1.246.0 (#1794) 2023-03-18 10:36:40 +01:00
7ec033577f Feature/extend trackinsight data enhancer by isin (#1793)
* Extend data enhancer by isin

* Update changelog
2023-03-18 10:34:50 +01:00
c8ca82b803 Feature/extend data source eod historical data by asset class and isin (#1791)
* Extend EodHistoricalDataService

* asset and asset sub class
* isin

* Update changelog
2023-03-18 10:09:11 +01:00
5db2faa17d Bugfix/fix border color in fire calculator (#1792)
* Fix border color

* Update changelog
2023-03-17 19:37:36 +01:00
1605fb8d48 Feature/improve language localization for data gathering (#1790)
* Improve locales

* Update changelog
2023-03-16 20:54:34 +01:00
b6a7804a26 Refactoring (#1784) 2023-03-14 10:46:11 +01:00
de31381fd9 Release 1.245.0 (#1787) 2023-03-12 14:33:00 +01:00
0d92b8d8bb Reduce search requests (#1786) 2023-03-12 14:29:22 +01:00
7c6ff776d9 Feature/add search functionality for eod historical data (#1783)
* Add search functionality for EOD_HISTORICAL_DATA

* Update changelog
2023-03-12 13:13:34 +01:00
e37a34ed6c Feature/improve exchange rate service for specific date (#1785)
* Calculate exchange rate indirectly via base currency

* Update changelog
2023-03-12 12:19:13 +01:00
c4d9c00f92 Feature/upgrade ngx device detector to version 5.0.1 (#1782)
* Upgrade ngx-device-detector to version 5.0.1

* Update changelog
2023-03-12 10:14:35 +01:00
3af8be89e3 Feature/improve usability of fire calculator (#1779)
* Improve usability

* Add debounce
* Persist annualInterestRate
* Partially disable date picker

* Update changelog
2023-03-12 09:55:55 +01:00
245 changed files with 5694 additions and 4139 deletions

View File

@ -5,6 +5,147 @@ 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/), 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). and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
## 1.254.0 - 2023-04-14
### Changed
- Improved the queue jobs implementation by adding in bulk
- Improved the queue jobs implementation by introducing unique job ids
## 1.253.0 - 2023-04-14
### Changed
- Reduced the execution interval of the data gathering to every 12 hours
### Fixed
- Fixed the background color of dialogs in dark mode
## 1.252.2 - 2023-04-11
### Changed
- Deprecated the `auth` endpoint of the login with _Security Token_ (`GET`)
## 1.252.1 - 2023-04-10
### Changed
- Changed the slide toggles to checkboxes on the account page
- Changed the slide toggles to checkboxes in the admin control panel
- Decreased the density of the theme
- Migrated the style of various components to `@angular/material` `15` (mdc)
- Upgraded `@angular/cdk` and `@angular/material` from version `15.2.5` to `15.2.6`
- Upgraded `bull` from version `4.10.2` to `4.10.4`
## 1.251.0 - 2023-04-07
### Changed
- Improved the activities import for `csv` files exported by _Interactive Brokers_
- Improved the rendering of the chart ticks (`0.5K``500`)
- Increased the historical market data gathering of currency pairs to 10+ years
- Improved the content of the Frequently Asked Questions (FAQ) page
- Improved the content of the pricing page
- Changed the `auth` endpoint of the login with _Security Token_ from `GET` to `POST`
- Changed the `auth` endpoint of the _Internet Identity_ login provider from `GET` to `POST`
- Migrated the style of the `libs` components to `@angular/material` `15` (mdc)
- `ActivitiesFilterComponent`
- `ActivitiesTableComponent`
- `BenchmarkComponent`
- `HoldingsTableComponent`
- Upgraded `angular` from version `15.1.5` to `15.2.5`
- Upgraded `Nx` from version `15.7.2` to `15.9.2`
## 1.250.0 - 2023-04-02
### Added
- Added support for multiple subscription offers
### Changed
- Improved the portfolio evolution chart (ignore first item)
- Improved the accounts import by handling the platform
### Fixed
- Fixed an issue with more than 50 activities in the activities import (`dryRun`)
## 1.249.0 - 2023-03-27
### Added
- Extended the testimonial section on the landing page
### Changed
- Improved the loading state of the value component on the allocations page
- Improved the value component by always showing the label (also while loading)
- Improved the language localization for German (`de`)
### Fixed
- Fixed an issue with the algebraic sign in the value component
## 1.248.0 - 2023-03-25
### Added
- Added a blog post: _Ghostfolio reaches 1000 Stars on GitHub_
- Added a breadcrumb navigation to the blog post pages
### Changed
- Refactored the calculation of the chart
- Hid the platform selector if no platforms are available in the create or update account dialog
- Upgraded `ng-extract-i18n-merge` from version `2.5.0` to `2.6.0`
## 1.247.0 - 2023-03-23
### Added
- Added the asset and asset sub class to the search functionality
- Added the subscription expiration date to the users table of the admin control panel
### Changed
- Updated the URL of the Ghostfolio Slack channel
- Upgraded `prisma` from version `4.10.1` to `4.11.0`
### Fixed
- Fixed the total amount calculation in the portfolio evolution chart
## 1.246.0 - 2023-03-18
### Added
- Added support for asset and asset sub class to the `EOD_HISTORICAL_DATA` data source type
- Added `isin` to the asset profile model
### Changed
- Extended the _Trackinsight_ data enhancer for asset profile data by `isin`
- Improved the language localization for _Gather Data_
### Fixed
- Fixed the border color in the _FIRE_ calculator (dark mode)
## 1.245.0 - 2023-03-12
### Added
- Added the search functionality for the `EOD_HISTORICAL_DATA` data source type
### Changed
- Improved the usability of the _FIRE_ calculator
- Improved the exchange rate service for a specific date used in activities with a manual currency
- Upgraded `ngx-device-detector` from version `3.0.0` to `5.0.1`
## 1.244.0 - 2023-03-09 ## 1.244.0 - 2023-03-09
### Added ### Added
@ -166,7 +307,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
### Added ### Added
- Added support to export accounts - Added support to export accounts
- Added suport to import accounts - Added support to import accounts
### Changed ### Changed

View File

@ -200,7 +200,9 @@ Set the header for each request as follows:
"Authorization": "Bearer eyJh..." "Authorization": "Bearer eyJh..."
``` ```
You can get the _Bearer Token_ via `GET http://localhost:3333/api/v1/auth/anonymous/<INSERT_SECURITY_TOKEN_OF_ACCOUNT>` or `curl -s http://localhost:3333/api/v1/auth/anonymous/<INSERT_SECURITY_TOKEN_OF_ACCOUNT>`. You can get the _Bearer Token_ via `POST http://localhost:3333/api/v1/auth/anonymous` (Body: `{ accessToken: <INSERT_SECURITY_TOKEN_OF_ACCOUNT> }`)
Deprecated: `GET http://localhost:3333/api/v1/auth/anonymous/<INSERT_SECURITY_TOKEN_OF_ACCOUNT>` or `curl -s http://localhost:3333/api/v1/auth/anonymous/<INSERT_SECURITY_TOKEN_OF_ACCOUNT>`.
### Import Activities ### Import Activities

View File

@ -2,13 +2,14 @@
export default { export default {
displayName: 'api', displayName: 'api',
globals: { globals: {},
'ts-jest': { transform: {
'^.+\\.[tj]s$': [
'ts-jest',
{
tsconfig: '<rootDir>/tsconfig.spec.json' tsconfig: '<rootDir>/tsconfig.spec.json'
} }
}, ]
transform: {
'^.+\\.[tj]s$': 'ts-jest'
}, },
moduleFileExtensions: ['ts', 'js', 'html'], moduleFileExtensions: ['ts', 'js', 'html'],
coverageDirectory: '../../coverage/apps/api', coverageDirectory: '../../coverage/apps/api',

View File

@ -107,7 +107,10 @@ export class AdminController {
dataSource, dataSource,
symbol symbol
}, },
GATHER_ASSET_PROFILE_PROCESS_OPTIONS {
...GATHER_ASSET_PROFILE_PROCESS_OPTIONS,
jobId: `${dataSource}-${symbol}}`
}
); );
} }
@ -138,7 +141,10 @@ export class AdminController {
dataSource, dataSource,
symbol symbol
}, },
GATHER_ASSET_PROFILE_PROCESS_OPTIONS {
...GATHER_ASSET_PROFILE_PROCESS_OPTIONS,
jobId: `${dataSource}-${symbol}}`
}
); );
} }
} }
@ -167,7 +173,10 @@ export class AdminController {
dataSource, dataSource,
symbol symbol
}, },
GATHER_ASSET_PROFILE_PROCESS_OPTIONS {
...GATHER_ASSET_PROFILE_PROCESS_OPTIONS,
jobId: `${dataSource}-${symbol}}`
}
); );
} }

View File

@ -100,6 +100,7 @@ export class AdminService {
dataSource, dataSource,
marketDataItemCount, marketDataItemCount,
symbol, symbol,
assetClass: 'CASH',
countriesCount: 0, countriesCount: 0,
sectorsCount: 0 sectorsCount: 0
}; };
@ -186,8 +187,11 @@ export class AdminService {
]); ]);
return { return {
assetProfile, marketData,
marketData assetProfile: assetProfile ?? {
symbol,
currency: '-'
}
}; };
} }

View File

@ -33,8 +33,11 @@ export class AuthController {
private readonly webAuthService: WebAuthService private readonly webAuthService: WebAuthService
) {} ) {}
/**
* @deprecated
*/
@Get('anonymous/:accessToken') @Get('anonymous/:accessToken')
public async accessTokenLogin( public async accessTokenLoginGet(
@Param('accessToken') accessToken: string @Param('accessToken') accessToken: string
): Promise<OAuthResponse> { ): Promise<OAuthResponse> {
try { try {
@ -50,6 +53,23 @@ export class AuthController {
} }
} }
@Post('anonymous')
public async accessTokenLogin(
@Body() body: { accessToken: string }
): Promise<OAuthResponse> {
try {
const authToken = await this.authService.validateAnonymousLogin(
body.accessToken
);
return { authToken };
} catch {
throw new HttpException(
getReasonPhrase(StatusCodes.FORBIDDEN),
StatusCodes.FORBIDDEN
);
}
}
@Get('google') @Get('google')
@UseGuards(AuthGuard('google')) @UseGuards(AuthGuard('google'))
public googleLogin() { public googleLogin() {
@ -81,13 +101,13 @@ export class AuthController {
} }
} }
@Get('internet-identity/:principalId') @Post('internet-identity')
public async internetIdentityLogin( public async internetIdentityLogin(
@Param('principalId') principalId: string @Body() body: { principalId: string }
): Promise<OAuthResponse> { ): Promise<OAuthResponse> {
try { try {
const authToken = await this.authService.validateInternetIdentityLogin( const authToken = await this.authService.validateInternetIdentityLogin(
principalId body.principalId
); );
return { authToken }; return { authToken };
} catch { } catch {

View File

@ -87,6 +87,13 @@ export class FrontendMiddleware implements NestMiddleware {
) { ) {
featureGraphicPath = 'assets/images/blog/ghostfolio-x-umbrel.png'; featureGraphicPath = 'assets/images/blog/ghostfolio-x-umbrel.png';
title = `Ghostfolio meets Umbrel - ${title}`; title = `Ghostfolio meets Umbrel - ${title}`;
} else if (
request.path.startsWith(
'/en/blog/2023/03/ghostfolio-reaches-1000-stars-on-github'
)
) {
featureGraphicPath = 'assets/images/blog/1000-stars-on-github.jpg';
title = `Ghostfolio reaches 1000 Stars on GitHub - ${title}`;
} }
if ( if (

View File

@ -13,6 +13,7 @@ import { Module } from '@nestjs/common';
import { ImportController } from './import.controller'; import { ImportController } from './import.controller';
import { ImportService } from './import.service'; import { ImportService } from './import.service';
import { PlatformModule } from '@ghostfolio/api/services/platform/platform.module';
@Module({ @Module({
controllers: [ImportController], controllers: [ImportController],
@ -24,6 +25,7 @@ import { ImportService } from './import.service';
DataProviderModule, DataProviderModule,
ExchangeRateDataModule, ExchangeRateDataModule,
OrderModule, OrderModule,
PlatformModule,
PortfolioModule, PortfolioModule,
PrismaModule, PrismaModule,
RedisCacheModule, RedisCacheModule,

View File

@ -6,6 +6,7 @@ import { OrderService } from '@ghostfolio/api/app/order/order.service';
import { PortfolioService } from '@ghostfolio/api/app/portfolio/portfolio.service'; import { PortfolioService } from '@ghostfolio/api/app/portfolio/portfolio.service';
import { DataProviderService } from '@ghostfolio/api/services/data-provider/data-provider.service'; import { DataProviderService } from '@ghostfolio/api/services/data-provider/data-provider.service';
import { ExchangeRateDataService } from '@ghostfolio/api/services/exchange-rate-data.service'; import { ExchangeRateDataService } from '@ghostfolio/api/services/exchange-rate-data.service';
import { PlatformService } from '@ghostfolio/api/services/platform/platform.service';
import { SymbolProfileService } from '@ghostfolio/api/services/symbol-profile.service'; import { SymbolProfileService } from '@ghostfolio/api/services/symbol-profile.service';
import { parseDate } from '@ghostfolio/common/helper'; import { parseDate } from '@ghostfolio/common/helper';
import { UniqueAsset } from '@ghostfolio/common/interfaces'; import { UniqueAsset } from '@ghostfolio/common/interfaces';
@ -14,7 +15,7 @@ import {
OrderWithAccount OrderWithAccount
} from '@ghostfolio/common/types'; } from '@ghostfolio/common/types';
import { Injectable } from '@nestjs/common'; import { Injectable } from '@nestjs/common';
import { SymbolProfile } from '@prisma/client'; import { Prisma, SymbolProfile } from '@prisma/client';
import Big from 'big.js'; import Big from 'big.js';
import { endOfToday, isAfter, isSameDay, parseISO } from 'date-fns'; import { endOfToday, isAfter, isSameDay, parseISO } from 'date-fns';
import { v4 as uuidv4 } from 'uuid'; import { v4 as uuidv4 } from 'uuid';
@ -26,6 +27,7 @@ export class ImportService {
private readonly dataProviderService: DataProviderService, private readonly dataProviderService: DataProviderService,
private readonly exchangeRateDataService: ExchangeRateDataService, private readonly exchangeRateDataService: ExchangeRateDataService,
private readonly orderService: OrderService, private readonly orderService: OrderService,
private readonly platformService: PlatformService,
private readonly portfolioService: PortfolioService, private readonly portfolioService: PortfolioService,
private readonly symbolProfileService: SymbolProfileService private readonly symbolProfileService: SymbolProfileService
) {} ) {}
@ -118,7 +120,8 @@ export class ImportService {
const accountIdMapping: { [oldAccountId: string]: string } = {}; const accountIdMapping: { [oldAccountId: string]: string } = {};
if (!isDryRun && accountsDto?.length) { if (!isDryRun && accountsDto?.length) {
const existingAccounts = await this.accountService.accounts({ const [existingAccounts, existingPlatforms] = await Promise.all([
this.accountService.accounts({
where: { where: {
id: { id: {
in: accountsDto.map(({ id }) => { in: accountsDto.map(({ id }) => {
@ -126,7 +129,9 @@ export class ImportService {
}) })
} }
} }
}); }),
this.platformService.get()
]);
for (const account of accountsDto) { for (const account of accountsDto) {
// Check if there is any existing account with the same ID // Check if there is any existing account with the same ID
@ -146,19 +151,24 @@ export class ImportService {
delete account.id; delete account.id;
} }
const newAccountObject = { let accountObject: Prisma.AccountCreateInput = {
...account, ...account,
User: { connect: { id: userId } } User: { connect: { id: userId } }
}; };
if (platformId) { if (
Object.assign(newAccountObject, { existingPlatforms.some(({ id }) => {
return id === platformId;
})
) {
accountObject = {
...accountObject,
Platform: { connect: { id: platformId } } Platform: { connect: { id: platformId } }
}); };
} }
const newAccount = await this.accountService.createAccount( const newAccount = await this.accountService.createAccount(
newAccountObject, accountObject,
userId userId
); );
@ -254,6 +264,7 @@ export class ImportService {
countries: null, countries: null,
createdAt: undefined, createdAt: undefined,
id: undefined, id: undefined,
isin: null,
name: null, name: null,
scraperConfiguration: null, scraperConfiguration: null,
sectors: null, sectors: null,

View File

@ -22,6 +22,7 @@ import { InfoItem } from '@ghostfolio/common/interfaces';
import { Statistics } from '@ghostfolio/common/interfaces/statistics.interface'; import { Statistics } from '@ghostfolio/common/interfaces/statistics.interface';
import { Subscription } from '@ghostfolio/common/interfaces/subscription.interface'; import { Subscription } from '@ghostfolio/common/interfaces/subscription.interface';
import { permissions } from '@ghostfolio/common/permissions'; import { permissions } from '@ghostfolio/common/permissions';
import { SubscriptionOffer } from '@ghostfolio/common/types';
import { Injectable, Logger } from '@nestjs/common'; import { Injectable, Logger } from '@nestjs/common';
import { JwtService } from '@nestjs/jwt'; import { JwtService } from '@nestjs/jwt';
import * as bent from 'bent'; import * as bent from 'bent';
@ -304,19 +305,17 @@ export class InfoService {
return statistics; return statistics;
} }
private async getSubscriptions(): Promise<Subscription[]> { private async getSubscriptions(): Promise<{
[offer in SubscriptionOffer]: Subscription;
}> {
if (!this.configurationService.get('ENABLE_FEATURE_SUBSCRIPTION')) { if (!this.configurationService.get('ENABLE_FEATURE_SUBSCRIPTION')) {
return undefined; return undefined;
} }
let subscriptions: Subscription[] = [];
const stripeConfig = (await this.prismaService.property.findUnique({ const stripeConfig = (await this.prismaService.property.findUnique({
where: { key: PROPERTY_STRIPE_CONFIG } where: { key: PROPERTY_STRIPE_CONFIG }
})) ?? { value: '{}' }; })) ?? { value: '{}' };
subscriptions = [JSON.parse(stripeConfig.value)]; return JSON.parse(stripeConfig.value);
return subscriptions;
} }
} }

View File

@ -118,7 +118,10 @@ export class OrderService {
dataSource: data.SymbolProfile.connectOrCreate.create.dataSource, dataSource: data.SymbolProfile.connectOrCreate.create.dataSource,
symbol: data.SymbolProfile.connectOrCreate.create.symbol symbol: data.SymbolProfile.connectOrCreate.create.symbol
}, },
GATHER_ASSET_PROFILE_PROCESS_OPTIONS {
...GATHER_ASSET_PROFILE_PROCESS_OPTIONS,
jobId: `${data.SymbolProfile.connectOrCreate.create.dataSource}-${data.SymbolProfile.connectOrCreate.create.symbol}}`
}
); );
const isDraft = isAfter(data.date as Date, endOfToday()); const isDraft = isAfter(data.date as Date, endOfToday());

View File

@ -86,7 +86,7 @@ describe('PortfolioCalculator', () => {
netPerformanceInPercentage: 13.100263852242744, netPerformanceInPercentage: 13.100263852242744,
netPerformance: 19.86, netPerformance: 19.86,
totalInvestment: 0, totalInvestment: 0,
value: 19.86 value: 0
}); });
expect(currentPositions).toEqual({ expect(currentPositions).toEqual({

View File

@ -182,10 +182,10 @@ export class PortfolioCalculator {
return isBefore(parseDate(transactionPoint.date), end); return isBefore(parseDate(transactionPoint.date), end);
}) ?? []; }) ?? [];
const firstIndex = transactionPointsBeforeEndDate.length; const currencies: { [symbol: string]: string } = {};
const dates: Date[] = []; const dates: Date[] = [];
const dataGatheringItems: IDataGatheringItem[] = []; const dataGatheringItems: IDataGatheringItem[] = [];
const currencies: { [symbol: string]: string } = {}; const firstIndex = transactionPointsBeforeEndDate.length;
let day = start; let day = start;
@ -235,25 +235,31 @@ export class PortfolioCalculator {
} }
} }
const netPerformanceValuesBySymbol: { const valuesByDate: {
[symbol: string]: { [date: string]: Big }; [date: string]: {
maxTotalInvestmentValue: Big;
totalCurrentValue: Big;
totalInvestmentValue: Big;
totalNetPerformanceValue: Big;
};
} = {}; } = {};
const investmentValuesBySymbol: { const valuesBySymbol: {
[symbol: string]: { [date: string]: Big }; [symbol: string]: {
currentValues: { [date: string]: Big };
investmentValues: { [date: string]: Big };
maxInvestmentValues: { [date: string]: Big };
netPerformanceValues: { [date: string]: Big };
};
} = {}; } = {};
const maxInvestmentValuesBySymbol: {
[symbol: string]: { [date: string]: Big };
} = {};
const totalNetPerformanceValues: { [date: string]: Big } = {};
const totalInvestmentValues: { [date: string]: Big } = {};
const maxTotalInvestmentValues: { [date: string]: Big } = {};
for (const symbol of Object.keys(symbols)) { for (const symbol of Object.keys(symbols)) {
const { investmentValues, maxInvestmentValues, netPerformanceValues } = const {
this.getSymbolMetrics({ currentValues,
investmentValues,
maxInvestmentValues,
netPerformanceValues
} = this.getSymbolMetrics({
end, end,
marketSymbolMap, marketSymbolMap,
start, start,
@ -262,60 +268,67 @@ export class PortfolioCalculator {
isChartMode: true isChartMode: true
}); });
netPerformanceValuesBySymbol[symbol] = netPerformanceValues; valuesBySymbol[symbol] = {
investmentValuesBySymbol[symbol] = investmentValues; currentValues,
maxInvestmentValuesBySymbol[symbol] = maxInvestmentValues; investmentValues,
maxInvestmentValues,
netPerformanceValues
};
} }
for (const currentDate of dates) { for (const currentDate of dates) {
const dateString = format(currentDate, DATE_FORMAT); const dateString = format(currentDate, DATE_FORMAT);
for (const symbol of Object.keys(netPerformanceValuesBySymbol)) { for (const symbol of Object.keys(valuesBySymbol)) {
totalNetPerformanceValues[dateString] = const symbolValues = valuesBySymbol[symbol];
totalNetPerformanceValues[dateString] ?? new Big(0);
if (netPerformanceValuesBySymbol[symbol]?.[dateString]) { const currentValue =
totalNetPerformanceValues[dateString] = totalNetPerformanceValues[ symbolValues.currentValues?.[dateString] ?? new Big(0);
dateString const investmentValue =
].add(netPerformanceValuesBySymbol[symbol][dateString]); symbolValues.investmentValues?.[dateString] ?? new Big(0);
} const maxInvestmentValue =
symbolValues.maxInvestmentValues?.[dateString] ?? new Big(0);
const netPerformanceValue =
symbolValues.netPerformanceValues?.[dateString] ?? new Big(0);
totalInvestmentValues[dateString] = valuesByDate[dateString] = {
totalInvestmentValues[dateString] ?? new Big(0); totalCurrentValue: (
valuesByDate[dateString]?.totalCurrentValue ?? new Big(0)
maxTotalInvestmentValues[dateString] = ).add(currentValue),
maxTotalInvestmentValues[dateString] ?? new Big(0); totalInvestmentValue: (
valuesByDate[dateString]?.totalInvestmentValue ?? new Big(0)
if (investmentValuesBySymbol[symbol]?.[dateString]) { ).add(investmentValue),
totalInvestmentValues[dateString] = totalInvestmentValues[ maxTotalInvestmentValue: (
dateString valuesByDate[dateString]?.maxTotalInvestmentValue ?? new Big(0)
].add(investmentValuesBySymbol[symbol][dateString]); ).add(maxInvestmentValue),
} totalNetPerformanceValue: (
valuesByDate[dateString]?.totalNetPerformanceValue ?? new Big(0)
if (maxInvestmentValuesBySymbol[symbol]?.[dateString]) { ).add(netPerformanceValue)
maxTotalInvestmentValues[dateString] = maxTotalInvestmentValues[ };
dateString
].add(maxInvestmentValuesBySymbol[symbol][dateString]);
}
} }
} }
return Object.keys(totalNetPerformanceValues).map((date) => { return Object.entries(valuesByDate).map(([date, values]) => {
const netPerformanceInPercentage = maxTotalInvestmentValues[date].eq(0) const {
maxTotalInvestmentValue,
totalCurrentValue,
totalInvestmentValue,
totalNetPerformanceValue
} = values;
const netPerformanceInPercentage = maxTotalInvestmentValue.eq(0)
? 0 ? 0
: totalNetPerformanceValues[date] : totalNetPerformanceValue
.div(maxTotalInvestmentValues[date]) .div(maxTotalInvestmentValue)
.mul(100) .mul(100)
.toNumber(); .toNumber();
return { return {
date, date,
netPerformanceInPercentage, netPerformanceInPercentage,
netPerformance: totalNetPerformanceValues[date].toNumber(), netPerformance: totalNetPerformanceValue.toNumber(),
totalInvestment: totalInvestmentValues[date].toNumber(), totalInvestment: totalInvestmentValue.toNumber(),
value: totalInvestmentValues[date] value: totalCurrentValue.toNumber()
.plus(totalNetPerformanceValues[date])
.toNumber()
}; };
}); });
} }
@ -906,12 +919,16 @@ export class PortfolioCalculator {
if (orders.length <= 0) { if (orders.length <= 0) {
return { return {
currentValues: {},
grossPerformance: new Big(0),
grossPerformancePercentage: new Big(0),
hasErrors: false, hasErrors: false,
initialValue: new Big(0), initialValue: new Big(0),
investmentValues: {},
maxInvestmentValues: {},
netPerformance: new Big(0), netPerformance: new Big(0),
netPerformancePercentage: new Big(0), netPerformancePercentage: new Big(0),
grossPerformance: new Big(0), netPerformanceValues: {}
grossPerformancePercentage: new Big(0)
}; };
} }
@ -946,6 +963,7 @@ export class PortfolioCalculator {
let grossPerformanceFromSells = new Big(0); let grossPerformanceFromSells = new Big(0);
let initialValue: Big; let initialValue: Big;
let investmentAtStartDate: Big; let investmentAtStartDate: Big;
const currentValues: { [date: string]: Big } = {};
const investmentValues: { [date: string]: Big } = {}; const investmentValues: { [date: string]: Big } = {};
const maxInvestmentValues: { [date: string]: Big } = {}; const maxInvestmentValues: { [date: string]: Big } = {};
let lastAveragePrice = new Big(0); let lastAveragePrice = new Big(0);
@ -1164,6 +1182,7 @@ export class PortfolioCalculator {
} }
if (isChartMode && i > indexOfStartOrder) { if (isChartMode && i > indexOfStartOrder) {
currentValues[order.date] = valueOfInvestment;
netPerformanceValues[order.date] = grossPerformance netPerformanceValues[order.date] = grossPerformance
.minus(grossPerformanceAtStartDate) .minus(grossPerformanceAtStartDate)
.minus(fees.minus(feesAtStartDate)); .minus(fees.minus(feesAtStartDate));
@ -1261,15 +1280,16 @@ export class PortfolioCalculator {
} }
return { return {
initialValue, currentValues,
grossPerformancePercentage, grossPerformancePercentage,
initialValue,
investmentValues, investmentValues,
maxInvestmentValues, maxInvestmentValues,
netPerformancePercentage, netPerformancePercentage,
netPerformanceValues, netPerformanceValues,
grossPerformance: totalGrossPerformance,
hasErrors: totalUnits.gt(0) && (!initialValue || !unitPriceAtEndDate), hasErrors: totalUnits.gt(0) && (!initialValue || !unitPriceAtEndDate),
netPerformance: totalNetPerformance, netPerformance: totalNetPerformance
grossPerformance: totalGrossPerformance
}; };
} }

View File

@ -37,8 +37,7 @@ import {
PortfolioSummary, PortfolioSummary,
Position, Position,
TimelinePosition, TimelinePosition,
UserSettings, UserSettings
UserWithSettings
} from '@ghostfolio/common/interfaces'; } from '@ghostfolio/common/interfaces';
import { InvestmentItem } from '@ghostfolio/common/interfaces/investment-item.interface'; import { InvestmentItem } from '@ghostfolio/common/interfaces/investment-item.interface';
import type { import type {
@ -47,7 +46,8 @@ import type {
GroupBy, GroupBy,
Market, Market,
OrderWithAccount, OrderWithAccount,
RequestWithUser RequestWithUser,
UserWithSettings
} from '@ghostfolio/common/types'; } from '@ghostfolio/common/types';
import { Inject, Injectable } from '@nestjs/common'; import { Inject, Injectable } from '@nestjs/common';
import { REQUEST } from '@nestjs/core'; import { REQUEST } from '@nestjs/core';

View File

@ -4,9 +4,9 @@ import {
DEFAULT_LANGUAGE_CODE, DEFAULT_LANGUAGE_CODE,
PROPERTY_STRIPE_CONFIG PROPERTY_STRIPE_CONFIG
} from '@ghostfolio/common/config'; } from '@ghostfolio/common/config';
import { UserWithSettings } from '@ghostfolio/common/interfaces';
import { Subscription as SubscriptionInterface } from '@ghostfolio/common/interfaces/subscription.interface'; import { Subscription as SubscriptionInterface } from '@ghostfolio/common/interfaces/subscription.interface';
import { SubscriptionType } from '@ghostfolio/common/types/subscription.type'; import { UserWithSettings } from '@ghostfolio/common/types';
import { SubscriptionType } from '@ghostfolio/common/types/subscription-type.type';
import { Injectable, Logger } from '@nestjs/common'; import { Injectable, Logger } from '@nestjs/common';
import { Subscription } from '@prisma/client'; import { Subscription } from '@prisma/client';
import { addMilliseconds, isBefore } from 'date-fns'; import { addMilliseconds, isBefore } from 'date-fns';
@ -123,7 +123,9 @@ export class SubscriptionService {
} }
} }
public getSubscription(aSubscriptions: Subscription[]) { public getSubscription(
aSubscriptions: Subscription[]
): UserWithSettings['subscription'] {
if (aSubscriptions.length > 0) { if (aSubscriptions.length > 0) {
const latestSubscription = aSubscriptions.reduce((a, b) => { const latestSubscription = aSubscriptions.reduce((a, b) => {
return new Date(a.expiresAt) > new Date(b.expiresAt) ? a : b; return new Date(a.expiresAt) > new Date(b.expiresAt) ? a : b;
@ -131,12 +133,14 @@ export class SubscriptionService {
return { return {
expiresAt: latestSubscription.expiresAt, expiresAt: latestSubscription.expiresAt,
offer: latestSubscription.price === 0 ? 'default' : 'renewal',
type: isBefore(new Date(), latestSubscription.expiresAt) type: isBefore(new Date(), latestSubscription.expiresAt)
? SubscriptionType.Premium ? SubscriptionType.Premium
: SubscriptionType.Basic : SubscriptionType.Basic
}; };
} else { } else {
return { return {
offer: 'default',
type: SubscriptionType.Basic type: SubscriptionType.Basic
}; };
} }

View File

@ -1,6 +1,8 @@
import { DataSource } from '@prisma/client'; import { AssetClass, AssetSubClass, DataSource } from '@prisma/client';
export interface LookupItem { export interface LookupItem {
assetClass: AssetClass;
assetSubClass: AssetSubClass;
currency: string; currency: string;
dataSource: DataSource; dataSource: DataSource;
name: string; name: string;

View File

@ -1,15 +1,18 @@
import { TransformDataSourceInRequestInterceptor } from '@ghostfolio/api/interceptors/transform-data-source-in-request.interceptor'; import { TransformDataSourceInRequestInterceptor } from '@ghostfolio/api/interceptors/transform-data-source-in-request.interceptor';
import { TransformDataSourceInResponseInterceptor } from '@ghostfolio/api/interceptors/transform-data-source-in-response.interceptor'; import { TransformDataSourceInResponseInterceptor } from '@ghostfolio/api/interceptors/transform-data-source-in-response.interceptor';
import { IDataProviderHistoricalResponse } from '@ghostfolio/api/services/interfaces/interfaces'; import { IDataProviderHistoricalResponse } from '@ghostfolio/api/services/interfaces/interfaces';
import type { RequestWithUser } from '@ghostfolio/common/types';
import { import {
Controller, Controller,
Get, Get,
HttpException, HttpException,
Inject,
Param, Param,
Query, Query,
UseGuards, UseGuards,
UseInterceptors UseInterceptors
} from '@nestjs/common'; } from '@nestjs/common';
import { REQUEST } from '@nestjs/core';
import { AuthGuard } from '@nestjs/passport'; import { AuthGuard } from '@nestjs/passport';
import { DataSource } from '@prisma/client'; import { DataSource } from '@prisma/client';
import { StatusCodes, getReasonPhrase } from 'http-status-codes'; import { StatusCodes, getReasonPhrase } from 'http-status-codes';
@ -21,7 +24,10 @@ import { SymbolService } from './symbol.service';
@Controller('symbol') @Controller('symbol')
export class SymbolController { export class SymbolController {
public constructor(private readonly symbolService: SymbolService) {} public constructor(
@Inject(REQUEST) private readonly request: RequestWithUser,
private readonly symbolService: SymbolService
) {}
/** /**
* Must be before /:symbol * Must be before /:symbol
@ -33,7 +39,10 @@ export class SymbolController {
@Query() { query = '' } @Query() { query = '' }
): Promise<{ items: LookupItem[] }> { ): Promise<{ items: LookupItem[] }> {
try { try {
return this.symbolService.lookup(query.toLowerCase()); return this.symbolService.lookup({
query: query.toLowerCase(),
user: this.request.user
});
} catch { } catch {
throw new HttpException( throw new HttpException(
getReasonPhrase(StatusCodes.INTERNAL_SERVER_ERROR), getReasonPhrase(StatusCodes.INTERNAL_SERVER_ERROR),

View File

@ -6,6 +6,7 @@ import {
import { MarketDataService } from '@ghostfolio/api/services/market-data.service'; import { MarketDataService } from '@ghostfolio/api/services/market-data.service';
import { DATE_FORMAT } from '@ghostfolio/common/helper'; import { DATE_FORMAT } from '@ghostfolio/common/helper';
import { HistoricalDataItem } from '@ghostfolio/common/interfaces'; import { HistoricalDataItem } from '@ghostfolio/common/interfaces';
import { UserWithSettings } from '@ghostfolio/common/types';
import { Injectable, Logger } from '@nestjs/common'; import { Injectable, Logger } from '@nestjs/common';
import { format, subDays } from 'date-fns'; import { format, subDays } from 'date-fns';
@ -79,15 +80,24 @@ export class SymbolService {
}; };
} }
public async lookup(aQuery: string): Promise<{ items: LookupItem[] }> { public async lookup({
query,
user
}: {
query: string;
user: UserWithSettings;
}): Promise<{ items: LookupItem[] }> {
const results: { items: LookupItem[] } = { items: [] }; const results: { items: LookupItem[] } = { items: [] };
if (!aQuery) { if (!query) {
return results; return results;
} }
try { try {
const { items } = await this.dataProviderService.search(aQuery); const { items } = await this.dataProviderService.search({
query,
user
});
results.items = items; results.items = items;
return results; return results;
} catch (error) { } catch (error) {

View File

@ -3,17 +3,20 @@ import type {
DateRange, DateRange,
ViewMode ViewMode
} from '@ghostfolio/common/types'; } from '@ghostfolio/common/types';
import { Type } from 'class-transformer';
import { import {
IsBoolean, IsBoolean,
IsIn,
IsISO8601, IsISO8601,
IsIn,
IsNumber, IsNumber,
IsOptional, IsOptional,
IsString IsString
} from 'class-validator'; } from 'class-validator';
export class UpdateUserSettingDto { export class UpdateUserSettingDto {
@IsNumber()
@IsOptional()
annualInterestRate?: number;
@IsOptional() @IsOptional()
@IsString() @IsString()
baseCurrency?: string; baseCurrency?: string;

View File

@ -4,16 +4,13 @@ import { PrismaService } from '@ghostfolio/api/services/prisma.service';
import { PropertyService } from '@ghostfolio/api/services/property/property.service'; import { PropertyService } from '@ghostfolio/api/services/property/property.service';
import { TagService } from '@ghostfolio/api/services/tag/tag.service'; import { TagService } from '@ghostfolio/api/services/tag/tag.service';
import { PROPERTY_IS_READ_ONLY_MODE, locale } from '@ghostfolio/common/config'; import { PROPERTY_IS_READ_ONLY_MODE, locale } from '@ghostfolio/common/config';
import { import { User as IUser, UserSettings } from '@ghostfolio/common/interfaces';
User as IUser,
UserSettings,
UserWithSettings
} from '@ghostfolio/common/interfaces';
import { import {
getPermissions, getPermissions,
hasRole, hasRole,
permissions permissions
} from '@ghostfolio/common/permissions'; } from '@ghostfolio/common/permissions';
import { UserWithSettings } from '@ghostfolio/common/types';
import { Injectable } from '@nestjs/common'; import { Injectable } from '@nestjs/common';
import { Prisma, Role, User } from '@prisma/client'; import { Prisma, Role, User } from '@prisma/client';
import { sortBy } from 'lodash'; import { sortBy } from 'lodash';

View File

@ -45,7 +45,10 @@ export class CronService {
dataSource, dataSource,
symbol symbol
}, },
GATHER_ASSET_PROFILE_PROCESS_OPTIONS {
...GATHER_ASSET_PROFILE_PROCESS_OPTIONS,
jobId: `${dataSource}-${symbol}}`
}
); );
} }
} }

View File

@ -2,8 +2,7 @@ import { SymbolProfileService } from '@ghostfolio/api/services/symbol-profile.se
import { import {
DATA_GATHERING_QUEUE, DATA_GATHERING_QUEUE,
GATHER_HISTORICAL_MARKET_DATA_PROCESS, GATHER_HISTORICAL_MARKET_DATA_PROCESS,
GATHER_HISTORICAL_MARKET_DATA_PROCESS_OPTIONS, GATHER_HISTORICAL_MARKET_DATA_PROCESS_OPTIONS
QUEUE_JOB_STATUS_LIST
} from '@ghostfolio/common/config'; } from '@ghostfolio/common/config';
import { DATE_FORMAT, resetHours } from '@ghostfolio/common/helper'; import { DATE_FORMAT, resetHours } from '@ghostfolio/common/helper';
import { UniqueAsset } from '@ghostfolio/common/interfaces'; import { UniqueAsset } from '@ghostfolio/common/interfaces';
@ -11,7 +10,7 @@ import { InjectQueue } from '@nestjs/bull';
import { Inject, Injectable, Logger } from '@nestjs/common'; import { Inject, Injectable, Logger } from '@nestjs/common';
import { DataSource } from '@prisma/client'; import { DataSource } from '@prisma/client';
import { JobOptions, Queue } from 'bull'; import { JobOptions, Queue } from 'bull';
import { format, subDays } from 'date-fns'; import { format, min, subDays, subYears } from 'date-fns';
import { DataProviderService } from './data-provider/data-provider.service'; import { DataProviderService } from './data-provider/data-provider.service';
import { DataEnhancerInterface } from './data-provider/interfaces/data-enhancer.interface'; import { DataEnhancerInterface } from './data-provider/interfaces/data-enhancer.interface';
@ -34,17 +33,14 @@ export class DataGatheringService {
private readonly symbolProfileService: SymbolProfileService private readonly symbolProfileService: SymbolProfileService
) {} ) {}
public async addJobToQueue(name: string, data: any, options?: JobOptions) { public async addJobToQueue(name: string, data: any, opts?: JobOptions) {
const hasJob = await this.hasJob(name, data); return this.dataGatheringQueue.add(name, data, opts);
if (hasJob) {
Logger.log(
`Job ${name} with data ${JSON.stringify(data)} already exists.`,
'DataGatheringService'
);
} else {
return this.dataGatheringQueue.add(name, data, options);
} }
public async addJobsToQueue(
jobs: { data: any; name: string; opts?: JobOptions }[]
) {
return this.dataGatheringQueue.addBulk(jobs);
} }
public async gather7Days() { public async gather7Days() {
@ -152,10 +148,11 @@ export class DataGatheringService {
countries, countries,
currency, currency,
dataSource, dataSource,
isin,
name, name,
sectors, sectors,
url url
} = assetProfiles[symbol]; } = assetProfile;
try { try {
await this.prismaService.symbolProfile.upsert({ await this.prismaService.symbolProfile.upsert({
@ -165,6 +162,7 @@ export class DataGatheringService {
countries, countries,
currency, currency,
dataSource, dataSource,
isin,
name, name,
sectors, sectors,
symbol, symbol,
@ -175,6 +173,7 @@ export class DataGatheringService {
assetSubClass, assetSubClass,
countries, countries,
currency, currency,
isin,
name, name,
sectors, sectors,
url url
@ -206,17 +205,22 @@ export class DataGatheringService {
} }
public async gatherSymbols(aSymbolsWithStartDate: IDataGatheringItem[]) { public async gatherSymbols(aSymbolsWithStartDate: IDataGatheringItem[]) {
for (const { dataSource, date, symbol } of aSymbolsWithStartDate) { await this.addJobsToQueue(
await this.addJobToQueue( aSymbolsWithStartDate.map(({ dataSource, date, symbol }) => {
GATHER_HISTORICAL_MARKET_DATA_PROCESS, return {
{ data: {
dataSource, dataSource,
date, date,
symbol symbol
}, },
GATHER_HISTORICAL_MARKET_DATA_PROCESS_OPTIONS name: GATHER_HISTORICAL_MARKET_DATA_PROCESS,
); opts: {
...GATHER_HISTORICAL_MARKET_DATA_PROCESS_OPTIONS,
jobId: `${dataSource}-${symbol}-${format(date, DATE_FORMAT)}`
} }
};
})
);
} }
public async getSymbolsMax(): Promise<IDataGatheringItem[]> { public async getSymbolsMax(): Promise<IDataGatheringItem[]> {
@ -233,7 +237,7 @@ export class DataGatheringService {
return { return {
dataSource, dataSource,
symbol, symbol,
date: startDate date: min([startDate, subYears(new Date(), 10)])
}; };
}); });
@ -338,18 +342,4 @@ export class DataGatheringService {
return [...currencyPairsToGather, ...symbolProfilesToGather]; return [...currencyPairsToGather, ...symbolProfilesToGather];
} }
private async hasJob(name: string, data: any) {
const jobs = await this.dataGatheringQueue.getJobs(
QUEUE_JOB_STATUS_LIST.filter((status) => {
return status !== 'completed';
})
);
return jobs.some((job) => {
return (
job.name === name && JSON.stringify(job.data) === JSON.stringify(data)
);
});
}
} }

View File

@ -163,10 +163,6 @@ export class CoinGeckoService implements DataProviderInterface {
public async search(aQuery: string): Promise<{ items: LookupItem[] }> { public async search(aQuery: string): Promise<{ items: LookupItem[] }> {
let items: LookupItem[] = []; let items: LookupItem[] = [];
if (aQuery.length <= 2) {
return { items };
}
try { try {
const get = bent( const get = bent(
`${this.URL}/search?query=${aQuery}`, `${this.URL}/search?query=${aQuery}`,
@ -180,6 +176,8 @@ export class CoinGeckoService implements DataProviderInterface {
return { return {
name, name,
symbol, symbol,
assetClass: AssetClass.CASH,
assetSubClass: AssetSubClass.CRYPTOCURRENCY,
currency: this.baseCurrency, currency: this.baseCurrency,
dataSource: this.getName() dataSource: this.getName()
}; };

View File

@ -7,7 +7,7 @@ import bent from 'bent';
const getJSON = bent('json'); const getJSON = bent('json');
export class TrackinsightDataEnhancerService implements DataEnhancerInterface { export class TrackinsightDataEnhancerService implements DataEnhancerInterface {
private static baseUrl = 'https://data.trackinsight.com/holdings'; private static baseUrl = 'https://data.trackinsight.com';
private static countries = require('countries-list/dist/countries.json'); private static countries = require('countries-list/dist/countries.json');
private static countriesMapping = { private static countriesMapping = {
'Russian Federation': 'Russia' 'Russian Federation': 'Russia'
@ -32,17 +32,29 @@ export class TrackinsightDataEnhancerService implements DataEnhancerInterface {
return response; return response;
} }
const result = await getJSON( const profile = await getJSON(
`${TrackinsightDataEnhancerService.baseUrl}/${symbol}.json` `${TrackinsightDataEnhancerService.baseUrl}/data-api/funds/${symbol}.json`
).catch(() => {
return {};
});
const isin = profile.isin?.split(';')?.[0];
if (isin) {
response.isin = isin;
}
const holdings = await getJSON(
`${TrackinsightDataEnhancerService.baseUrl}/holdings/${symbol}.json`
).catch(() => { ).catch(() => {
return getJSON( return getJSON(
`${TrackinsightDataEnhancerService.baseUrl}/${ `${TrackinsightDataEnhancerService.baseUrl}/holdings/${
symbol.split('.')[0] symbol.split('.')?.[0]
}.json` }.json`
); );
}); });
if (result.weight < 0.95) { if (holdings?.weight < 0.95) {
// Skip if data is inaccurate // Skip if data is inaccurate
return response; return response;
} }
@ -52,7 +64,9 @@ export class TrackinsightDataEnhancerService implements DataEnhancerInterface {
(response.countries as unknown as Country[]).length === 0 (response.countries as unknown as Country[]).length === 0
) { ) {
response.countries = []; response.countries = [];
for (const [name, value] of Object.entries<any>(result.countries)) { for (const [name, value] of Object.entries<any>(
holdings?.countries ?? {}
)) {
let countryCode: string; let countryCode: string;
for (const [key, country] of Object.entries<any>( for (const [key, country] of Object.entries<any>(
@ -80,7 +94,9 @@ export class TrackinsightDataEnhancerService implements DataEnhancerInterface {
(response.sectors as unknown as Sector[]).length === 0 (response.sectors as unknown as Sector[]).length === 0
) { ) {
response.sectors = []; response.sectors = [];
for (const [name, value] of Object.entries<any>(result.sectors)) { for (const [name, value] of Object.entries<any>(
holdings?.sectors ?? {}
)) {
response.sectors.push({ response.sectors.push({
name: TrackinsightDataEnhancerService.sectorsMapping[name] ?? name, name: TrackinsightDataEnhancerService.sectorsMapping[name] ?? name,
weight: value.weight weight: value.weight

View File

@ -8,6 +8,7 @@ import {
} from '@ghostfolio/api/services/interfaces/interfaces'; } from '@ghostfolio/api/services/interfaces/interfaces';
import { PrismaService } from '@ghostfolio/api/services/prisma.service'; import { PrismaService } from '@ghostfolio/api/services/prisma.service';
import { DATE_FORMAT } from '@ghostfolio/common/helper'; import { DATE_FORMAT } from '@ghostfolio/common/helper';
import { UserWithSettings } from '@ghostfolio/common/types';
import { Granularity } from '@ghostfolio/common/types'; import { Granularity } from '@ghostfolio/common/types';
import { Inject, Injectable, Logger } from '@nestjs/common'; import { Inject, Injectable, Logger } from '@nestjs/common';
import { DataSource, MarketData, SymbolProfile } from '@prisma/client'; import { DataSource, MarketData, SymbolProfile } from '@prisma/client';
@ -260,25 +261,50 @@ export class DataProviderService {
return response; return response;
} }
public async search(aQuery: string): Promise<{ items: LookupItem[] }> { public async search({
query,
user
}: {
query: string;
user: UserWithSettings;
}): Promise<{ items: LookupItem[] }> {
const promises: Promise<{ items: LookupItem[] }>[] = []; const promises: Promise<{ items: LookupItem[] }>[] = [];
let lookupItems: LookupItem[] = []; let lookupItems: LookupItem[] = [];
for (const dataSource of this.configurationService.get('DATA_SOURCES')) { if (query?.length < 2) {
promises.push( return { items: lookupItems };
this.getDataProvider(DataSource[dataSource]).search(aQuery) }
);
let dataSources = this.configurationService.get('DATA_SOURCES');
if (
this.configurationService.get('ENABLE_FEATURE_SUBSCRIPTION') &&
user.subscription.type === 'Basic'
) {
dataSources = dataSources.filter((dataSource) => {
return !this.isPremiumDataSource(DataSource[dataSource]);
});
}
for (const dataSource of dataSources) {
promises.push(this.getDataProvider(DataSource[dataSource]).search(query));
} }
const searchResults = await Promise.all(promises); const searchResults = await Promise.all(promises);
searchResults.forEach((searchResult) => { searchResults.forEach(({ items }) => {
lookupItems = lookupItems.concat(searchResult.items); if (items?.length > 0) {
lookupItems = lookupItems.concat(items);
}
}); });
const filteredItems = lookupItems.filter((lookupItem) => { const filteredItems = lookupItems
.filter((lookupItem) => {
// Only allow symbols with supported currency // Only allow symbols with supported currency
return lookupItem.currency ? true : false; return lookupItem.currency ? true : false;
})
.sort(({ name: name1 }, { name: name2 }) => {
return name1?.toLowerCase().localeCompare(name2?.toLowerCase());
}); });
return { return {
@ -295,4 +321,9 @@ export class DataProviderService {
throw new Error('No data provider has been found.'); throw new Error('No data provider has been found.');
} }
private isPremiumDataSource(aDataSource: DataSource) {
const premiumDataSources: DataSource[] = [DataSource.EOD_HISTORICAL_DATA];
return premiumDataSources.includes(aDataSource);
}
} }

View File

@ -5,13 +5,17 @@ import {
IDataProviderHistoricalResponse, IDataProviderHistoricalResponse,
IDataProviderResponse IDataProviderResponse
} from '@ghostfolio/api/services/interfaces/interfaces'; } from '@ghostfolio/api/services/interfaces/interfaces';
import { SymbolProfileService } from '@ghostfolio/api/services/symbol-profile.service';
import { DATE_FORMAT } from '@ghostfolio/common/helper'; import { DATE_FORMAT } from '@ghostfolio/common/helper';
import { Granularity } from '@ghostfolio/common/types'; import { Granularity } from '@ghostfolio/common/types';
import { Injectable, Logger } from '@nestjs/common'; import { Injectable, Logger } from '@nestjs/common';
import { DataSource, SymbolProfile } from '@prisma/client'; import {
AssetClass,
AssetSubClass,
DataSource,
SymbolProfile
} from '@prisma/client';
import bent from 'bent'; import bent from 'bent';
import { format } from 'date-fns'; import { format, isToday } from 'date-fns';
@Injectable() @Injectable()
export class EodHistoricalDataService implements DataProviderInterface { export class EodHistoricalDataService implements DataProviderInterface {
@ -19,8 +23,7 @@ export class EodHistoricalDataService implements DataProviderInterface {
private readonly URL = 'https://eodhistoricaldata.com/api'; private readonly URL = 'https://eodhistoricaldata.com/api';
public constructor( public constructor(
private readonly configurationService: ConfigurationService, private readonly configurationService: ConfigurationService
private readonly symbolProfileService: SymbolProfileService
) { ) {
this.apiKey = this.configurationService.get('EOD_HISTORICAL_DATA_API_KEY'); this.apiKey = this.configurationService.get('EOD_HISTORICAL_DATA_API_KEY');
} }
@ -32,8 +35,15 @@ export class EodHistoricalDataService implements DataProviderInterface {
public async getAssetProfile( public async getAssetProfile(
aSymbol: string aSymbol: string
): Promise<Partial<SymbolProfile>> { ): Promise<Partial<SymbolProfile>> {
const [searchResult] = await this.getSearchResult(aSymbol);
return { return {
dataSource: this.getName() assetClass: searchResult?.assetClass,
assetSubClass: searchResult?.assetSubClass,
currency: searchResult?.currency,
dataSource: this.getName(),
isin: searchResult?.isin,
name: searchResult?.name
}; };
} }
@ -122,32 +132,30 @@ export class EodHistoricalDataService implements DataProviderInterface {
200 200
); );
const [response, symbolProfiles] = await Promise.all([ const [realTimeResponse, searchResponse] = await Promise.all([
get(), get(),
this.symbolProfileService.getSymbolProfiles( this.search(aSymbols[0])
aSymbols.map((symbol) => {
return {
symbol,
dataSource: DataSource.EOD_HISTORICAL_DATA
};
})
)
]); ]);
const quotes = aSymbols.length === 1 ? [response] : response; const quotes =
aSymbols.length === 1 ? [realTimeResponse] : realTimeResponse;
return quotes.reduce((result, item, index, array) => { return quotes.reduce(
result[item.code] = { (
currency: symbolProfiles.find((symbolProfile) => { result: { [symbol: string]: IDataProviderResponse },
return symbolProfile.symbol === item.code; { close, code, timestamp }
})?.currency, ) => {
result[code] = {
currency: searchResponse?.items[0]?.currency,
dataSource: DataSource.EOD_HISTORICAL_DATA, dataSource: DataSource.EOD_HISTORICAL_DATA,
marketPrice: item.close, marketPrice: close,
marketState: 'delayed' marketState: isToday(new Date(timestamp * 1000)) ? 'open' : 'closed'
}; };
return result; return result;
}, {}); },
{}
);
} catch (error) { } catch (error) {
Logger.error(error, 'EodHistoricalDataService'); Logger.error(error, 'EodHistoricalDataService');
} }
@ -156,6 +164,117 @@ export class EodHistoricalDataService implements DataProviderInterface {
} }
public async search(aQuery: string): Promise<{ items: LookupItem[] }> { public async search(aQuery: string): Promise<{ items: LookupItem[] }> {
return { items: [] }; const searchResult = await this.getSearchResult(aQuery);
return {
items: searchResult
.filter(({ symbol }) => {
return !symbol.toLowerCase().endsWith('forex');
})
.map(
({
assetClass,
assetSubClass,
currency,
dataSource,
name,
symbol
}) => {
return {
assetClass,
assetSubClass,
currency,
dataSource,
name,
symbol
};
}
)
};
}
private async getSearchResult(aQuery: string): Promise<
(LookupItem & {
assetClass: AssetClass;
assetSubClass: AssetSubClass;
isin: string;
})[]
> {
let searchResult = [];
try {
const get = bent(
`${this.URL}/search/${aQuery}?api_token=${this.apiKey}`,
'GET',
'json',
200
);
const response = await get();
searchResult = response.map(
({
Code,
Currency: currency,
Exchange,
ISIN: isin,
Name: name,
Type
}) => {
const { assetClass, assetSubClass } = this.parseAssetClass({
Exchange,
Type
});
return {
assetClass,
assetSubClass,
currency,
isin,
name,
dataSource: this.getName(),
symbol: `${Code}.${Exchange}`
};
}
);
} catch (error) {
Logger.error(error, 'EodHistoricalDataService');
}
return searchResult;
}
private parseAssetClass({
Exchange,
Type
}: {
Exchange: string;
Type: string;
}): {
assetClass: AssetClass;
assetSubClass: AssetSubClass;
} {
let assetClass: AssetClass;
let assetSubClass: AssetSubClass;
switch (Type?.toLowerCase()) {
case 'common stock':
assetClass = AssetClass.EQUITY;
assetSubClass = AssetSubClass.STOCK;
break;
case 'currency':
assetClass = AssetClass.CASH;
if (Exchange?.toLowerCase() === 'cc') {
assetSubClass = AssetSubClass.CRYPTOCURRENCY;
}
break;
case 'etf':
assetClass = AssetClass.EQUITY;
assetSubClass = AssetSubClass.ETF;
break;
}
return { assetClass, assetSubClass };
} }
} }

View File

@ -145,6 +145,8 @@ export class GoogleSheetsService implements DataProviderInterface {
public async search(aQuery: string): Promise<{ items: LookupItem[] }> { public async search(aQuery: string): Promise<{ items: LookupItem[] }> {
const items = await this.prismaService.symbolProfile.findMany({ const items = await this.prismaService.symbolProfile.findMany({
select: { select: {
assetClass: true,
assetSubClass: true,
currency: true, currency: true,
dataSource: true, dataSource: true,
name: true, name: true,

View File

@ -165,6 +165,8 @@ export class ManualService implements DataProviderInterface {
public async search(aQuery: string): Promise<{ items: LookupItem[] }> { public async search(aQuery: string): Promise<{ items: LookupItem[] }> {
let items = await this.prismaService.symbolProfile.findMany({ let items = await this.prismaService.symbolProfile.findMany({
select: { select: {
assetClass: true,
assetSubClass: true,
currency: true, currency: true,
dataSource: true, dataSource: true,
name: true, name: true,

View File

@ -101,9 +101,10 @@ export class YahooFinanceService implements DataProviderInterface {
modules: ['price', 'summaryProfile', 'topHoldings'] modules: ['price', 'summaryProfile', 'topHoldings']
}); });
const { assetClass, assetSubClass } = this.parseAssetClass( const { assetClass, assetSubClass } = this.parseAssetClass({
assetProfile.price quoteType: assetProfile.price.quoteType,
); shortName: assetProfile.price.shortName
});
response.assetClass = assetClass; response.assetClass = assetClass;
response.assetSubClass = assetSubClass; response.assetSubClass = assetSubClass;
@ -408,7 +409,14 @@ export class YahooFinanceService implements DataProviderInterface {
marketDataItem.symbol marketDataItem.symbol
); );
const { assetClass, assetSubClass } = this.parseAssetClass({
quoteType: quote.quoteType,
shortName: quote.shortname
});
items.push({ items.push({
assetClass,
assetSubClass,
symbol, symbol,
currency: marketDataItem.currency, currency: marketDataItem.currency,
dataSource: this.getName(), dataSource: this.getName(),
@ -484,14 +492,20 @@ export class YahooFinanceService implements DataProviderInterface {
return value; return value;
} }
private parseAssetClass(aPrice: Price): { private parseAssetClass({
quoteType,
shortName
}: {
quoteType: string;
shortName: string;
}): {
assetClass: AssetClass; assetClass: AssetClass;
assetSubClass: AssetSubClass; assetSubClass: AssetSubClass;
} { } {
let assetClass: AssetClass; let assetClass: AssetClass;
let assetSubClass: AssetSubClass; let assetSubClass: AssetSubClass;
switch (aPrice?.quoteType?.toLowerCase()) { switch (quoteType?.toLowerCase()) {
case 'cryptocurrency': case 'cryptocurrency':
assetClass = AssetClass.CASH; assetClass = AssetClass.CASH;
assetSubClass = AssetSubClass.CRYPTOCURRENCY; assetSubClass = AssetSubClass.CRYPTOCURRENCY;
@ -509,10 +523,10 @@ export class YahooFinanceService implements DataProviderInterface {
assetSubClass = AssetSubClass.COMMODITY; assetSubClass = AssetSubClass.COMMODITY;
if ( if (
aPrice?.shortName?.toLowerCase()?.startsWith('gold') || shortName?.toLowerCase()?.startsWith('gold') ||
aPrice?.shortName?.toLowerCase()?.startsWith('palladium') || shortName?.toLowerCase()?.startsWith('palladium') ||
aPrice?.shortName?.toLowerCase()?.startsWith('platinum') || shortName?.toLowerCase()?.startsWith('platinum') ||
aPrice?.shortName?.toLowerCase()?.startsWith('silver') shortName?.toLowerCase()?.startsWith('silver')
) { ) {
assetSubClass = AssetSubClass.PRECIOUS_METAL; assetSubClass = AssetSubClass.PRECIOUS_METAL;
} }

View File

@ -183,8 +183,29 @@ export class ExchangeRateDataService {
if (marketData?.marketPrice) { if (marketData?.marketPrice) {
factor = marketData?.marketPrice; factor = marketData?.marketPrice;
} else { } else {
// TODO: Get from data provider service or calculate indirectly via base currency // Calculate indirectly via base currency
// and market data try {
const [
{ marketPrice: marketPriceBaseCurrencyFromCurrency },
{ marketPrice: marketPriceBaseCurrencyToCurrency }
] = await Promise.all([
this.marketDataService.get({
dataSource,
date: aDate,
symbol: `${this.baseCurrency}${aFromCurrency}`
}),
this.marketDataService.get({
dataSource,
date: aDate,
symbol: `${this.baseCurrency}${aToCurrency}`
})
]);
// Calculate the opposite direction
factor =
(1 / marketPriceBaseCurrencyFromCurrency) *
marketPriceBaseCurrencyToCurrency;
} catch {}
} }
} }

View File

@ -0,0 +1,11 @@
import { PrismaModule } from '@ghostfolio/api/services/prisma.module';
import { Module } from '@nestjs/common';
import { PlatformService } from './platform.service';
@Module({
exports: [PlatformService],
imports: [PrismaModule],
providers: [PlatformService]
})
export class PlatformModule {}

View File

@ -0,0 +1,11 @@
import { PrismaService } from '@ghostfolio/api/services/prisma.service';
import { Injectable } from '@nestjs/common';
@Injectable()
export class PlatformService {
public constructor(private readonly prismaService: PrismaService) {}
public async get() {
return this.prismaService.platform.findMany();
}
}

View File

@ -3,12 +3,7 @@ export default {
displayName: 'client', displayName: 'client',
setupFilesAfterEnv: ['<rootDir>/src/test-setup.ts'], setupFilesAfterEnv: ['<rootDir>/src/test-setup.ts'],
globals: { globals: {},
'ts-jest': {
tsconfig: '<rootDir>/tsconfig.spec.json',
stringifyContentPathRegex: '\\.(html|svg)$'
}
},
coverageDirectory: '../../coverage/apps/client', coverageDirectory: '../../coverage/apps/client',
snapshotSerializers: [ snapshotSerializers: [
'jest-preset-angular/build/serializers/no-ng-attributes', 'jest-preset-angular/build/serializers/no-ng-attributes',
@ -16,7 +11,13 @@ export default {
'jest-preset-angular/build/serializers/html-comment' 'jest-preset-angular/build/serializers/html-comment'
], ],
transform: { transform: {
'^.+.(ts|mjs|js|html)$': 'jest-preset-angular' '^.+.(ts|mjs|js|html)$': [
'jest-preset-angular',
{
tsconfig: '<rootDir>/tsconfig.spec.json',
stringifyContentPathRegex: '\\.(html|svg)$'
}
]
}, },
transformIgnorePatterns: ['node_modules/(?!.*.mjs$)'], transformIgnorePatterns: ['node_modules/(?!.*.mjs$)'],
preset: '../../jest.preset.js' preset: '../../jest.preset.js'

View File

@ -130,6 +130,13 @@ const routes: Routes = [
'./pages/blog/2023/02/ghostfolio-meets-umbrel/ghostfolio-meets-umbrel-page.module' './pages/blog/2023/02/ghostfolio-meets-umbrel/ghostfolio-meets-umbrel-page.module'
).then((m) => m.GhostfolioMeetsUmbrelPageModule) ).then((m) => m.GhostfolioMeetsUmbrelPageModule)
}, },
{
path: 'blog/2023/03/ghostfolio-reaches-1000-stars-on-github',
loadChildren: () =>
import(
'./pages/blog/2023/03/1000-stars-on-github/1000-stars-on-github-page.module'
).then((m) => m.ThousandStarsOnGitHubPageModule)
},
{ {
path: 'demo', path: 'demo',
loadChildren: () => loadChildren: () =>

View File

@ -7,10 +7,10 @@ import {
MAT_DATE_LOCALE, MAT_DATE_LOCALE,
MatNativeDateModule MatNativeDateModule
} from '@angular/material/core'; } from '@angular/material/core';
import { MatLegacyAutocompleteModule as MatAutocompleteModule } from '@angular/material/legacy-autocomplete'; import { MatAutocompleteModule } from '@angular/material/autocomplete';
import { MatLegacyChipsModule as MatChipsModule } from '@angular/material/legacy-chips'; import { MatChipsModule } from '@angular/material/chips';
import { MatLegacySnackBarModule as MatSnackBarModule } from '@angular/material/legacy-snack-bar'; import { MatSnackBarModule } from '@angular/material/snack-bar';
import { MatLegacyTooltipModule as MatTooltipModule } from '@angular/material/legacy-tooltip'; import { MatTooltipModule } from '@angular/material/tooltip';
import { BrowserModule } from '@angular/platform-browser'; import { BrowserModule } from '@angular/platform-browser';
import { BrowserAnimationsModule } from '@angular/platform-browser/animations'; import { BrowserAnimationsModule } from '@angular/platform-browser/animations';
import { ServiceWorkerModule } from '@angular/service-worker'; import { ServiceWorkerModule } from '@angular/service-worker';

View File

@ -7,7 +7,7 @@ import {
OnInit, OnInit,
Output Output
} from '@angular/core'; } from '@angular/core';
import { MatLegacyTableDataSource as MatTableDataSource } from '@angular/material/legacy-table'; import { MatTableDataSource } from '@angular/material/table';
import { DEFAULT_LANGUAGE_CODE } from '@ghostfolio/common/config'; import { DEFAULT_LANGUAGE_CODE } from '@ghostfolio/common/config';
import { Access } from '@ghostfolio/common/interfaces'; import { Access } from '@ghostfolio/common/interfaces';

View File

@ -1,8 +1,8 @@
import { CommonModule } from '@angular/common'; import { CommonModule } from '@angular/common';
import { CUSTOM_ELEMENTS_SCHEMA, NgModule } from '@angular/core'; import { CUSTOM_ELEMENTS_SCHEMA, NgModule } from '@angular/core';
import { MatLegacyButtonModule as MatButtonModule } from '@angular/material/legacy-button'; import { MatButtonModule } from '@angular/material/button';
import { MatLegacyMenuModule as MatMenuModule } from '@angular/material/legacy-menu'; import { MatMenuModule } from '@angular/material/menu';
import { MatLegacyTableModule as MatTableModule } from '@angular/material/legacy-table'; import { MatTableModule } from '@angular/material/table';
import { AccessTableComponent } from './access-table.component'; import { AccessTableComponent } from './access-table.component';

View File

@ -1,7 +1,7 @@
:host { :host {
display: block; display: block;
.mat-dialog-content { .mat-mdc-dialog-content {
max-height: unset; max-height: unset;
} }
} }

View File

@ -6,10 +6,7 @@ import {
OnDestroy, OnDestroy,
OnInit OnInit
} from '@angular/core'; } from '@angular/core';
import { import { MAT_DIALOG_DATA, MatDialogRef } from '@angular/material/dialog';
MAT_LEGACY_DIALOG_DATA as MAT_DIALOG_DATA,
MatLegacyDialogRef as MatDialogRef
} from '@angular/material/legacy-dialog';
import { DataService } from '@ghostfolio/client/services/data.service'; import { DataService } from '@ghostfolio/client/services/data.service';
import { UserService } from '@ghostfolio/client/services/user/user.service'; import { UserService } from '@ghostfolio/client/services/user/user.service';
import { downloadAsFile } from '@ghostfolio/common/helper'; import { downloadAsFile } from '@ghostfolio/common/helper';

View File

@ -1,7 +1,7 @@
import { CommonModule } from '@angular/common'; import { CommonModule } from '@angular/common';
import { CUSTOM_ELEMENTS_SCHEMA, NgModule } from '@angular/core'; import { CUSTOM_ELEMENTS_SCHEMA, NgModule } from '@angular/core';
import { MatLegacyButtonModule as MatButtonModule } from '@angular/material/legacy-button'; import { MatButtonModule } from '@angular/material/button';
import { MatLegacyDialogModule as MatDialogModule } from '@angular/material/legacy-dialog'; import { MatDialogModule } from '@angular/material/dialog';
import { GfDialogFooterModule } from '@ghostfolio/client/components/dialog-footer/dialog-footer.module'; import { GfDialogFooterModule } from '@ghostfolio/client/components/dialog-footer/dialog-footer.module';
import { GfDialogHeaderModule } from '@ghostfolio/client/components/dialog-header/dialog-header.module'; import { GfDialogHeaderModule } from '@ghostfolio/client/components/dialog-header/dialog-header.module';
import { GfActivitiesTableModule } from '@ghostfolio/ui/activities-table/activities-table.module'; import { GfActivitiesTableModule } from '@ghostfolio/ui/activities-table/activities-table.module';

View File

@ -3,17 +3,7 @@
:host { :host {
display: block; display: block;
.mat-table { .mat-mdc-table {
td {
&.mat-footer-cell {
border-top: 1px solid
rgba(
var(--palette-foreground-divider),
var(--palette-foreground-divider-alpha)
);
}
}
th { th {
::ng-deep { ::ng-deep {
.mat-sort-header-container { .mat-sort-header-container {
@ -23,16 +13,3 @@
} }
} }
} }
:host-context(.is-dark-theme) {
.mat-table {
td {
&.mat-footer-cell {
border-top-color: rgba(
var(--palette-foreground-divider-dark),
var(--palette-foreground-divider-dark-alpha)
);
}
}
}
}

View File

@ -9,7 +9,7 @@ import {
Output, Output,
ViewChild ViewChild
} from '@angular/core'; } from '@angular/core';
import { MatLegacyTableDataSource as MatTableDataSource } from '@angular/material/legacy-table'; import { MatTableDataSource } from '@angular/material/table';
import { MatSort } from '@angular/material/sort'; import { MatSort } from '@angular/material/sort';
import { Router } from '@angular/router'; import { Router } from '@angular/router';
import { Account as AccountModel } from '@prisma/client'; import { Account as AccountModel } from '@prisma/client';

View File

@ -1,8 +1,8 @@
import { CommonModule } from '@angular/common'; import { CommonModule } from '@angular/common';
import { CUSTOM_ELEMENTS_SCHEMA, NgModule } from '@angular/core'; import { CUSTOM_ELEMENTS_SCHEMA, NgModule } from '@angular/core';
import { MatLegacyButtonModule as MatButtonModule } from '@angular/material/legacy-button'; import { MatButtonModule } from '@angular/material/button';
import { MatLegacyMenuModule as MatMenuModule } from '@angular/material/legacy-menu'; import { MatMenuModule } from '@angular/material/menu';
import { MatLegacyTableModule as MatTableModule } from '@angular/material/legacy-table'; import { MatTableModule } from '@angular/material/table';
import { MatSortModule } from '@angular/material/sort'; import { MatSortModule } from '@angular/material/sort';
import { RouterModule } from '@angular/router'; import { RouterModule } from '@angular/router';
import { GfSymbolIconModule } from '@ghostfolio/client/components/symbol-icon/symbol-icon.module'; import { GfSymbolIconModule } from '@ghostfolio/client/components/symbol-icon/symbol-icon.module';

View File

@ -2,10 +2,7 @@
<div class="row"> <div class="row">
<div class="col"> <div class="col">
<form class="align-items-center d-flex" [formGroup]="filterForm"> <form class="align-items-center d-flex" [formGroup]="filterForm">
<mat-form-field <mat-form-field appearance="outline" class="w-100 without-hint">
appearance="outline"
class="compact-with-outline without-hint w-100"
>
<mat-select formControlName="status"> <mat-select formControlName="status">
<mat-option></mat-option> <mat-option></mat-option>
<mat-option <mat-option

View File

@ -1,9 +1,9 @@
import { CommonModule } from '@angular/common'; import { CommonModule } from '@angular/common';
import { CUSTOM_ELEMENTS_SCHEMA, NgModule } from '@angular/core'; import { CUSTOM_ELEMENTS_SCHEMA, NgModule } from '@angular/core';
import { FormsModule, ReactiveFormsModule } from '@angular/forms'; import { FormsModule, ReactiveFormsModule } from '@angular/forms';
import { MatLegacyButtonModule as MatButtonModule } from '@angular/material/legacy-button'; import { MatButtonModule } from '@angular/material/button';
import { MatLegacyMenuModule as MatMenuModule } from '@angular/material/legacy-menu'; import { MatMenuModule } from '@angular/material/menu';
import { MatLegacySelectModule as MatSelectModule } from '@angular/material/legacy-select'; import { MatSelectModule } from '@angular/material/select';
import { AdminJobsComponent } from './admin-jobs.component'; import { AdminJobsComponent } from './admin-jobs.component';

View File

@ -7,7 +7,7 @@ import {
OnInit, OnInit,
Output Output
} from '@angular/core'; } from '@angular/core';
import { MatLegacyDialog as MatDialog } from '@angular/material/legacy-dialog'; import { MatDialog } from '@angular/material/dialog';
import { UserService } from '@ghostfolio/client/services/user/user.service'; import { UserService } from '@ghostfolio/client/services/user/user.service';
import { import {
DATE_FORMAT, DATE_FORMAT,

View File

@ -6,10 +6,7 @@ import {
OnDestroy OnDestroy
} from '@angular/core'; } from '@angular/core';
import { DateAdapter, MAT_DATE_LOCALE } from '@angular/material/core'; import { DateAdapter, MAT_DATE_LOCALE } from '@angular/material/core';
import { import { MAT_DIALOG_DATA, MatDialogRef } from '@angular/material/dialog';
MAT_LEGACY_DIALOG_DATA as MAT_DIALOG_DATA,
MatLegacyDialogRef as MatDialogRef
} from '@angular/material/legacy-dialog';
import { AdminService } from '@ghostfolio/client/services/admin.service'; import { AdminService } from '@ghostfolio/client/services/admin.service';
import { Subject, takeUntil } from 'rxjs'; import { Subject, takeUntil } from 'rxjs';

View File

@ -1,6 +1,6 @@
<form class="d-flex flex-column h-100"> <form class="d-flex flex-column h-100">
<h1 i18n mat-dialog-title>Details for {{ data.symbol }}</h1> <h1 i18n mat-dialog-title>Details for {{ data.symbol }}</h1>
<div class="flex-grow-1 pt-3" mat-dialog-content> <div class="flex-grow-1 py-3" mat-dialog-content>
<div class="mb-3"> <div class="mb-3">
<mat-form-field appearance="outline" class="w-100"> <mat-form-field appearance="outline" class="w-100">
<mat-label i18n>Date</mat-label> <mat-label i18n>Date</mat-label>

View File

@ -5,7 +5,7 @@ import { MatButtonModule } from '@angular/material/button';
import { MatDatepickerModule } from '@angular/material/datepicker'; import { MatDatepickerModule } from '@angular/material/datepicker';
import { MatFormFieldModule } from '@angular/material/form-field'; import { MatFormFieldModule } from '@angular/material/form-field';
import { MatInputModule } from '@angular/material/input'; import { MatInputModule } from '@angular/material/input';
import { MatLegacyDialogModule as MatDialogModule } from '@angular/material/legacy-dialog'; import { MatDialogModule } from '@angular/material/dialog';
import { MarketDataDetailDialog } from './market-data-detail-dialog.component'; import { MarketDataDetailDialog } from './market-data-detail-dialog.component';

View File

@ -1,7 +1,7 @@
:host { :host {
display: block; display: block;
.mat-dialog-content { .mat-mdc-dialog-content {
max-height: unset; max-height: unset;
.mat-mdc-button { .mat-mdc-button {

View File

@ -6,8 +6,8 @@ import {
OnInit, OnInit,
ViewChild ViewChild
} from '@angular/core'; } from '@angular/core';
import { MatLegacyDialog as MatDialog } from '@angular/material/legacy-dialog'; import { MatDialog } from '@angular/material/dialog';
import { MatLegacyTableDataSource as MatTableDataSource } from '@angular/material/legacy-table'; import { MatTableDataSource } from '@angular/material/table';
import { MatSort } from '@angular/material/sort'; import { MatSort } from '@angular/material/sort';
import { ActivatedRoute, Router } from '@angular/router'; import { ActivatedRoute, Router } from '@angular/router';
import { AdminService } from '@ghostfolio/client/services/admin.service'; import { AdminService } from '@ghostfolio/client/services/admin.service';

View File

@ -152,7 +152,7 @@
mat-menu-item mat-menu-item
(click)="onGatherSymbol({dataSource: element.dataSource, symbol: element.symbol})" (click)="onGatherSymbol({dataSource: element.dataSource, symbol: element.symbol})"
> >
<ng-container i18n>Gather Data</ng-container> <ng-container i18n>Gather Historical Data</ng-container>
</button> </button>
<button <button
mat-menu-item mat-menu-item

View File

@ -1,8 +1,8 @@
import { CommonModule } from '@angular/common'; import { CommonModule } from '@angular/common';
import { CUSTOM_ELEMENTS_SCHEMA, NgModule } from '@angular/core'; import { CUSTOM_ELEMENTS_SCHEMA, NgModule } from '@angular/core';
import { MatLegacyButtonModule as MatButtonModule } from '@angular/material/legacy-button'; import { MatButtonModule } from '@angular/material/button';
import { MatLegacyMenuModule as MatMenuModule } from '@angular/material/legacy-menu'; import { MatMenuModule } from '@angular/material/menu';
import { MatLegacyTableModule as MatTableModule } from '@angular/material/legacy-table'; import { MatTableModule } from '@angular/material/table';
import { MatSortModule } from '@angular/material/sort'; import { MatSortModule } from '@angular/material/sort';
import { GfActivitiesFilterModule } from '@ghostfolio/ui/activities-filter/activities-filter.module'; import { GfActivitiesFilterModule } from '@ghostfolio/ui/activities-filter/activities-filter.module';

View File

@ -1,7 +1,7 @@
:host { :host {
display: block; display: block;
.mat-dialog-content { .mat-mdc-dialog-content {
max-height: unset; max-height: unset;
} }
} }

View File

@ -7,13 +7,11 @@ import {
OnInit OnInit
} from '@angular/core'; } from '@angular/core';
import { FormBuilder } from '@angular/forms'; import { FormBuilder } from '@angular/forms';
import { import { MAT_DIALOG_DATA, MatDialogRef } from '@angular/material/dialog';
MAT_LEGACY_DIALOG_DATA as MAT_DIALOG_DATA,
MatLegacyDialogRef as MatDialogRef
} from '@angular/material/legacy-dialog';
import { UpdateAssetProfileDto } from '@ghostfolio/api/app/admin/update-asset-profile.dto'; import { UpdateAssetProfileDto } from '@ghostfolio/api/app/admin/update-asset-profile.dto';
import { AdminService } from '@ghostfolio/client/services/admin.service'; import { AdminService } from '@ghostfolio/client/services/admin.service';
import { import {
AdminMarketDataDetails,
EnhancedSymbolProfile, EnhancedSymbolProfile,
UniqueAsset UniqueAsset
} from '@ghostfolio/common/interfaces'; } from '@ghostfolio/common/interfaces';
@ -33,7 +31,7 @@ import { AssetProfileDialogParams } from './interfaces/interfaces';
}) })
export class AssetProfileDialog implements OnDestroy, OnInit { export class AssetProfileDialog implements OnDestroy, OnInit {
public assetClass: string; public assetClass: string;
public assetProfile: EnhancedSymbolProfile; public assetProfile: AdminMarketDataDetails['assetProfile'];
public assetProfileForm = this.formBuilder.group({ public assetProfileForm = this.formBuilder.group({
comment: '', comment: '',
symbolMapping: '' symbolMapping: ''

View File

@ -27,7 +27,7 @@
[disabled]="assetProfileForm.dirty" [disabled]="assetProfileForm.dirty"
(click)="onGatherSymbol({dataSource: data.dataSource, symbol: data.symbol})" (click)="onGatherSymbol({dataSource: data.dataSource, symbol: data.symbol})"
> >
<ng-container i18n>Gather Data</ng-container> <ng-container i18n>Gather Historical Data</ng-container>
</button> </button>
<button <button
mat-menu-item mat-menu-item

View File

@ -2,10 +2,10 @@ import { TextFieldModule } from '@angular/cdk/text-field';
import { CommonModule } from '@angular/common'; import { CommonModule } from '@angular/common';
import { CUSTOM_ELEMENTS_SCHEMA, NgModule } from '@angular/core'; import { CUSTOM_ELEMENTS_SCHEMA, NgModule } from '@angular/core';
import { FormsModule, ReactiveFormsModule } from '@angular/forms'; import { FormsModule, ReactiveFormsModule } from '@angular/forms';
import { MatLegacyButtonModule as MatButtonModule } from '@angular/material/legacy-button'; import { MatButtonModule } from '@angular/material/button';
import { MatLegacyDialogModule as MatDialogModule } from '@angular/material/legacy-dialog'; import { MatDialogModule } from '@angular/material/dialog';
import { MatLegacyInputModule as MatInputModule } from '@angular/material/legacy-input'; import { MatInputModule } from '@angular/material/input';
import { MatLegacyMenuModule as MatMenuModule } from '@angular/material/legacy-menu'; import { MatMenuModule } from '@angular/material/menu';
import { GfAdminMarketDataDetailModule } from '@ghostfolio/client/components/admin-market-data-detail/admin-market-data-detail.module'; import { GfAdminMarketDataDetailModule } from '@ghostfolio/client/components/admin-market-data-detail/admin-market-data-detail.module';
import { GfPortfolioProportionChartModule } from '@ghostfolio/ui/portfolio-proportion-chart/portfolio-proportion-chart.module'; import { GfPortfolioProportionChartModule } from '@ghostfolio/ui/portfolio-proportion-chart/portfolio-proportion-chart.module';
import { GfValueModule } from '@ghostfolio/ui/value'; import { GfValueModule } from '@ghostfolio/ui/value';

View File

@ -1,5 +1,5 @@
import { ChangeDetectorRef, Component, OnDestroy, OnInit } from '@angular/core'; import { ChangeDetectorRef, Component, OnDestroy, OnInit } from '@angular/core';
import { MatLegacySlideToggleChange as MatSlideToggleChange } from '@angular/material/legacy-slide-toggle'; import { MatCheckboxChange } from '@angular/material/checkbox';
import { CacheService } from '@ghostfolio/client/services/cache.service'; import { CacheService } from '@ghostfolio/client/services/cache.service';
import { DataService } from '@ghostfolio/client/services/data.service'; import { DataService } from '@ghostfolio/client/services/data.service';
import { UserService } from '@ghostfolio/client/services/user/user.service'; import { UserService } from '@ghostfolio/client/services/user/user.service';
@ -166,14 +166,14 @@ export class AdminOverviewComponent implements OnDestroy, OnInit {
} }
} }
public onReadOnlyModeChange(aEvent: MatSlideToggleChange) { public onReadOnlyModeChange(aEvent: MatCheckboxChange) {
this.putAdminSetting({ this.putAdminSetting({
key: PROPERTY_IS_READ_ONLY_MODE, key: PROPERTY_IS_READ_ONLY_MODE,
value: aEvent.checked ? true : undefined value: aEvent.checked ? true : undefined
}); });
} }
public onEnableUserSignupModeChange(aEvent: MatSlideToggleChange) { public onEnableUserSignupModeChange(aEvent: MatCheckboxChange) {
this.putAdminSetting({ this.putAdminSetting({
key: PROPERTY_IS_USER_SIGNUP_ENABLED, key: PROPERTY_IS_USER_SIGNUP_ENABLED,
value: aEvent.checked ? undefined : false value: aEvent.checked ? undefined : false

View File

@ -1,7 +1,7 @@
<div class="container"> <div class="container">
<div class="mb-5 row"> <div class="row">
<div class="col"> <div class="col">
<mat-card class="mb-3"> <mat-card appearance="outlined" class="mb-3">
<mat-card-content> <mat-card-content>
<div class="d-flex my-3"> <div class="d-flex my-3">
<div class="w-50" i18n>User Count</div> <div class="w-50" i18n>User Count</div>
@ -51,7 +51,7 @@
<td> <td>
<button <button
*ngIf="customCurrencies.includes(exchangeRate.label2)" *ngIf="customCurrencies.includes(exchangeRate.label2)"
class="mini-icon mx-1 no-min-width px-2" class="h-100 mx-1 no-min-width px-2"
mat-button mat-button
(click)="onDeleteCurrency(exchangeRate.label2)" (click)="onDeleteCurrency(exchangeRate.label2)"
> >
@ -101,21 +101,21 @@
<div class="d-flex my-3"> <div class="d-flex my-3">
<div class="w-50" i18n>User Signup</div> <div class="w-50" i18n>User Signup</div>
<div class="w-50"> <div class="w-50">
<mat-slide-toggle <mat-checkbox
color="primary" color="primary"
[checked]="info.globalPermissions.includes(permissions.createUserAccount)" [checked]="info.globalPermissions.includes(permissions.createUserAccount)"
(change)="onEnableUserSignupModeChange($event)" (change)="onEnableUserSignupModeChange($event)"
></mat-slide-toggle> ></mat-checkbox>
</div> </div>
</div> </div>
<div *ngIf="hasPermissionToToggleReadOnlyMode" class="d-flex my-3"> <div *ngIf="hasPermissionToToggleReadOnlyMode" class="d-flex my-3">
<div class="w-50" i18n>Read-only Mode</div> <div class="w-50" i18n>Read-only Mode</div>
<div class="w-50"> <div class="w-50">
<mat-slide-toggle <mat-checkbox
color="primary" color="primary"
[checked]="info?.isReadOnlyMode" [checked]="info?.isReadOnlyMode"
(change)="onReadOnlyModeChange($event)" (change)="onReadOnlyModeChange($event)"
></mat-slide-toggle> ></mat-checkbox>
</div> </div>
</div> </div>
<div *ngIf="hasPermissionForSystemMessage" class="d-flex my-3"> <div *ngIf="hasPermissionForSystemMessage" class="d-flex my-3">
@ -124,7 +124,7 @@
<div *ngIf="info?.systemMessage"> <div *ngIf="info?.systemMessage">
<span>{{ info.systemMessage }}</span> <span>{{ info.systemMessage }}</span>
<button <button
class="mini-icon mx-1 no-min-width px-2" class="h-100 mx-1 no-min-width px-2"
mat-button mat-button
(click)="onDeleteSystemMessage()" (click)="onDeleteSystemMessage()"
> >
@ -159,7 +159,7 @@
</td> </td>
<td> <td>
<button <button
class="mini-icon mx-1 no-min-width px-2" class="h-100 mx-1 no-min-width px-2"
mat-button mat-button
(click)="onDeleteCoupon(coupon.code)" (click)="onDeleteCoupon(coupon.code)"
> >
@ -172,7 +172,7 @@
<form #couponForm="ngForm" class="align-items-center d-flex"> <form #couponForm="ngForm" class="align-items-center d-flex">
<mat-form-field <mat-form-field
appearance="outline" appearance="outline"
class="compact-with-outline mr-2 without-hint" class="mr-2 without-hint"
> >
<mat-select <mat-select
name="duration" name="duration"

View File

@ -1,10 +1,10 @@
import { CommonModule } from '@angular/common'; import { CommonModule } from '@angular/common';
import { CUSTOM_ELEMENTS_SCHEMA, NgModule } from '@angular/core'; import { CUSTOM_ELEMENTS_SCHEMA, NgModule } from '@angular/core';
import { FormsModule, ReactiveFormsModule } from '@angular/forms'; import { FormsModule, ReactiveFormsModule } from '@angular/forms';
import { MatLegacyButtonModule as MatButtonModule } from '@angular/material/legacy-button'; import { MatButtonModule } from '@angular/material/button';
import { MatLegacyCardModule as MatCardModule } from '@angular/material/legacy-card'; import { MatCardModule } from '@angular/material/card';
import { MatLegacySelectModule as MatSelectModule } from '@angular/material/legacy-select'; import { MatCheckboxModule } from '@angular/material/checkbox';
import { MatLegacySlideToggleModule as MatSlideToggleModule } from '@angular/material/legacy-slide-toggle'; import { MatSelectModule } from '@angular/material/select';
import { CacheService } from '@ghostfolio/client/services/cache.service'; import { CacheService } from '@ghostfolio/client/services/cache.service';
import { GfValueModule } from '@ghostfolio/ui/value'; import { GfValueModule } from '@ghostfolio/ui/value';
@ -18,9 +18,9 @@ import { AdminOverviewComponent } from './admin-overview.component';
FormsModule, FormsModule,
GfValueModule, GfValueModule,
MatButtonModule, MatButtonModule,
MatCheckboxModule,
MatCardModule, MatCardModule,
MatSelectModule, MatSelectModule,
MatSlideToggleModule,
ReactiveFormsModule ReactiveFormsModule
], ],
providers: [CacheService], providers: [CacheService],

View File

@ -3,26 +3,8 @@
:host { :host {
display: block; display: block;
.mat-button {
&.mini-icon {
line-height: 1.5;
}
}
.mat-flat-button {
::ng-deep {
.mat-button-wrapper {
display: block;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
width: 100%;
}
}
}
.subscription { .subscription {
.mat-form-field { .mat-mdc-form-field {
max-width: 100%; max-width: 100%;
} }
} }

View File

@ -1,7 +1,7 @@
import { ChangeDetectorRef, Component, OnDestroy, OnInit } from '@angular/core'; import { ChangeDetectorRef, Component, OnDestroy, OnInit } from '@angular/core';
import { DataService } from '@ghostfolio/client/services/data.service'; import { DataService } from '@ghostfolio/client/services/data.service';
import { UserService } from '@ghostfolio/client/services/user/user.service'; import { UserService } from '@ghostfolio/client/services/user/user.service';
import { getEmojiFlag } from '@ghostfolio/common/helper'; import { getDateFormatString, getEmojiFlag } from '@ghostfolio/common/helper';
import { AdminData, InfoItem, User } from '@ghostfolio/common/interfaces'; import { AdminData, InfoItem, User } from '@ghostfolio/common/interfaces';
import { hasPermission, permissions } from '@ghostfolio/common/permissions'; import { hasPermission, permissions } from '@ghostfolio/common/permissions';
import { import {
@ -18,6 +18,7 @@ import { takeUntil } from 'rxjs/operators';
templateUrl: './admin-users.html' templateUrl: './admin-users.html'
}) })
export class AdminUsersComponent implements OnDestroy, OnInit { export class AdminUsersComponent implements OnDestroy, OnInit {
public defaultDateFormat: string;
public getEmojiFlag = getEmojiFlag; public getEmojiFlag = getEmojiFlag;
public hasPermissionForSubscription: boolean; public hasPermissionForSubscription: boolean;
public info: InfoItem; public info: InfoItem;
@ -43,6 +44,10 @@ export class AdminUsersComponent implements OnDestroy, OnInit {
.subscribe((state) => { .subscribe((state) => {
if (state?.user) { if (state?.user) {
this.user = state.user; this.user = state.user;
this.defaultDateFormat = getDateFormatString(
this.user.settings.locale
);
} }
}); });
} }

View File

@ -4,44 +4,47 @@
<div class="users"> <div class="users">
<table class="gf-table"> <table class="gf-table">
<thead> <thead>
<tr class="mat-header-row"> <tr class="mat-mdc-header-row">
<th class="mat-header-cell px-1 py-2 text-right">#</th> <th class="mat-mdc-header-cell px-1 py-2 text-right">#</th>
<th class="mat-header-cell px-1 py-2" i18n>User</th> <th class="mat-mdc-header-cell px-1 py-2" i18n>User</th>
<th <th
*ngIf="hasPermissionForSubscription" *ngIf="hasPermissionForSubscription"
class="mat-header-cell px-1 py-2" class="mat-mdc-header-cell px-1 py-2"
> >
<ng-container i18n>Country</ng-container> <ng-container i18n>Country</ng-container>
</th> </th>
<th class="mat-header-cell px-1 py-2"> <th class="mat-mdc-header-cell px-1 py-2">
<ng-container i18n>Registration</ng-container> <ng-container i18n>Registration</ng-container>
</th> </th>
<th class="mat-header-cell px-1 py-2 text-right"> <th class="mat-mdc-header-cell px-1 py-2 text-right">
<ng-container i18n>Accounts</ng-container> <ng-container i18n>Accounts</ng-container>
</th> </th>
<th class="mat-header-cell px-1 py-2 text-right"> <th class="mat-mdc-header-cell px-1 py-2 text-right">
<ng-container i18n>Activities</ng-container> <ng-container i18n>Activities</ng-container>
</th> </th>
<th <th
*ngIf="hasPermissionForSubscription" *ngIf="hasPermissionForSubscription"
class="mat-header-cell px-1 py-2 text-right" class="mat-mdc-header-cell px-1 py-2 text-right"
> >
<ng-container i18n>Engagement per Day</ng-container> <ng-container i18n>Engagement per Day</ng-container>
</th> </th>
<th <th
*ngIf="hasPermissionForSubscription" *ngIf="hasPermissionForSubscription"
class="mat-header-cell px-1 py-2" class="mat-mdc-header-cell px-1 py-2"
i18n i18n
> >
Last Request Last Request
</th> </th>
<th class="mat-header-cell px-1 py-2"></th> <th class="mat-mdc-header-cell px-1 py-2"></th>
</tr> </tr>
</thead> </thead>
<tbody> <tbody>
<tr *ngFor="let userItem of users; let i = index" class="mat-row"> <tr
<td class="mat-cell px-1 py-2 text-right">{{ i + 1 }}</td> *ngFor="let userItem of users; let i = index"
<td class="mat-cell px-1 py-2"> class="mat-mdc-row"
>
<td class="mat-mdc-cell px-1 py-2 text-right">{{ i + 1 }}</td>
<td class="mat-mdc-cell px-1 py-2">
<div class="d-flex align-items-center"> <div class="d-flex align-items-center">
<span class="d-none d-sm-inline-block text-monospace" <span class="d-none d-sm-inline-block text-monospace"
>{{ userItem.id }}</span >{{ userItem.id }}</span
@ -53,28 +56,29 @@
*ngIf="userItem?.subscription?.type === 'Premium'" *ngIf="userItem?.subscription?.type === 'Premium'"
class="ml-1" class="ml-1"
[enableLink]="false" [enableLink]="false"
[title]="userItem.subscription.expiresAt | date: defaultDateFormat"
></gf-premium-indicator> ></gf-premium-indicator>
</div> </div>
</td> </td>
<td <td
*ngIf="hasPermissionForSubscription" *ngIf="hasPermissionForSubscription"
class="mat-cell px-1 py-2" class="mat-mdc-cell px-1 py-2"
> >
<span class="h5" [title]="userItem.country" <span class="h5" [title]="userItem.country"
>{{ getEmojiFlag(userItem.country) }}</span >{{ getEmojiFlag(userItem.country) }}</span
> >
</td> </td>
<td class="mat-cell px-1 py-2"> <td class="mat-mdc-cell px-1 py-2">
{{ formatDistanceToNow(userItem.createdAt) }} {{ formatDistanceToNow(userItem.createdAt) }}
</td> </td>
<td class="mat-cell px-1 py-2 text-right"> <td class="mat-mdc-cell px-1 py-2 text-right">
<gf-value <gf-value
class="d-inline-block justify-content-end" class="d-inline-block justify-content-end"
[locale]="user?.settings?.locale" [locale]="user?.settings?.locale"
[value]="userItem.accountCount" [value]="userItem.accountCount"
></gf-value> ></gf-value>
</td> </td>
<td class="mat-cell px-1 py-2 text-right"> <td class="mat-mdc-cell px-1 py-2 text-right">
<gf-value <gf-value
class="d-inline-block justify-content-end" class="d-inline-block justify-content-end"
[locale]="user?.settings?.locale" [locale]="user?.settings?.locale"
@ -83,7 +87,7 @@
</td> </td>
<td <td
*ngIf="hasPermissionForSubscription" *ngIf="hasPermissionForSubscription"
class="mat-cell px-1 py-2 text-right" class="mat-mdc-cell px-1 py-2 text-right"
> >
<gf-value <gf-value
class="d-inline-block justify-content-end" class="d-inline-block justify-content-end"
@ -94,11 +98,11 @@
</td> </td>
<td <td
*ngIf="hasPermissionForSubscription" *ngIf="hasPermissionForSubscription"
class="mat-cell px-1 py-2" class="mat-mdc-cell px-1 py-2"
> >
{{ formatDistanceToNow(userItem.lastActivity) }} {{ formatDistanceToNow(userItem.lastActivity) }}
</td> </td>
<td class="mat-cell px-1 py-2"> <td class="mat-mdc-cell px-1 py-2">
<button <button
class="mx-1 no-min-width px-2" class="mx-1 no-min-width px-2"
mat-button mat-button

View File

@ -1,7 +1,7 @@
import { CommonModule } from '@angular/common'; import { CommonModule } from '@angular/common';
import { CUSTOM_ELEMENTS_SCHEMA, NgModule } from '@angular/core'; import { CUSTOM_ELEMENTS_SCHEMA, NgModule } from '@angular/core';
import { MatLegacyButtonModule as MatButtonModule } from '@angular/material/legacy-button'; import { MatButtonModule } from '@angular/material/button';
import { MatLegacyMenuModule as MatMenuModule } from '@angular/material/legacy-menu'; import { MatMenuModule } from '@angular/material/menu';
import { GfPremiumIndicatorModule } from '@ghostfolio/ui/premium-indicator'; import { GfPremiumIndicatorModule } from '@ghostfolio/ui/premium-indicator';
import { GfValueModule } from '@ghostfolio/ui/value'; import { GfValueModule } from '@ghostfolio/ui/value';

View File

@ -9,8 +9,8 @@
table { table {
min-width: 100%; min-width: 100%;
.mat-row, .mat-mdc-row,
.mat-header-row { .mat-mdc-header-row {
width: 100%; width: 100%;
} }
} }

View File

@ -1,7 +1,7 @@
import { CommonModule } from '@angular/common'; import { CommonModule } from '@angular/common';
import { NgModule } from '@angular/core'; import { NgModule } from '@angular/core';
import { FormsModule, ReactiveFormsModule } from '@angular/forms'; import { FormsModule, ReactiveFormsModule } from '@angular/forms';
import { MatLegacySelectModule as MatSelectModule } from '@angular/material/legacy-select'; import { MatSelectModule } from '@angular/material/select';
import { GfPremiumIndicatorModule } from '@ghostfolio/ui/premium-indicator'; import { GfPremiumIndicatorModule } from '@ghostfolio/ui/premium-indicator';
import { NgxSkeletonLoaderModule } from 'ngx-skeleton-loader'; import { NgxSkeletonLoaderModule } from 'ngx-skeleton-loader';

View File

@ -3,5 +3,5 @@
flex: 0 0 auto; flex: 0 0 auto;
margin-bottom: 0; margin-bottom: 0;
min-height: 0; min-height: 0;
padding: 0; padding: 0 !important;
} }

View File

@ -1,6 +1,6 @@
import { CommonModule } from '@angular/common'; import { CommonModule } from '@angular/common';
import { CUSTOM_ELEMENTS_SCHEMA, NgModule } from '@angular/core'; import { CUSTOM_ELEMENTS_SCHEMA, NgModule } from '@angular/core';
import { MatLegacyButtonModule as MatButtonModule } from '@angular/material/legacy-button'; import { MatButtonModule } from '@angular/material/button';
import { DialogFooterComponent } from './dialog-footer.component'; import { DialogFooterComponent } from './dialog-footer.component';

View File

@ -1,6 +1,6 @@
import { CommonModule } from '@angular/common'; import { CommonModule } from '@angular/common';
import { CUSTOM_ELEMENTS_SCHEMA, NgModule } from '@angular/core'; import { CUSTOM_ELEMENTS_SCHEMA, NgModule } from '@angular/core';
import { MatLegacyButtonModule as MatButtonModule } from '@angular/material/legacy-button'; import { MatButtonModule } from '@angular/material/button';
import { DialogHeaderComponent } from './dialog-header.component'; import { DialogHeaderComponent } from './dialog-header.component';

View File

@ -6,7 +6,7 @@ import {
OnChanges, OnChanges,
Output Output
} from '@angular/core'; } from '@angular/core';
import { MatLegacyDialog as MatDialog } from '@angular/material/legacy-dialog'; import { MatDialog } from '@angular/material/dialog';
import { Router } from '@angular/router'; import { Router } from '@angular/router';
import { LoginWithAccessTokenDialog } from '@ghostfolio/client/components/login-with-access-token-dialog/login-with-access-token-dialog.component'; import { LoginWithAccessTokenDialog } from '@ghostfolio/client/components/login-with-access-token-dialog/login-with-access-token-dialog.component';
import { DataService } from '@ghostfolio/client/services/data.service'; import { DataService } from '@ghostfolio/client/services/data.service';
@ -56,8 +56,8 @@ export class HeaderComponent implements OnChanges {
this.impersonationStorageService this.impersonationStorageService
.onChangeHasImpersonation() .onChangeHasImpersonation()
.pipe(takeUntil(this.unsubscribeSubject)) .pipe(takeUntil(this.unsubscribeSubject))
.subscribe((id) => { .subscribe((impersonationId) => {
this.impersonationId = id; this.impersonationId = impersonationId;
}); });
} }

View File

@ -1,5 +1,5 @@
import { ChangeDetectorRef, Component, OnDestroy, OnInit } from '@angular/core'; import { ChangeDetectorRef, Component, OnDestroy, OnInit } from '@angular/core';
import { MatLegacyDialog as MatDialog } from '@angular/material/legacy-dialog'; import { MatDialog } from '@angular/material/dialog';
import { ActivatedRoute, Router } from '@angular/router'; import { ActivatedRoute, Router } from '@angular/router';
import { PositionDetailDialog } from '@ghostfolio/client/components/position/position-detail-dialog/position-detail-dialog.component'; import { PositionDetailDialog } from '@ghostfolio/client/components/position/position-detail-dialog/position-detail-dialog.component';
import { ToggleComponent } from '@ghostfolio/client/components/toggle/toggle.component'; import { ToggleComponent } from '@ghostfolio/client/components/toggle/toggle.component';
@ -78,8 +78,8 @@ export class HomeHoldingsComponent implements OnDestroy, OnInit {
this.impersonationStorageService this.impersonationStorageService
.onChangeHasImpersonation() .onChangeHasImpersonation()
.pipe(takeUntil(this.unsubscribeSubject)) .pipe(takeUntil(this.unsubscribeSubject))
.subscribe((aId) => { .subscribe((impersonationId) => {
this.hasImpersonationId = !!aId; this.hasImpersonationId = !!impersonationId;
}); });
this.update(); this.update();

View File

@ -9,8 +9,8 @@
</div> </div>
<div class="row"> <div class="row">
<div class="align-items-center col-xs-12 col-md-8 offset-md-2"> <div class="align-items-center col-xs-12 col-md-8 offset-md-2">
<mat-card class="p-0"> <mat-card appearance="outlined">
<mat-card-content> <mat-card-content class="p-0">
<gf-positions <gf-positions
[baseCurrency]="user?.settings?.baseCurrency" [baseCurrency]="user?.settings?.baseCurrency"
[deviceType]="deviceType" [deviceType]="deviceType"

View File

@ -1,7 +1,7 @@
import { CommonModule } from '@angular/common'; import { CommonModule } from '@angular/common';
import { CUSTOM_ELEMENTS_SCHEMA, NgModule } from '@angular/core'; import { CUSTOM_ELEMENTS_SCHEMA, NgModule } from '@angular/core';
import { MatLegacyButtonModule as MatButtonModule } from '@angular/material/legacy-button'; import { MatButtonModule } from '@angular/material/button';
import { MatLegacyCardModule as MatCardModule } from '@angular/material/legacy-card'; import { MatCardModule } from '@angular/material/card';
import { RouterModule } from '@angular/router'; import { RouterModule } from '@angular/router';
import { GfPositionDetailDialogModule } from '@ghostfolio/client/components/position/position-detail-dialog/position-detail-dialog.module'; import { GfPositionDetailDialogModule } from '@ghostfolio/client/components/position/position-detail-dialog/position-detail-dialog.module';
import { GfPositionsModule } from '@ghostfolio/client/components/positions/positions.module'; import { GfPositionsModule } from '@ghostfolio/client/components/positions/positions.module';

View File

@ -66,8 +66,8 @@ export class HomeOverviewComponent implements OnDestroy, OnInit {
this.impersonationStorageService this.impersonationStorageService
.onChangeHasImpersonation() .onChangeHasImpersonation()
.pipe(takeUntil(this.unsubscribeSubject)) .pipe(takeUntil(this.unsubscribeSubject))
.subscribe((aId) => { .subscribe((impersonationId) => {
this.hasImpersonationId = !!aId; this.hasImpersonationId = !!impersonationId;
this.changeDetectorRef.markForCheck(); this.changeDetectorRef.markForCheck();
}); });

View File

@ -2,6 +2,7 @@
:host { :host {
display: block; display: block;
height: 100%;
.chart-container { .chart-container {
aspect-ratio: 16 / 9; aspect-ratio: 16 / 9;

View File

@ -1,9 +1,9 @@
import { ChangeDetectorRef, Component, OnDestroy, OnInit } from '@angular/core'; import { ChangeDetectorRef, Component, OnDestroy, OnInit } from '@angular/core';
import { import {
MatLegacySnackBar as MatSnackBar, MatSnackBar,
MatLegacySnackBarRef as MatSnackBarRef, MatSnackBarRef,
LegacyTextOnlySnackBar as TextOnlySnackBar TextOnlySnackBar
} from '@angular/material/legacy-snack-bar'; } from '@angular/material/snack-bar';
import { Router } from '@angular/router'; import { Router } from '@angular/router';
import { DataService } from '@ghostfolio/client/services/data.service'; import { DataService } from '@ghostfolio/client/services/data.service';
import { ImpersonationStorageService } from '@ghostfolio/client/services/impersonation-storage.service'; import { ImpersonationStorageService } from '@ghostfolio/client/services/impersonation-storage.service';
@ -69,8 +69,8 @@ export class HomeSummaryComponent implements OnDestroy, OnInit {
this.impersonationStorageService this.impersonationStorageService
.onChangeHasImpersonation() .onChangeHasImpersonation()
.pipe(takeUntil(this.unsubscribeSubject)) .pipe(takeUntil(this.unsubscribeSubject))
.subscribe((aId) => { .subscribe((impersonationId) => {
this.hasImpersonationId = !!aId; this.hasImpersonationId = !!impersonationId;
}); });
} }

View File

@ -2,7 +2,7 @@
<h3 class="d-none d-sm-block mb-3 text-center" i18n>Summary</h3> <h3 class="d-none d-sm-block mb-3 text-center" i18n>Summary</h3>
<div class="row"> <div class="row">
<div class="col-xs-12 col-md-8 offset-md-2"> <div class="col-xs-12 col-md-8 offset-md-2">
<mat-card class="h-100"> <mat-card appearance="outlined">
<mat-card-content> <mat-card-content>
<gf-portfolio-summary <gf-portfolio-summary
[baseCurrency]="user?.settings?.baseCurrency" [baseCurrency]="user?.settings?.baseCurrency"

View File

@ -1,6 +1,6 @@
import { CommonModule } from '@angular/common'; import { CommonModule } from '@angular/common';
import { CUSTOM_ELEMENTS_SCHEMA, NgModule } from '@angular/core'; import { CUSTOM_ELEMENTS_SCHEMA, NgModule } from '@angular/core';
import { MatLegacyCardModule as MatCardModule } from '@angular/material/legacy-card'; import { MatCardModule } from '@angular/material/card';
import { RouterModule } from '@angular/router'; import { RouterModule } from '@angular/router';
import { GfPortfolioSummaryModule } from '@ghostfolio/client/components/portfolio-summary/portfolio-summary.module'; import { GfPortfolioSummaryModule } from '@ghostfolio/client/components/portfolio-summary/portfolio-summary.module';

View File

@ -208,8 +208,10 @@ export class InvestmentChartComponent implements OnChanges, OnDestroy {
if ( if (
this.savingsRate && this.savingsRate &&
// @ts-ignore
this.chart.options.plugins.annotation.annotations.savingsRate this.chart.options.plugins.annotation.annotations.savingsRate
) { ) {
// @ts-ignore
this.chart.options.plugins.annotation.annotations.savingsRate.value = this.chart.options.plugins.annotation.annotations.savingsRate.value =
this.savingsRate; this.savingsRate;
} }

View File

@ -1,9 +1,6 @@
import { ChangeDetectionStrategy, Component, Inject } from '@angular/core'; import { ChangeDetectionStrategy, Component, Inject } from '@angular/core';
import { MatLegacyCheckboxChange as MatCheckboxChange } from '@angular/material/legacy-checkbox'; import { MatCheckboxChange } from '@angular/material/checkbox';
import { import { MAT_DIALOG_DATA, MatDialogRef } from '@angular/material/dialog';
MAT_LEGACY_DIALOG_DATA as MAT_DIALOG_DATA,
MatLegacyDialogRef as MatDialogRef
} from '@angular/material/legacy-dialog';
import { Router } from '@angular/router'; import { Router } from '@angular/router';
import { InternetIdentityService } from '@ghostfolio/client/services/internet-identity.service'; import { InternetIdentityService } from '@ghostfolio/client/services/internet-identity.service';
import { import {

View File

@ -4,9 +4,9 @@
(closeButtonClicked)="onClose()" (closeButtonClicked)="onClose()"
></gf-dialog-header> ></gf-dialog-header>
<div mat-dialog-content> <div class="py-3" mat-dialog-content>
<div class="align-items-center d-flex flex-column"> <div class="align-items-center d-flex flex-column">
<mat-form-field appearance="outline" class="w-100"> <mat-form-field appearance="outline" class="without-hint w-100">
<mat-label i18n>Security Token</mat-label> <mat-label i18n>Security Token</mat-label>
<textarea <textarea
cdkTextareaAutosize cdkTextareaAutosize
@ -45,7 +45,7 @@
</div> </div>
<div mat-dialog-actions> <div mat-dialog-actions>
<div class="flex-grow-1"> <div class="flex-grow-1">
<mat-checkbox i18n (change)="onChangeStaySignedIn($event)" <mat-checkbox color="primary" i18n (change)="onChangeStaySignedIn($event)"
>Stay signed in</mat-checkbox >Stay signed in</mat-checkbox
> >
</div> </div>

View File

@ -2,11 +2,11 @@ import { TextFieldModule } from '@angular/cdk/text-field';
import { CommonModule } from '@angular/common'; import { CommonModule } from '@angular/common';
import { CUSTOM_ELEMENTS_SCHEMA, NgModule } from '@angular/core'; import { CUSTOM_ELEMENTS_SCHEMA, NgModule } from '@angular/core';
import { FormsModule, ReactiveFormsModule } from '@angular/forms'; import { FormsModule, ReactiveFormsModule } from '@angular/forms';
import { MatLegacyButtonModule as MatButtonModule } from '@angular/material/legacy-button'; import { MatButtonModule } from '@angular/material/button';
import { MatLegacyCheckboxModule as MatCheckboxModule } from '@angular/material/legacy-checkbox'; import { MatCheckboxModule } from '@angular/material/checkbox';
import { MatLegacyDialogModule as MatDialogModule } from '@angular/material/legacy-dialog'; import { MatDialogModule } from '@angular/material/dialog';
import { MatLegacyFormFieldModule as MatFormFieldModule } from '@angular/material/legacy-form-field'; import { MatFormFieldModule } from '@angular/material/form-field';
import { MatLegacyInputModule as MatInputModule } from '@angular/material/legacy-input'; import { MatInputModule } from '@angular/material/input';
import { GfDialogHeaderModule } from '../dialog-header/dialog-header.module'; import { GfDialogHeaderModule } from '../dialog-header/dialog-header.module';
import { LoginWithAccessTokenDialog } from './login-with-access-token-dialog.component'; import { LoginWithAccessTokenDialog } from './login-with-access-token-dialog.component';

View File

@ -1,23 +1,3 @@
:host { :host {
display: block; display: block;
textarea.mat-input-element.cdk-textarea-autosize {
box-sizing: content-box;
}
.mat-checkbox {
::ng-deep {
label {
margin-bottom: 0;
}
}
}
.mat-form-field {
::ng-deep {
.mat-form-field-wrapper {
padding-bottom: 0;
}
}
}
} }

View File

@ -1,7 +1,7 @@
:host { :host {
display: block; display: block;
.mat-dialog-content { .mat-mdc-dialog-content {
max-height: unset; max-height: unset;
gf-line-chart { gf-line-chart {

View File

@ -6,10 +6,7 @@ import {
OnDestroy, OnDestroy,
OnInit OnInit
} from '@angular/core'; } from '@angular/core';
import { import { MAT_DIALOG_DATA, MatDialogRef } from '@angular/material/dialog';
MAT_LEGACY_DIALOG_DATA as MAT_DIALOG_DATA,
MatLegacyDialogRef as MatDialogRef
} from '@angular/material/legacy-dialog';
import { DataService } from '@ghostfolio/client/services/data.service'; import { DataService } from '@ghostfolio/client/services/data.service';
import { DATE_FORMAT, downloadAsFile } from '@ghostfolio/common/helper'; import { DATE_FORMAT, downloadAsFile } from '@ghostfolio/common/helper';
import { import {

View File

@ -257,9 +257,11 @@
<div *ngIf="tags?.length > 0" class="row"> <div *ngIf="tags?.length > 0" class="row">
<div class="col"> <div class="col">
<div class="h5" i18n>Tags</div> <div class="h5" i18n>Tags</div>
<mat-chip-list> <mat-chip-listbox>
<mat-chip *ngFor="let tag of tags">{{ tag.name }}</mat-chip> <mat-chip-option *ngFor="let tag of tags" disabled
</mat-chip-list> >{{ tag.name }}</mat-chip-option
>
</mat-chip-listbox>
</div> </div>
</div> </div>

View File

@ -1,8 +1,8 @@
import { CommonModule } from '@angular/common'; import { CommonModule } from '@angular/common';
import { CUSTOM_ELEMENTS_SCHEMA, NgModule } from '@angular/core'; import { CUSTOM_ELEMENTS_SCHEMA, NgModule } from '@angular/core';
import { MatLegacyButtonModule as MatButtonModule } from '@angular/material/legacy-button'; import { MatButtonModule } from '@angular/material/button';
import { MatLegacyChipsModule as MatChipsModule } from '@angular/material/legacy-chips'; import { MatChipsModule } from '@angular/material/chips';
import { MatLegacyDialogModule as MatDialogModule } from '@angular/material/legacy-dialog'; import { MatDialogModule } from '@angular/material/dialog';
import { GfDialogFooterModule } from '@ghostfolio/client/components/dialog-footer/dialog-footer.module'; import { GfDialogFooterModule } from '@ghostfolio/client/components/dialog-footer/dialog-footer.module';
import { GfDialogHeaderModule } from '@ghostfolio/client/components/dialog-header/dialog-header.module'; import { GfDialogHeaderModule } from '@ghostfolio/client/components/dialog-header/dialog-header.module';
import { GfActivitiesTableModule } from '@ghostfolio/ui/activities-table/activities-table.module'; import { GfActivitiesTableModule } from '@ghostfolio/ui/activities-table/activities-table.module';

View File

@ -1,6 +1,6 @@
import { CommonModule } from '@angular/common'; import { CommonModule } from '@angular/common';
import { CUSTOM_ELEMENTS_SCHEMA, NgModule } from '@angular/core'; import { CUSTOM_ELEMENTS_SCHEMA, NgModule } from '@angular/core';
import { MatLegacyDialogModule as MatDialogModule } from '@angular/material/legacy-dialog'; import { MatDialogModule } from '@angular/material/dialog';
import { RouterModule } from '@angular/router'; import { RouterModule } from '@angular/router';
import { GfSymbolModule } from '@ghostfolio/client/pipes/symbol/symbol.module'; import { GfSymbolModule } from '@ghostfolio/client/pipes/symbol/symbol.module';
import { GfTrendIndicatorModule } from '@ghostfolio/ui/trend-indicator'; import { GfTrendIndicatorModule } from '@ghostfolio/ui/trend-indicator';

View File

@ -1,6 +1,6 @@
import { CommonModule } from '@angular/common'; import { CommonModule } from '@angular/common';
import { CUSTOM_ELEMENTS_SCHEMA, NgModule } from '@angular/core'; import { CUSTOM_ELEMENTS_SCHEMA, NgModule } from '@angular/core';
import { MatLegacyButtonModule as MatButtonModule } from '@angular/material/legacy-button'; import { MatButtonModule } from '@angular/material/button';
import { GfNoTransactionsInfoModule } from '@ghostfolio/ui/no-transactions-info'; import { GfNoTransactionsInfoModule } from '@ghostfolio/ui/no-transactions-info';
import { GfPositionModule } from '../position/position.module'; import { GfPositionModule } from '../position/position.module';

View File

@ -3,11 +3,14 @@
<div class="col"> <div class="col">
<mat-card <mat-card
*ngIf="hasPermissionToCreateOrder && rules === null" *ngIf="hasPermissionToCreateOrder && rules === null"
appearance="outlined"
class="my-2 text-center" class="my-2 text-center"
> >
<mat-card-content>
<gf-no-transactions-info-indicator <gf-no-transactions-info-indicator
[hasBorder]="false" [hasBorder]="false"
></gf-no-transactions-info-indicator> ></gf-no-transactions-info-indicator
></mat-card-content>
</mat-card> </mat-card>
<gf-rule *ngIf="rules?.length === 0" [isLoading]="true"></gf-rule> <gf-rule *ngIf="rules?.length === 0" [isLoading]="true"></gf-rule>

View File

@ -1,7 +1,7 @@
import { CommonModule } from '@angular/common'; import { CommonModule } from '@angular/common';
import { CUSTOM_ELEMENTS_SCHEMA, NgModule } from '@angular/core'; import { CUSTOM_ELEMENTS_SCHEMA, NgModule } from '@angular/core';
import { MatLegacyButtonModule as MatButtonModule } from '@angular/material/legacy-button'; import { MatButtonModule } from '@angular/material/button';
import { MatLegacyCardModule as MatCardModule } from '@angular/material/legacy-card'; import { MatCardModule } from '@angular/material/card';
import { GfRuleModule } from '@ghostfolio/client/components/rule/rule.module'; import { GfRuleModule } from '@ghostfolio/client/components/rule/rule.module';
import { GfNoTransactionsInfoModule } from '@ghostfolio/ui/no-transactions-info'; import { GfNoTransactionsInfoModule } from '@ghostfolio/ui/no-transactions-info';

View File

@ -1,8 +1,5 @@
import { ChangeDetectionStrategy, Component, Inject } from '@angular/core'; import { ChangeDetectionStrategy, Component, Inject } from '@angular/core';
import { import { MAT_DIALOG_DATA, MatDialogRef } from '@angular/material/dialog';
MAT_LEGACY_DIALOG_DATA as MAT_DIALOG_DATA,
MatLegacyDialogRef as MatDialogRef
} from '@angular/material/legacy-dialog';
import { SubscriptionInterstitialDialogParams } from './interfaces/interfaces'; import { SubscriptionInterstitialDialogParams } from './interfaces/interfaces';

View File

@ -17,17 +17,21 @@
<ion-icon class="mr-1" name="checkmark-circle-outline"></ion-icon> <ion-icon class="mr-1" name="checkmark-circle-outline"></ion-icon>
<span i18n>Portfolio Summary</span> <span i18n>Portfolio Summary</span>
</li> </li>
<li class="align-items-center d-flex mb-1">
<ion-icon class="mr-1" name="checkmark-circle-outline"></ion-icon>
<span i18n>Portfolio Allocations</span>
</li>
<li class="align-items-center d-flex mb-1"> <li class="align-items-center d-flex mb-1">
<ion-icon class="mr-1" name="checkmark-circle-outline"></ion-icon> <ion-icon class="mr-1" name="checkmark-circle-outline"></ion-icon>
<span i18n>Performance Benchmarks</span> <span i18n>Performance Benchmarks</span>
</li> </li>
<li class="align-items-center d-flex mb-1"> <li class="align-items-center d-flex mb-1">
<ion-icon class="mr-1" name="checkmark-circle-outline"></ion-icon> <ion-icon class="mr-1" name="checkmark-circle-outline"></ion-icon>
<span i18n>Allocations</span> <span i18n>FIRE Calculator</span>
</li> </li>
<li class="align-items-center d-flex mb-1"> <li class="align-items-center d-flex mb-1">
<ion-icon class="mr-1" name="checkmark-circle-outline"></ion-icon> <ion-icon class="mr-1" name="checkmark-circle-outline"></ion-icon>
<span i18n>FIRE Calculator</span> <span i18n>Professional Data Provider</span>
</li> </li>
<li class="align-items-center d-flex mb-1"> <li class="align-items-center d-flex mb-1">
<ion-icon class="mr-1" name="checkmark-circle-outline"></ion-icon> <ion-icon class="mr-1" name="checkmark-circle-outline"></ion-icon>

View File

@ -1,7 +1,7 @@
import { CommonModule } from '@angular/common'; import { CommonModule } from '@angular/common';
import { CUSTOM_ELEMENTS_SCHEMA, NgModule } from '@angular/core'; import { CUSTOM_ELEMENTS_SCHEMA, NgModule } from '@angular/core';
import { MatLegacyButtonModule as MatButtonModule } from '@angular/material/legacy-button'; import { MatButtonModule } from '@angular/material/button';
import { MatLegacyDialogModule as MatDialogModule } from '@angular/material/legacy-dialog'; import { MatDialogModule } from '@angular/material/dialog';
import { RouterModule } from '@angular/router'; import { RouterModule } from '@angular/router';
import { GfPremiumIndicatorModule } from '@ghostfolio/ui/premium-indicator'; import { GfPremiumIndicatorModule } from '@ghostfolio/ui/premium-indicator';

View File

@ -1,7 +1,7 @@
:host { :host {
display: block; display: block;
.mat-dialog-content { .mat-mdc-dialog-content {
max-height: unset; max-height: unset;
ion-icon[name='checkmark-circle-outline'] { ion-icon[name='checkmark-circle-outline'] {

View File

@ -6,6 +6,7 @@
<mat-radio-button <mat-radio-button
*ngFor="let option of options" *ngFor="let option of options"
[disabled]="isLoading" [disabled]="isLoading"
[ngClass]="{ 'cursor-pointer': !isLoading }"
[value]="option.value" [value]="option.value"
>{{ option.label }}</mat-radio-button >{{ option.label }}</mat-radio-button
> >

View File

@ -1,26 +1,24 @@
:host { :host {
display: block; display: block;
.mat-radio-button { .mat-mdc-radio-button {
border-radius: 1rem; border-radius: 1rem;
margin: 0 0.25rem; margin: 0 0.25rem;
padding: 0.15rem 0.75rem; padding: 0.15rem 0.75rem;
&.mat-radio-checked { &.mat-mdc-radio-checked {
background-color: rgba(var(--dark-dividers)); background-color: rgba(var(--dark-dividers));
} }
::ng-deep { ::ng-deep {
.mat-radio-container { .mdc-radio {
display: none; display: none;
} }
.mat-radio-label { label {
margin-bottom: 0;
}
.mat-radio-label-content {
color: rgba(var(--dark-primary-text), 1); color: rgba(var(--dark-primary-text), 1);
cursor: inherit;
margin: 0;
padding: 0; padding: 0;
} }
} }
@ -28,14 +26,14 @@
} }
:host-context(.is-dark-theme) { :host-context(.is-dark-theme) {
.mat-radio-button { .mat-mdc-radio-button {
&.mat-radio-checked { &.mat-mdc-radio-checked {
background-color: rgba(var(--light-dividers)); background-color: rgba(var(--light-dividers));
border: 1px solid rgba(var(--light-disabled-text)); border: 1px solid rgba(var(--light-disabled-text));
} }
::ng-deep { ::ng-deep {
.mat-radio-label-content { label {
color: rgba(var(--light-primary-text), 1); color: rgba(var(--light-primary-text), 1);
} }
} }

View File

@ -1,7 +1,7 @@
import { CommonModule } from '@angular/common'; import { CommonModule } from '@angular/common';
import { NgModule } from '@angular/core'; import { NgModule } from '@angular/core';
import { ReactiveFormsModule } from '@angular/forms'; import { ReactiveFormsModule } from '@angular/forms';
import { MatLegacyRadioModule as MatRadioModule } from '@angular/material/legacy-radio'; import { MatRadioModule } from '@angular/material/radio';
import { ToggleComponent } from './toggle.component'; import { ToggleComponent } from './toggle.component';

View File

@ -8,10 +8,10 @@ import {
} from '@angular/common/http'; } from '@angular/common/http';
import { Injectable } from '@angular/core'; import { Injectable } from '@angular/core';
import { import {
MatLegacySnackBar as MatSnackBar, MatSnackBar,
MatLegacySnackBarRef as MatSnackBarRef, MatSnackBarRef,
LegacyTextOnlySnackBar as TextOnlySnackBar TextOnlySnackBar
} from '@angular/material/legacy-snack-bar'; } from '@angular/material/snack-bar';
import { Router } from '@angular/router'; import { Router } from '@angular/router';
import { DataService } from '@ghostfolio/client/services/data.service'; import { DataService } from '@ghostfolio/client/services/data.service';
import { TokenStorageService } from '@ghostfolio/client/services/token-storage.service'; import { TokenStorageService } from '@ghostfolio/client/services/token-storage.service';

View File

@ -34,7 +34,7 @@
<a [routerLink]="['/features']">feature</a>, please join the <a [routerLink]="['/features']">feature</a>, please join the
Ghostfolio Ghostfolio
<a <a
href="https://join.slack.com/t/ghostfolio/shared_invite/zt-vsaan64h-F_I0fEo5M0P88lP9ibCxFg" href="https://ghostfolio.slack.com"
title="Join the Ghostfolio Slack community" title="Join the Ghostfolio Slack community"
>Slack community</a >Slack community</a
>, tweet to >, tweet to
@ -42,7 +42,7 @@
href="https://twitter.com/ghostfolio_" href="https://twitter.com/ghostfolio_"
title="Tweet to Ghostfolio on Twitter" title="Tweet to Ghostfolio on Twitter"
>@ghostfolio_</a >@ghostfolio_</a
><ng-container *ngIf="hasPermissionForSubscription" ><ng-container *ngIf="user?.subscription?.type === 'Premium'"
>, send an e-mail to >, send an e-mail to
<a href="mailto:hi@ghostfol.io" title="Send an e-mail" <a href="mailto:hi@ghostfol.io" title="Send an e-mail"
>hi@ghostfol.io</a >hi@ghostfol.io</a
@ -65,6 +65,7 @@
<ion-icon name="logo-twitter"></ion-icon> <ion-icon name="logo-twitter"></ion-icon>
</a> </a>
<a <a
*ngIf="user?.subscription?.type === 'Premium'"
class="mx-2" class="mx-2"
href="mailto:hi@ghostfol.io" href="mailto:hi@ghostfol.io"
mat-icon-button mat-icon-button
@ -74,7 +75,7 @@
</a> </a>
<a <a
class="mx-2" class="mx-2"
href="https://join.slack.com/t/ghostfolio/shared_invite/zt-vsaan64h-F_I0fEo5M0P88lP9ibCxFg" href="https://ghostfolio.slack.com"
mat-icon-button mat-icon-button
title="Join the Ghostfolio Slack channel" title="Join the Ghostfolio Slack channel"
> >
@ -147,10 +148,7 @@
> >
</div> </div>
<div class="col-xs-12 col-md-4 my-2"> <div class="col-xs-12 col-md-4 my-2">
<a <a class="d-block" href="https://ghostfolio.slack.com">
class="d-block"
href="https://join.slack.com/t/ghostfolio/shared_invite/zt-vsaan64h-F_I0fEo5M0P88lP9ibCxFg"
>
<gf-value <gf-value
size="large" size="large"
[value]="statistics?.slackCommunityUsers ?? '-'" [value]="statistics?.slackCommunityUsers ?? '-'"

Some files were not shown because too many files have changed in this diff Show More