Browse Source

Merge pull request #35 from dandevaud/bugfix/several-bugfixes

Bugfix/several bugfixes
pull/5027/head
dandevaud 2 years ago
committed by GitHub
parent
commit
30f916a1ab
No known key found for this signature in database GPG Key ID: 4AEE18F83AFDEB23
  1. 57
      CHANGELOG.md
  2. 4
      README.md
  3. 5
      apps/api/src/app/account/create-account.dto.ts
  4. 5
      apps/api/src/app/account/update-account.dto.ts
  5. 21
      apps/api/src/app/admin/admin.controller.ts
  6. 4
      apps/api/src/app/admin/admin.module.ts
  7. 55
      apps/api/src/app/admin/admin.service.ts
  8. 7
      apps/api/src/app/info/info.service.ts
  9. 2
      apps/api/src/app/portfolio/current-rate.service.mock.ts
  10. 14
      apps/api/src/app/portfolio/current-rate.service.spec.ts
  11. 33
      apps/api/src/app/portfolio/current-rate.service.ts
  12. 5
      apps/api/src/app/portfolio/interfaces/get-value-object.interface.ts
  13. 7
      apps/api/src/app/symbol/symbol.service.ts
  14. 27
      apps/api/src/app/user/user.service.ts
  15. 80
      apps/api/src/assets/sitemap.xml
  16. 3
      apps/api/src/services/data-provider/alpha-vantage/alpha-vantage.service.ts
  17. 4
      apps/api/src/services/data-provider/coingecko/coingecko.service.ts
  18. 2
      apps/api/src/services/data-provider/data-enhancer/data-enhancer.service.ts
  19. 4
      apps/api/src/services/data-provider/data-enhancer/openfigi/openfigi.service.ts
  20. 4
      apps/api/src/services/data-provider/data-enhancer/trackinsight/trackinsight.service.ts
  21. 8
      apps/api/src/services/data-provider/data-enhancer/yahoo-finance/yahoo-finance.service.ts
  22. 6
      apps/api/src/services/data-provider/data-provider.service.ts
  23. 4
      apps/api/src/services/data-provider/eod-historical-data/eod-historical-data.service.ts
  24. 8
      apps/api/src/services/data-provider/financial-modeling-prep/financial-modeling-prep.service.ts
  25. 3
      apps/api/src/services/data-provider/google-sheets/google-sheets.service.ts
  26. 2
      apps/api/src/services/data-provider/interfaces/data-enhancer.interface.ts
  27. 2
      apps/api/src/services/data-provider/interfaces/data-provider.interface.ts
  28. 2
      apps/api/src/services/data-provider/manual/manual.service.ts
  29. 2
      apps/api/src/services/data-provider/rapid-api/rapid-api.service.ts
  30. 7
      apps/api/src/services/data-provider/yahoo-finance/yahoo-finance.service.ts
  31. 24
      apps/api/src/services/exchange-rate-data/exchange-rate-data.service.ts
  32. 17
      apps/api/src/services/market-data/market-data.service.ts
  33. 11
      apps/api/src/services/symbol-profile/symbol-profile-overwrite.module.ts
  34. 68
      apps/api/src/services/symbol-profile/symbol-profile-overwrite.service.ts
  35. 8
      apps/client/src/app/app.component.html
  36. 17
      apps/client/src/app/app.component.ts
  37. 4
      apps/client/src/app/components/admin-market-data/admin-market-data.component.ts
  38. 18
      apps/client/src/app/components/admin-market-data/admin-market-data.html
  39. 2
      apps/client/src/app/components/admin-market-data/admin-market-data.module.ts
  40. 46
      apps/client/src/app/components/admin-market-data/asset-profile-dialog/asset-profile-dialog.component.ts
  41. 13
      apps/client/src/app/components/admin-market-data/asset-profile-dialog/asset-profile-dialog.html
  42. 2
      apps/client/src/app/components/admin-market-data/asset-profile-dialog/asset-profile-dialog.module.ts
  43. 30
      apps/client/src/app/components/admin-overview/admin-overview.component.ts
  44. 73
      apps/client/src/app/components/admin-overview/admin-overview.html
  45. 2
      apps/client/src/app/components/admin-overview/admin-overview.module.ts
  46. 5
      apps/client/src/app/components/fear-and-greed-index/fear-and-greed-index.component.ts
  47. 3
      apps/client/src/app/components/home-overview/home-overview.component.ts
  48. 2
      apps/client/src/app/components/home-overview/home-overview.html
  49. 10
      apps/client/src/app/components/portfolio-performance/portfolio-performance.component.html
  50. 8
      apps/client/src/app/components/portfolio-performance/portfolio-performance.component.ts
  51. 3
      apps/client/src/app/components/symbol-icon/symbol-icon.component.scss
  52. 28
      apps/client/src/app/pages/accounts/transfer-balance/transfer-balance-dialog.html
  53. 2
      apps/client/src/app/pages/accounts/transfer-balance/transfer-balance-dialog.module.ts
  54. 2
      apps/client/src/app/pages/blog/2023/11/hacktoberfest-2023/hacktoberfest-2023-debriefing-page.html
  55. 2
      apps/client/src/app/pages/landing/landing-page.html
  56. 17
      apps/client/src/app/pages/portfolio/activities/create-or-update-activity-dialog/create-or-update-activity-dialog.html
  57. 2
      apps/client/src/app/pages/portfolio/activities/create-or-update-activity-dialog/create-or-update-activity-dialog.module.ts
  58. 57
      apps/client/src/app/pages/resources/personal-finance-tools/products.ts
  59. 31
      apps/client/src/app/pages/resources/personal-finance-tools/products/allvue-systems-page.component.ts
  60. 31
      apps/client/src/app/pages/resources/personal-finance-tools/products/basil-finance-page.component.ts
  61. 31
      apps/client/src/app/pages/resources/personal-finance-tools/products/magnifi-page.component.ts
  62. 31
      apps/client/src/app/pages/resources/personal-finance-tools/products/monarch-money-page.component.ts
  63. 31
      apps/client/src/app/pages/resources/personal-finance-tools/products/ynab-page.component.ts
  64. 5284
      apps/client/src/locales/messages.de.xlf
  65. 5284
      apps/client/src/locales/messages.es.xlf
  66. 5284
      apps/client/src/locales/messages.fr.xlf
  67. 5284
      apps/client/src/locales/messages.it.xlf
  68. 5284
      apps/client/src/locales/messages.nl.xlf
  69. 5284
      apps/client/src/locales/messages.pt.xlf
  70. 4502
      apps/client/src/locales/messages.tr.xlf
  71. 4452
      apps/client/src/locales/messages.xlf
  72. 10
      libs/common/src/lib/helper.ts
  73. 2
      libs/common/src/lib/interfaces/index.ts
  74. 1
      libs/common/src/lib/interfaces/info-item.interface.ts
  75. 7
      libs/common/src/lib/interfaces/system-message.interface.ts
  76. 2
      libs/common/src/lib/interfaces/user.interface.ts
  77. 8
      libs/ui/src/lib/carousel/carousel.component.html
  78. 5
      libs/ui/src/lib/carousel/carousel.component.scss
  79. 9
      libs/ui/src/lib/i18n.ts
  80. 4
      package.json
  81. 2
      prisma/migrations/20231107080536_removed_account_type_from_account/migration.sql
  82. 1
      prisma/schema.prisma
  83. 137
      yarn.lock

57
CHANGELOG.md

@ -7,10 +7,61 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
## Unreleased ## Unreleased
### Changed
- Improved the language localization for German (`de`)
- Upgraded `ng-extract-i18n-merge` from version `2.7.0` to `2.8.3`
## 2.22.0 - 2023-11-11
### Added
- Added the platform icon to the account selectors in the cash balance transfer from one to another account
- Added the platform icon to the account selector of the create or edit activity dialog
### Changed
- Optimized the style of the carousel component on mobile for the testimonial section on the landing page
- Introduced action menus in the overview of the admin control panel
- Harmonized the name column in the historical market data table of the admin control panel
- Refactored the implementation of the data range functionality (`getRange()`) in the market data service
## 2.21.0 - 2023-11-09
### Changed
- Extended the system message
### Fixed
- Fixed the unit for the _Zen Mode_ in the overview tab of the home page
- Fixed an issue to get quotes in the _Financial Modeling Prep_ service
## 2.20.0 - 2023-11-08
### Changed
- Removed the loading indicator of the unit in the overview tab of the home page
- Improved the import of historical market data in the admin control panel
- Increased the timeout in the health check endpoint for data enhancers
- Increased the timeout in the health check endpoint for data providers
- Removed the account type from the `Account` database schema
## 2.19.0 - 2023-11-06
### Added ### Added
- Added a data migration to set `accountType` to `NULL` in the account database table - Added a data migration to set `accountType` to `NULL` in the account database table
### Changed
- Improved the language localization for the _Fear & Greed Index_ (market mood)
- Improved the language localization for German (`de`)
### Fixed
- Improved the handling of derived currencies (`GBp`, `ILA`, `ZAc`)
## 2.18.0 - 2023-11-05 ## 2.18.0 - 2023-11-05
### Added ### Added
@ -352,7 +403,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
### Added ### Added
- Added health check endpoints for data enhancers - Added a health check endpoint for data enhancers
### Changed ### Changed
@ -528,7 +579,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
### Changed ### Changed
- Improved the usability of the login dialog - Improved the usability of the login dialog
- Disabled the caching in the health check endpoints for data providers - Disabled the caching in the health check endpoint for data providers
- Improved the content of the Frequently Asked Questions (FAQ) page - Improved the content of the Frequently Asked Questions (FAQ) page
- Upgraded `prisma` from version `4.15.0` to `4.16.2` - Upgraded `prisma` from version `4.15.0` to `4.16.2`
@ -916,7 +967,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- Added a fallback to historical market data if a data provider does not provide live data - Added a fallback to historical market data if a data provider does not provide live data
- Added a general health check endpoint - Added a general health check endpoint
- Added health check endpoints for data providers - Added a health check endpoint for data providers
### Changed ### Changed

4
README.md

@ -231,7 +231,7 @@ Deprecated: `GET http://localhost:3333/api/v1/auth/anonymous/<INSERT_SECURITY_TO
``` ```
| Field | Type | Description | | Field | Type | Description |
| ---------- | ------------------- | ---------------------------------------------------- | | ---------- | ------------------- | ----------------------------------------------------------------------------- |
| accountId | string (`optional`) | Id of the account | | accountId | string (`optional`) | Id of the account |
| comment | string (`optional`) | Comment of the activity | | comment | string (`optional`) | Comment of the activity |
| currency | string | `CHF` \| `EUR` \| `USD` etc. | | currency | string | `CHF` \| `EUR` \| `USD` etc. |
@ -240,7 +240,7 @@ Deprecated: `GET http://localhost:3333/api/v1/auth/anonymous/<INSERT_SECURITY_TO
| fee | number | Fee of the activity | | fee | number | Fee of the activity |
| quantity | number | Quantity of the activity | | quantity | number | Quantity of the activity |
| symbol | string | Symbol of the activity (suitable for `dataSource`) | | symbol | string | Symbol of the activity (suitable for `dataSource`) |
| type | string | `BUY` \| `DIVIDEND` \| `ITEM` \| `SELL` | | type | string | `BUY` \| `DIVIDEND` \| `FEE` \| `INTEREST` \| `ITEM` \| `LIABILITY` \| `SELL` |
| unitPrice | number | Price per unit of the activity | | unitPrice | number | Price per unit of the activity |
#### Response #### Response

5
apps/api/src/app/account/create-account.dto.ts

@ -1,4 +1,3 @@
import { AccountType } from '@prisma/client';
import { Transform, TransformFnParams } from 'class-transformer'; import { Transform, TransformFnParams } from 'class-transformer';
import { import {
IsBoolean, IsBoolean,
@ -10,10 +9,6 @@ import {
import { isString } from 'lodash'; import { isString } from 'lodash';
export class CreateAccountDto { export class CreateAccountDto {
@IsOptional()
@IsString()
accountType?: AccountType;
@IsNumber() @IsNumber()
balance: number; balance: number;

5
apps/api/src/app/account/update-account.dto.ts

@ -1,4 +1,3 @@
import { AccountType } from '@prisma/client';
import { Transform, TransformFnParams } from 'class-transformer'; import { Transform, TransformFnParams } from 'class-transformer';
import { import {
IsBoolean, IsBoolean,
@ -10,10 +9,6 @@ import {
import { isString } from 'lodash'; import { isString } from 'lodash';
export class UpdateAccountDto { export class UpdateAccountDto {
@IsOptional()
@IsString()
accountType?: AccountType;
@IsNumber() @IsNumber()
balance: number; balance: number;

21
apps/api/src/app/admin/admin.controller.ts

@ -448,6 +448,7 @@ export class AdminController {
); );
} }
if (dataSource === 'MANUAL') {
await this.adminService.patchAssetProfileData({ await this.adminService.patchAssetProfileData({
dataSource, dataSource,
symbol, symbol,
@ -466,6 +467,26 @@ export class AdminController {
}) })
} }
}); });
} else {
await this.adminService.patchAssetProfileData({
dataSource,
symbol,
tags: {
set: []
}
});
return this.adminService.patchAssetProfileData({
...assetProfileData,
dataSource,
symbol,
tags: {
connect: assetProfileData.tags?.map(({ id }) => {
return { id };
})
}
});
}
} }
@Put('settings/:key') @Put('settings/:key')

4
apps/api/src/app/admin/admin.module.ts

@ -13,6 +13,7 @@ import { Module } from '@nestjs/common';
import { AdminController } from './admin.controller'; import { AdminController } from './admin.controller';
import { AdminService } from './admin.service'; import { AdminService } from './admin.service';
import { QueueModule } from './queue/queue.module'; import { QueueModule } from './queue/queue.module';
import { SymbolProfileOverwriteModule } from '@ghostfolio/api/services/symbol-profile/symbol-profile-overwrite.module';
@Module({ @Module({
imports: [ imports: [
@ -26,7 +27,8 @@ import { QueueModule } from './queue/queue.module';
PropertyModule, PropertyModule,
QueueModule, QueueModule,
SubscriptionModule, SubscriptionModule,
SymbolProfileModule SymbolProfileModule,
SymbolProfileOverwriteModule
], ],
controllers: [AdminController], controllers: [AdminController],
providers: [AdminService], providers: [AdminService],

55
apps/api/src/app/admin/admin.service.ts

@ -6,6 +6,7 @@ import { ExchangeRateDataService } from '@ghostfolio/api/services/exchange-rate-
import { MarketDataService } from '@ghostfolio/api/services/market-data/market-data.service'; import { MarketDataService } from '@ghostfolio/api/services/market-data/market-data.service';
import { PrismaService } from '@ghostfolio/api/services/prisma/prisma.service'; import { PrismaService } from '@ghostfolio/api/services/prisma/prisma.service';
import { PropertyService } from '@ghostfolio/api/services/property/property.service'; import { PropertyService } from '@ghostfolio/api/services/property/property.service';
import { SymbolProfileOverwriteService } from '@ghostfolio/api/services/symbol-profile/symbol-profile-overwrite.service';
import { SymbolProfileService } from '@ghostfolio/api/services/symbol-profile/symbol-profile.service'; import { SymbolProfileService } from '@ghostfolio/api/services/symbol-profile/symbol-profile.service';
import { import {
DEFAULT_CURRENCY, DEFAULT_CURRENCY,
@ -29,7 +30,8 @@ import {
Property, Property,
SymbolProfile, SymbolProfile,
DataSource, DataSource,
Tag Tag,
SymbolProfileOverrides
} from '@prisma/client'; } from '@prisma/client';
import { differenceInDays } from 'date-fns'; import { differenceInDays } from 'date-fns';
import { groupBy } from 'lodash'; import { groupBy } from 'lodash';
@ -44,7 +46,8 @@ export class AdminService {
private readonly prismaService: PrismaService, private readonly prismaService: PrismaService,
private readonly propertyService: PropertyService, private readonly propertyService: PropertyService,
private readonly subscriptionService: SubscriptionService, private readonly subscriptionService: SubscriptionService,
private readonly symbolProfileService: SymbolProfileService private readonly symbolProfileService: SymbolProfileService,
private readonly symbolProfileOverwriteService: SymbolProfileOverwriteService
) {} ) {}
public async addAssetProfile({ public async addAssetProfile({
@ -331,6 +334,7 @@ export class AdminService {
symbol, symbol,
symbolMapping symbolMapping
}: Prisma.SymbolProfileUpdateInput & UniqueAsset) { }: Prisma.SymbolProfileUpdateInput & UniqueAsset) {
if (dataSource === 'MANUAL') {
await this.symbolProfileService.updateSymbolProfile({ await this.symbolProfileService.updateSymbolProfile({
assetClass, assetClass,
assetSubClass, assetSubClass,
@ -342,6 +346,53 @@ export class AdminService {
symbol, symbol,
symbolMapping symbolMapping
}); });
} else {
await this.symbolProfileService.updateSymbolProfile({
comment,
dataSource,
name,
tags,
scraperConfiguration,
symbol,
symbolMapping
});
let symbolProfileId =
await this.symbolProfileOverwriteService.GetSymbolProfileId(
symbol,
dataSource
);
if (symbolProfileId) {
await this.symbolProfileOverwriteService.updateSymbolProfileOverrides({
assetClass,
assetSubClass,
symbolProfileId
});
} else {
symbolProfileId = await this.symbolProfileService.getSymbolProfiles([
{
dataSource,
symbol
}
])[0];
await this.symbolProfileOverwriteService.add({
SymbolProfile: {
connect: {
dataSource_symbol: {
dataSource,
symbol
}
}
}
});
await this.symbolProfileOverwriteService.updateSymbolProfileOverrides({
assetClass,
assetSubClass,
symbolProfileId
});
}
}
const [symbolProfile] = await this.symbolProfileService.getSymbolProfiles([ const [symbolProfile] = await this.symbolProfileService.getSymbolProfiles([
{ {

7
apps/api/src/app/info/info.service.ts

@ -15,7 +15,6 @@ import {
PROPERTY_IS_READ_ONLY_MODE, PROPERTY_IS_READ_ONLY_MODE,
PROPERTY_SLACK_COMMUNITY_USERS, PROPERTY_SLACK_COMMUNITY_USERS,
PROPERTY_STRIPE_CONFIG, PROPERTY_STRIPE_CONFIG,
PROPERTY_SYSTEM_MESSAGE,
ghostfolioFearAndGreedIndexDataSource ghostfolioFearAndGreedIndexDataSource
} from '@ghostfolio/common/config'; } from '@ghostfolio/common/config';
import { import {
@ -58,7 +57,6 @@ export class InfoService {
const platforms = await this.platformService.getPlatforms({ const platforms = await this.platformService.getPlatforms({
orderBy: { name: 'asc' } orderBy: { name: 'asc' }
}); });
let systemMessage: string;
const globalPermissions: string[] = []; const globalPermissions: string[] = [];
@ -104,10 +102,6 @@ export class InfoService {
if (this.configurationService.get('ENABLE_FEATURE_SYSTEM_MESSAGE')) { if (this.configurationService.get('ENABLE_FEATURE_SYSTEM_MESSAGE')) {
globalPermissions.push(permissions.enableSystemMessage); globalPermissions.push(permissions.enableSystemMessage);
systemMessage = (await this.propertyService.getByKey(
PROPERTY_SYSTEM_MESSAGE
)) as string;
} }
const isUserSignupEnabled = const isUserSignupEnabled =
@ -135,7 +129,6 @@ export class InfoService {
platforms, platforms,
statistics, statistics,
subscriptions, subscriptions,
systemMessage,
tags, tags,
baseCurrency: DEFAULT_CURRENCY, baseCurrency: DEFAULT_CURRENCY,
currencies: this.exchangeRateDataService.getCurrencies() currencies: this.exchangeRateDataService.getCurrencies()

2
apps/api/src/app/portfolio/current-rate.service.mock.ts

@ -61,6 +61,7 @@ export const CurrentRateServiceMock = {
for (const dataGatheringItem of dataGatheringItems) { for (const dataGatheringItem of dataGatheringItems) {
values.push({ values.push({
date, date,
dataSource: dataGatheringItem.dataSource,
marketPriceInBaseCurrency: mockGetValue( marketPriceInBaseCurrency: mockGetValue(
dataGatheringItem.symbol, dataGatheringItem.symbol,
date date
@ -74,6 +75,7 @@ export const CurrentRateServiceMock = {
for (const dataGatheringItem of dataGatheringItems) { for (const dataGatheringItem of dataGatheringItems) {
values.push({ values.push({
date, date,
dataSource: dataGatheringItem.dataSource,
marketPriceInBaseCurrency: mockGetValue( marketPriceInBaseCurrency: mockGetValue(
dataGatheringItem.symbol, dataGatheringItem.symbol,
date date

14
apps/api/src/app/portfolio/current-rate.service.spec.ts

@ -2,6 +2,7 @@ import { DataProviderService } from '@ghostfolio/api/services/data-provider/data
import { ExchangeRateDataService } from '@ghostfolio/api/services/exchange-rate-data/exchange-rate-data.service'; import { ExchangeRateDataService } from '@ghostfolio/api/services/exchange-rate-data/exchange-rate-data.service';
import { MarketDataService } from '@ghostfolio/api/services/market-data/market-data.service'; import { MarketDataService } from '@ghostfolio/api/services/market-data/market-data.service';
import { PropertyService } from '@ghostfolio/api/services/property/property.service'; import { PropertyService } from '@ghostfolio/api/services/property/property.service';
import { UniqueAsset } from '@ghostfolio/common/interfaces';
import { DataSource, MarketData } from '@prisma/client'; import { DataSource, MarketData } from '@prisma/client';
import { CurrentRateService } from './current-rate.service'; import { CurrentRateService } from './current-rate.service';
@ -25,30 +26,30 @@ jest.mock('@ghostfolio/api/services/market-data/market-data.service', () => {
getRange: ({ getRange: ({
dateRangeEnd, dateRangeEnd,
dateRangeStart, dateRangeStart,
symbols uniqueAssets
}: { }: {
dateRangeEnd: Date; dateRangeEnd: Date;
dateRangeStart: Date; dateRangeStart: Date;
symbols: string[]; uniqueAssets: UniqueAsset[];
}) => { }) => {
return Promise.resolve<MarketData[]>([ return Promise.resolve<MarketData[]>([
{ {
createdAt: dateRangeStart, createdAt: dateRangeStart,
dataSource: DataSource.YAHOO, dataSource: uniqueAssets[0].dataSource,
date: dateRangeStart, date: dateRangeStart,
id: '8fa48fde-f397-4b0d-adbc-fb940e830e6d', id: '8fa48fde-f397-4b0d-adbc-fb940e830e6d',
marketPrice: 1841.823902, marketPrice: 1841.823902,
state: 'CLOSE', state: 'CLOSE',
symbol: symbols[0] symbol: uniqueAssets[0].symbol
}, },
{ {
createdAt: dateRangeEnd, createdAt: dateRangeEnd,
dataSource: DataSource.YAHOO, dataSource: uniqueAssets[0].dataSource,
date: dateRangeEnd, date: dateRangeEnd,
id: '082d6893-df27-4c91-8a5d-092e84315b56', id: '082d6893-df27-4c91-8a5d-092e84315b56',
marketPrice: 1847.839966, marketPrice: 1847.839966,
state: 'CLOSE', state: 'CLOSE',
symbol: symbols[0] symbol: uniqueAssets[0].symbol
} }
]); ]);
} }
@ -134,6 +135,7 @@ describe('CurrentRateService', () => {
errors: [], errors: [],
values: [ values: [
{ {
dataSource: 'YAHOO',
date: undefined, date: undefined,
marketPriceInBaseCurrency: 1841.823902, marketPriceInBaseCurrency: 1841.823902,
symbol: 'AMZN' symbol: 'AMZN'

33
apps/api/src/app/portfolio/current-rate.service.ts

@ -2,7 +2,11 @@ import { DataProviderService } from '@ghostfolio/api/services/data-provider/data
import { ExchangeRateDataService } from '@ghostfolio/api/services/exchange-rate-data/exchange-rate-data.service'; import { ExchangeRateDataService } from '@ghostfolio/api/services/exchange-rate-data/exchange-rate-data.service';
import { MarketDataService } from '@ghostfolio/api/services/market-data/market-data.service'; import { MarketDataService } from '@ghostfolio/api/services/market-data/market-data.service';
import { resetHours } from '@ghostfolio/common/helper'; import { resetHours } from '@ghostfolio/common/helper';
import { DataProviderInfo, ResponseError } from '@ghostfolio/common/interfaces'; import {
DataProviderInfo,
ResponseError,
UniqueAsset
} from '@ghostfolio/common/interfaces';
import { Injectable } from '@nestjs/common'; import { Injectable } from '@nestjs/common';
import { isBefore, isToday } from 'date-fns'; import { isBefore, isToday } from 'date-fns';
import { flatten, isEmpty, uniqBy } from 'lodash'; import { flatten, isEmpty, uniqBy } from 'lodash';
@ -52,6 +56,7 @@ export class CurrentRateService {
if (dataResultProvider?.[dataGatheringItem.symbol]?.marketPrice) { if (dataResultProvider?.[dataGatheringItem.symbol]?.marketPrice) {
result.push({ result.push({
dataSource: dataGatheringItem.dataSource,
date: today, date: today,
marketPriceInBaseCurrency: marketPriceInBaseCurrency:
this.exchangeRateDataService.toCurrency( this.exchangeRateDataService.toCurrency(
@ -75,27 +80,30 @@ export class CurrentRateService {
); );
} }
const symbols = dataGatheringItems.map((dataGatheringItem) => { const uniqueAssets: UniqueAsset[] = dataGatheringItems.map(
return dataGatheringItem.symbol; ({ dataSource, symbol }) => {
}); return { dataSource, symbol };
}
);
promises.push( promises.push(
this.marketDataService this.marketDataService
.getRange({ .getRange({
dateQuery, dateQuery,
symbols uniqueAssets
}) })
.then((data) => { .then((data) => {
return data.map((marketDataItem) => { return data.map(({ dataSource, date, marketPrice, symbol }) => {
return { return {
date: marketDataItem.date, dataSource,
date,
symbol,
marketPriceInBaseCurrency: marketPriceInBaseCurrency:
this.exchangeRateDataService.toCurrency( this.exchangeRateDataService.toCurrency(
marketDataItem.marketPrice, marketPrice,
currencies[marketDataItem.symbol], currencies[symbol],
userCurrency userCurrency
), )
symbol: marketDataItem.symbol
}; };
}); });
}) })
@ -112,7 +120,7 @@ export class CurrentRateService {
}; };
if (!isEmpty(quoteErrors)) { if (!isEmpty(quoteErrors)) {
for (const { symbol } of quoteErrors) { for (const { dataSource, symbol } of quoteErrors) {
try { try {
// If missing quote, fallback to the latest available historical market price // If missing quote, fallback to the latest available historical market price
let value: GetValueObject = response.values.find((currentValue) => { let value: GetValueObject = response.values.find((currentValue) => {
@ -121,6 +129,7 @@ export class CurrentRateService {
if (!value) { if (!value) {
value = { value = {
dataSource,
symbol, symbol,
date: today, date: today,
marketPriceInBaseCurrency: 0 marketPriceInBaseCurrency: 0

5
apps/api/src/app/portfolio/interfaces/get-value-object.interface.ts

@ -1,5 +1,6 @@
export interface GetValueObject { import { UniqueAsset } from '@ghostfolio/common/interfaces';
export interface GetValueObject extends UniqueAsset {
date: Date; date: Date;
marketPriceInBaseCurrency: number; marketPriceInBaseCurrency: number;
symbol: string;
} }

7
apps/api/src/app/symbol/symbol.service.ts

@ -40,7 +40,12 @@ export class SymbolService {
const marketData = await this.marketDataService.getRange({ const marketData = await this.marketDataService.getRange({
dateQuery: { gte: subDays(new Date(), days) }, dateQuery: { gte: subDays(new Date(), days) },
symbols: [dataGatheringItem.symbol] uniqueAssets: [
{
dataSource: dataGatheringItem.dataSource,
symbol: dataGatheringItem.symbol
}
]
}); });
historicalData = marketData.map(({ date, marketPrice: value }) => { historicalData = marketData.map(({ date, marketPrice: value }) => {

27
apps/api/src/app/user/user.service.ts

@ -7,9 +7,14 @@ import { TagService } from '@ghostfolio/api/services/tag/tag.service';
import { import {
DEFAULT_CURRENCY, DEFAULT_CURRENCY,
PROPERTY_IS_READ_ONLY_MODE, PROPERTY_IS_READ_ONLY_MODE,
PROPERTY_SYSTEM_MESSAGE,
locale locale
} from '@ghostfolio/common/config'; } from '@ghostfolio/common/config';
import { User as IUser, UserSettings } from '@ghostfolio/common/interfaces'; import {
User as IUser,
SystemMessage,
UserSettings
} from '@ghostfolio/common/interfaces';
import { import {
getPermissions, getPermissions,
hasRole, hasRole,
@ -48,6 +53,17 @@ export class UserService {
orderBy: { alias: 'asc' }, orderBy: { alias: 'asc' },
where: { GranteeUser: { id } } where: { GranteeUser: { id } }
}); });
let systemMessage: SystemMessage;
const systemMessageProperty = (await this.propertyService.getByKey(
PROPERTY_SYSTEM_MESSAGE
)) as SystemMessage;
if (systemMessageProperty?.targetGroups?.includes(subscription.type)) {
systemMessage = systemMessageProperty;
}
let tags = await this.tagService.getByUser(id); let tags = await this.tagService.getByUser(id);
if ( if (
@ -61,6 +77,7 @@ export class UserService {
id, id,
permissions, permissions,
subscription, subscription,
systemMessage,
tags, tags,
access: access.map((accessItem) => { access: access.map((accessItem) => {
return { return {
@ -110,7 +127,9 @@ export class UserService {
updatedAt updatedAt
} = await this.prismaService.user.findUnique({ } = await this.prismaService.user.findUnique({
include: { include: {
Account: true, Account: {
include: { Platform: true }
},
Analytics: true, Analytics: true,
Settings: true, Settings: true,
Subscription: true Subscription: true
@ -233,8 +252,8 @@ export class UserService {
currentPermissions.push(permissions.impersonateAllUsers); currentPermissions.push(permissions.impersonateAllUsers);
} }
user.Account = sortBy(user.Account, (account) => { user.Account = sortBy(user.Account, ({ name }) => {
return account.name; return name.toLowerCase();
}); });
user.permissions = currentPermissions.sort(); user.permissions = currentPermissions.sort();

80
apps/api/src/assets/sitemap.xml

@ -58,10 +58,18 @@
<loc>https://ghostfol.io/de/ressourcen/personal-finance-tools/open-source-alternative-zu-8figures</loc> <loc>https://ghostfol.io/de/ressourcen/personal-finance-tools/open-source-alternative-zu-8figures</loc>
<lastmod>${currentDate}T00:00:00+00:00</lastmod> <lastmod>${currentDate}T00:00:00+00:00</lastmod>
</url> </url>
<url>
<loc>https://ghostfol.io/de/ressourcen/personal-finance-tools/open-source-alternative-zu-allvue-systems</loc>
<lastmod>${currentDate}T00:00:00+00:00</lastmod>
</url>
<url> <url>
<loc>https://ghostfol.io/de/ressourcen/personal-finance-tools/open-source-alternative-zu-altoo</loc> <loc>https://ghostfol.io/de/ressourcen/personal-finance-tools/open-source-alternative-zu-altoo</loc>
<lastmod>${currentDate}T00:00:00+00:00</lastmod> <lastmod>${currentDate}T00:00:00+00:00</lastmod>
</url> </url>
<url>
<loc>https://ghostfol.io/de/ressourcen/personal-finance-tools/open-source-alternative-zu-basil-finance</loc>
<lastmod>${currentDate}T00:00:00+00:00</lastmod>
</url>
<url> <url>
<loc>https://ghostfol.io/de/ressourcen/personal-finance-tools/open-source-alternative-zu-beanvest</loc> <loc>https://ghostfol.io/de/ressourcen/personal-finance-tools/open-source-alternative-zu-beanvest</loc>
<lastmod>${currentDate}T00:00:00+00:00</lastmod> <lastmod>${currentDate}T00:00:00+00:00</lastmod>
@ -122,6 +130,10 @@
<loc>https://ghostfol.io/de/ressourcen/personal-finance-tools/open-source-alternative-zu-kubera</loc> <loc>https://ghostfol.io/de/ressourcen/personal-finance-tools/open-source-alternative-zu-kubera</loc>
<lastmod>${currentDate}T00:00:00+00:00</lastmod> <lastmod>${currentDate}T00:00:00+00:00</lastmod>
</url> </url>
<url>
<loc>https://ghostfol.io/de/ressourcen/personal-finance-tools/open-source-alternative-zu-magnifi</loc>
<lastmod>${currentDate}T00:00:00+00:00</lastmod>
</url>
<url> <url>
<loc>https://ghostfol.io/de/ressourcen/personal-finance-tools/open-source-alternative-zu-markets.sh</loc> <loc>https://ghostfol.io/de/ressourcen/personal-finance-tools/open-source-alternative-zu-markets.sh</loc>
<lastmod>${currentDate}T00:00:00+00:00</lastmod> <lastmod>${currentDate}T00:00:00+00:00</lastmod>
@ -130,6 +142,10 @@
<loc>https://ghostfol.io/de/ressourcen/personal-finance-tools/open-source-alternative-zu-maybe-finance</loc> <loc>https://ghostfol.io/de/ressourcen/personal-finance-tools/open-source-alternative-zu-maybe-finance</loc>
<lastmod>${currentDate}T00:00:00+00:00</lastmod> <lastmod>${currentDate}T00:00:00+00:00</lastmod>
</url> </url>
<url>
<loc>https://ghostfol.io/de/ressourcen/personal-finance-tools/open-source-alternative-zu-monarch-money</loc>
<lastmod>${currentDate}T00:00:00+00:00</lastmod>
</url>
<url> <url>
<loc>https://ghostfol.io/de/ressourcen/personal-finance-tools/open-source-alternative-zu-monse</loc> <loc>https://ghostfol.io/de/ressourcen/personal-finance-tools/open-source-alternative-zu-monse</loc>
<lastmod>${currentDate}T00:00:00+00:00</lastmod> <lastmod>${currentDate}T00:00:00+00:00</lastmod>
@ -202,6 +218,10 @@
<loc>https://ghostfol.io/de/ressourcen/personal-finance-tools/open-source-alternative-zu-yeekatee</loc> <loc>https://ghostfol.io/de/ressourcen/personal-finance-tools/open-source-alternative-zu-yeekatee</loc>
<lastmod>${currentDate}T00:00:00+00:00</lastmod> <lastmod>${currentDate}T00:00:00+00:00</lastmod>
</url> </url>
<url>
<loc>https://ghostfol.io/de/ressourcen/personal-finance-tools/open-source-alternative-zu-ynab</loc>
<lastmod>${currentDate}T00:00:00+00:00</lastmod>
</url>
<url> <url>
<loc>https://ghostfol.io/de/ueber-uns</loc> <loc>https://ghostfol.io/de/ueber-uns</loc>
<lastmod>${currentDate}T00:00:00+00:00</lastmod> <lastmod>${currentDate}T00:00:00+00:00</lastmod>
@ -348,10 +368,18 @@
<loc>https://ghostfol.io/en/resources/personal-finance-tools/open-source-alternative-to-8figures</loc> <loc>https://ghostfol.io/en/resources/personal-finance-tools/open-source-alternative-to-8figures</loc>
<lastmod>${currentDate}T00:00:00+00:00</lastmod> <lastmod>${currentDate}T00:00:00+00:00</lastmod>
</url> </url>
<url>
<loc>https://ghostfol.io/en/resources/personal-finance-tools/open-source-alternative-to-allvue-systems</loc>
<lastmod>${currentDate}T00:00:00+00:00</lastmod>
</url>
<url> <url>
<loc>https://ghostfol.io/en/resources/personal-finance-tools/open-source-alternative-to-altoo</loc> <loc>https://ghostfol.io/en/resources/personal-finance-tools/open-source-alternative-to-altoo</loc>
<lastmod>${currentDate}T00:00:00+00:00</lastmod> <lastmod>${currentDate}T00:00:00+00:00</lastmod>
</url> </url>
<url>
<loc>https://ghostfol.io/en/resources/personal-finance-tools/open-source-alternative-to-basil-finance</loc>
<lastmod>${currentDate}T00:00:00+00:00</lastmod>
</url>
<url> <url>
<loc>https://ghostfol.io/en/resources/personal-finance-tools/open-source-alternative-to-beanvest</loc> <loc>https://ghostfol.io/en/resources/personal-finance-tools/open-source-alternative-to-beanvest</loc>
<lastmod>${currentDate}T00:00:00+00:00</lastmod> <lastmod>${currentDate}T00:00:00+00:00</lastmod>
@ -412,6 +440,10 @@
<loc>https://ghostfol.io/en/resources/personal-finance-tools/open-source-alternative-to-kubera</loc> <loc>https://ghostfol.io/en/resources/personal-finance-tools/open-source-alternative-to-kubera</loc>
<lastmod>${currentDate}T00:00:00+00:00</lastmod> <lastmod>${currentDate}T00:00:00+00:00</lastmod>
</url> </url>
<url>
<loc>https://ghostfol.io/en/resources/personal-finance-tools/open-source-alternative-to-magnifi</loc>
<lastmod>${currentDate}T00:00:00+00:00</lastmod>
</url>
<url> <url>
<loc>https://ghostfol.io/en/resources/personal-finance-tools/open-source-alternative-to-markets.sh</loc> <loc>https://ghostfol.io/en/resources/personal-finance-tools/open-source-alternative-to-markets.sh</loc>
<lastmod>${currentDate}T00:00:00+00:00</lastmod> <lastmod>${currentDate}T00:00:00+00:00</lastmod>
@ -420,6 +452,10 @@
<loc>https://ghostfol.io/en/resources/personal-finance-tools/open-source-alternative-to-maybe-finance</loc> <loc>https://ghostfol.io/en/resources/personal-finance-tools/open-source-alternative-to-maybe-finance</loc>
<lastmod>${currentDate}T00:00:00+00:00</lastmod> <lastmod>${currentDate}T00:00:00+00:00</lastmod>
</url> </url>
<url>
<loc>https://ghostfol.io/en/resources/personal-finance-tools/open-source-alternative-to-monarch-money</loc>
<lastmod>${currentDate}T00:00:00+00:00</lastmod>
</url>
<url> <url>
<loc>https://ghostfol.io/en/resources/personal-finance-tools/open-source-alternative-to-monse</loc> <loc>https://ghostfol.io/en/resources/personal-finance-tools/open-source-alternative-to-monse</loc>
<lastmod>${currentDate}T00:00:00+00:00</lastmod> <lastmod>${currentDate}T00:00:00+00:00</lastmod>
@ -492,6 +528,10 @@
<loc>https://ghostfol.io/en/resources/personal-finance-tools/open-source-alternative-to-yeekatee</loc> <loc>https://ghostfol.io/en/resources/personal-finance-tools/open-source-alternative-to-yeekatee</loc>
<lastmod>${currentDate}T00:00:00+00:00</lastmod> <lastmod>${currentDate}T00:00:00+00:00</lastmod>
</url> </url>
<url>
<loc>https://ghostfol.io/en/resources/personal-finance-tools/open-source-alternative-to-ynab</loc>
<lastmod>${currentDate}T00:00:00+00:00</lastmod>
</url>
<url> <url>
<loc>https://ghostfol.io/es</loc> <loc>https://ghostfol.io/es</loc>
<lastmod>${currentDate}T00:00:00+00:00</lastmod> <lastmod>${currentDate}T00:00:00+00:00</lastmod>
@ -662,10 +702,18 @@
<loc>https://ghostfol.io/it/risorse/personal-finance-tools/alternativa-open-source-a-8figures</loc> <loc>https://ghostfol.io/it/risorse/personal-finance-tools/alternativa-open-source-a-8figures</loc>
<lastmod>${currentDate}T00:00:00+00:00</lastmod> <lastmod>${currentDate}T00:00:00+00:00</lastmod>
</url> </url>
<url>
<loc>https://ghostfol.io/it/risorse/personal-finance-tools/alternativa-open-source-a-allvue-systems</loc>
<lastmod>${currentDate}T00:00:00+00:00</lastmod>
</url>
<url> <url>
<loc>https://ghostfol.io/it/risorse/personal-finance-tools/alternativa-open-source-a-altoo</loc> <loc>https://ghostfol.io/it/risorse/personal-finance-tools/alternativa-open-source-a-altoo</loc>
<lastmod>${currentDate}T00:00:00+00:00</lastmod> <lastmod>${currentDate}T00:00:00+00:00</lastmod>
</url> </url>
<url>
<loc>https://ghostfol.io/it/risorse/personal-finance-tools/alternativa-open-source-a-basil-finance</loc>
<lastmod>${currentDate}T00:00:00+00:00</lastmod>
</url>
<url> <url>
<loc>https://ghostfol.io/it/risorse/personal-finance-tools/alternativa-open-source-a-beanvest</loc> <loc>https://ghostfol.io/it/risorse/personal-finance-tools/alternativa-open-source-a-beanvest</loc>
<lastmod>${currentDate}T00:00:00+00:00</lastmod> <lastmod>${currentDate}T00:00:00+00:00</lastmod>
@ -726,6 +774,10 @@
<loc>https://ghostfol.io/it/risorse/personal-finance-tools/alternativa-open-source-a-kubera</loc> <loc>https://ghostfol.io/it/risorse/personal-finance-tools/alternativa-open-source-a-kubera</loc>
<lastmod>${currentDate}T00:00:00+00:00</lastmod> <lastmod>${currentDate}T00:00:00+00:00</lastmod>
</url> </url>
<url>
<loc>https://ghostfol.io/it/risorse/personal-finance-tools/alternativa-open-source-a-magnifi</loc>
<lastmod>${currentDate}T00:00:00+00:00</lastmod>
</url>
<url> <url>
<loc>https://ghostfol.io/it/risorse/personal-finance-tools/alternativa-open-source-a-markets.sh</loc> <loc>https://ghostfol.io/it/risorse/personal-finance-tools/alternativa-open-source-a-markets.sh</loc>
<lastmod>${currentDate}T00:00:00+00:00</lastmod> <lastmod>${currentDate}T00:00:00+00:00</lastmod>
@ -734,6 +786,10 @@
<loc>https://ghostfol.io/it/risorse/personal-finance-tools/alternativa-open-source-a-maybe-finance</loc> <loc>https://ghostfol.io/it/risorse/personal-finance-tools/alternativa-open-source-a-maybe-finance</loc>
<lastmod>${currentDate}T00:00:00+00:00</lastmod> <lastmod>${currentDate}T00:00:00+00:00</lastmod>
</url> </url>
<url>
<loc>https://ghostfol.io/it/risorse/personal-finance-tools/alternativa-open-source-a-monarch-money</loc>
<lastmod>${currentDate}T00:00:00+00:00</lastmod>
</url>
<url> <url>
<loc>https://ghostfol.io/it/risorse/personal-finance-tools/alternativa-open-source-a-monse</loc> <loc>https://ghostfol.io/it/risorse/personal-finance-tools/alternativa-open-source-a-monse</loc>
<lastmod>${currentDate}T00:00:00+00:00</lastmod> <lastmod>${currentDate}T00:00:00+00:00</lastmod>
@ -806,6 +862,10 @@
<loc>https://ghostfol.io/it/risorse/personal-finance-tools/alternativa-open-source-a-yeekatee</loc> <loc>https://ghostfol.io/it/risorse/personal-finance-tools/alternativa-open-source-a-yeekatee</loc>
<lastmod>${currentDate}T00:00:00+00:00</lastmod> <lastmod>${currentDate}T00:00:00+00:00</lastmod>
</url> </url>
<url>
<loc>https://ghostfol.io/it/risorse/personal-finance-tools/alternativa-open-source-a-ynab</loc>
<lastmod>${currentDate}T00:00:00+00:00</lastmod>
</url>
<url> <url>
<loc>https://ghostfol.io/nl</loc> <loc>https://ghostfol.io/nl</loc>
<lastmod>${currentDate}T00:00:00+00:00</lastmod> <lastmod>${currentDate}T00:00:00+00:00</lastmod>
@ -822,10 +882,18 @@
<loc>https://ghostfol.io/nl/bronnen/personal-finance-tools/open-source-alternatief-voor-8figures</loc> <loc>https://ghostfol.io/nl/bronnen/personal-finance-tools/open-source-alternatief-voor-8figures</loc>
<lastmod>${currentDate}T00:00:00+00:00</lastmod> <lastmod>${currentDate}T00:00:00+00:00</lastmod>
</url> </url>
<url>
<loc>https://ghostfol.io/nl/bronnen/personal-finance-tools/open-source-alternatief-voor-allvue-systems</loc>
<lastmod>${currentDate}T00:00:00+00:00</lastmod>
</url>
<url> <url>
<loc>https://ghostfol.io/nl/bronnen/personal-finance-tools/open-source-alternatief-voor-altoo</loc> <loc>https://ghostfol.io/nl/bronnen/personal-finance-tools/open-source-alternatief-voor-altoo</loc>
<lastmod>${currentDate}T00:00:00+00:00</lastmod> <lastmod>${currentDate}T00:00:00+00:00</lastmod>
</url> </url>
<url>
<loc>https://ghostfol.io/nl/bronnen/personal-finance-tools/open-source-alternatief-voor-basil-finance</loc>
<lastmod>${currentDate}T00:00:00+00:00</lastmod>
</url>
<url> <url>
<loc>https://ghostfol.io/nl/bronnen/personal-finance-tools/open-source-alternatief-voor-beanvest</loc> <loc>https://ghostfol.io/nl/bronnen/personal-finance-tools/open-source-alternatief-voor-beanvest</loc>
<lastmod>${currentDate}T00:00:00+00:00</lastmod> <lastmod>${currentDate}T00:00:00+00:00</lastmod>
@ -886,6 +954,10 @@
<loc>https://ghostfol.io/nl/bronnen/personal-finance-tools/open-source-alternatief-voor-kubera</loc> <loc>https://ghostfol.io/nl/bronnen/personal-finance-tools/open-source-alternatief-voor-kubera</loc>
<lastmod>${currentDate}T00:00:00+00:00</lastmod> <lastmod>${currentDate}T00:00:00+00:00</lastmod>
</url> </url>
<url>
<loc>https://ghostfol.io/nl/bronnen/personal-finance-tools/open-source-alternatief-voor-magnifi</loc>
<lastmod>${currentDate}T00:00:00+00:00</lastmod>
</url>
<url> <url>
<loc>https://ghostfol.io/nl/bronnen/personal-finance-tools/open-source-alternatief-voor-markets.sh</loc> <loc>https://ghostfol.io/nl/bronnen/personal-finance-tools/open-source-alternatief-voor-markets.sh</loc>
<lastmod>${currentDate}T00:00:00+00:00</lastmod> <lastmod>${currentDate}T00:00:00+00:00</lastmod>
@ -894,6 +966,10 @@
<loc>https://ghostfol.io/nl/bronnen/personal-finance-tools/open-source-alternatief-voor-maybe-finance</loc> <loc>https://ghostfol.io/nl/bronnen/personal-finance-tools/open-source-alternatief-voor-maybe-finance</loc>
<lastmod>${currentDate}T00:00:00+00:00</lastmod> <lastmod>${currentDate}T00:00:00+00:00</lastmod>
</url> </url>
<url>
<loc>https://ghostfol.io/nl/bronnen/personal-finance-tools/open-source-alternatief-voor-monarch-money</loc>
<lastmod>${currentDate}T00:00:00+00:00</lastmod>
</url>
<url> <url>
<loc>https://ghostfol.io/nl/bronnen/personal-finance-tools/open-source-alternatief-voor-monse</loc> <loc>https://ghostfol.io/nl/bronnen/personal-finance-tools/open-source-alternatief-voor-monse</loc>
<lastmod>${currentDate}T00:00:00+00:00</lastmod> <lastmod>${currentDate}T00:00:00+00:00</lastmod>
@ -966,6 +1042,10 @@
<loc>https://ghostfol.io/nl/bronnen/personal-finance-tools/open-source-alternatief-voor-yeekatee</loc> <loc>https://ghostfol.io/nl/bronnen/personal-finance-tools/open-source-alternatief-voor-yeekatee</loc>
<lastmod>${currentDate}T00:00:00+00:00</lastmod> <lastmod>${currentDate}T00:00:00+00:00</lastmod>
</url> </url>
<url>
<loc>https://ghostfol.io/nl/bronnen/personal-finance-tools/open-source-alternatief-voor-ynab</loc>
<lastmod>${currentDate}T00:00:00+00:00</lastmod>
</url>
<url> <url>
<loc>https://ghostfol.io/nl/functionaliteiten</loc> <loc>https://ghostfol.io/nl/functionaliteiten</loc>
<lastmod>${currentDate}T00:00:00+00:00</lastmod> <lastmod>${currentDate}T00:00:00+00:00</lastmod>

3
apps/api/src/services/data-provider/alpha-vantage/alpha-vantage.service.ts

@ -5,6 +5,7 @@ import {
IDataProviderHistoricalResponse, IDataProviderHistoricalResponse,
IDataProviderResponse IDataProviderResponse
} from '@ghostfolio/api/services/interfaces/interfaces'; } from '@ghostfolio/api/services/interfaces/interfaces';
import { DEFAULT_REQUEST_TIMEOUT } from '@ghostfolio/common/config';
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 } from '@nestjs/common'; import { Injectable } from '@nestjs/common';
@ -106,8 +107,10 @@ export class AlphaVantageService implements DataProviderInterface {
} }
public async getQuotes({ public async getQuotes({
requestTimeout = DEFAULT_REQUEST_TIMEOUT,
symbols symbols
}: { }: {
requestTimeout?: number;
symbols: string[]; symbols: string[];
}): Promise<{ [symbol: string]: IDataProviderResponse }> { }): Promise<{ [symbol: string]: IDataProviderResponse }> {
return {}; return {};

4
apps/api/src/services/data-provider/coingecko/coingecko.service.ts

@ -135,8 +135,10 @@ export class CoinGeckoService implements DataProviderInterface {
} }
public async getQuotes({ public async getQuotes({
requestTimeout = DEFAULT_REQUEST_TIMEOUT,
symbols symbols
}: { }: {
requestTimeout?: number;
symbols: string[]; symbols: string[];
}): Promise<{ [symbol: string]: IDataProviderResponse }> { }): Promise<{ [symbol: string]: IDataProviderResponse }> {
const response: { [symbol: string]: IDataProviderResponse } = {}; const response: { [symbol: string]: IDataProviderResponse } = {};
@ -150,7 +152,7 @@ export class CoinGeckoService implements DataProviderInterface {
setTimeout(() => { setTimeout(() => {
abortController.abort(); abortController.abort();
}, DEFAULT_REQUEST_TIMEOUT); }, requestTimeout);
const quotes = await got( const quotes = await got(
`${this.URL}/simple/price?ids=${symbols.join( `${this.URL}/simple/price?ids=${symbols.join(

2
apps/api/src/services/data-provider/data-enhancer/data-enhancer.service.ts

@ -2,6 +2,7 @@ import { DataEnhancerInterface } from '@ghostfolio/api/services/data-provider/in
import { HttpException, Inject, Injectable } from '@nestjs/common'; import { HttpException, Inject, Injectable } from '@nestjs/common';
import { Prisma } from '@prisma/client'; import { Prisma } from '@prisma/client';
import { StatusCodes, getReasonPhrase } from 'http-status-codes'; import { StatusCodes, getReasonPhrase } from 'http-status-codes';
import ms from 'ms';
@Injectable() @Injectable()
export class DataEnhancerService { export class DataEnhancerService {
@ -24,6 +25,7 @@ export class DataEnhancerService {
try { try {
const assetProfile = await dataEnhancer.enhance({ const assetProfile = await dataEnhancer.enhance({
requestTimeout: ms('30 seconds'),
response: { response: {
assetClass: 'EQUITY', assetClass: 'EQUITY',
assetSubClass: 'ETF' assetSubClass: 'ETF'

4
apps/api/src/services/data-provider/data-enhancer/openfigi/openfigi.service.ts

@ -15,9 +15,11 @@ export class OpenFigiDataEnhancerService implements DataEnhancerInterface {
) {} ) {}
public async enhance({ public async enhance({
requestTimeout = DEFAULT_REQUEST_TIMEOUT,
response, response,
symbol symbol
}: { }: {
requestTimeout?: number;
response: Partial<SymbolProfile>; response: Partial<SymbolProfile>;
symbol: string; symbol: string;
}): Promise<Partial<SymbolProfile>> { }): Promise<Partial<SymbolProfile>> {
@ -45,7 +47,7 @@ export class OpenFigiDataEnhancerService implements DataEnhancerInterface {
setTimeout(() => { setTimeout(() => {
abortController.abort(); abortController.abort();
}, DEFAULT_REQUEST_TIMEOUT); }, requestTimeout);
const mappings = await got const mappings = await got
.post(`${OpenFigiDataEnhancerService.baseUrl}/v3/mapping`, { .post(`${OpenFigiDataEnhancerService.baseUrl}/v3/mapping`, {

4
apps/api/src/services/data-provider/data-enhancer/trackinsight/trackinsight.service.ts

@ -21,9 +21,11 @@ export class TrackinsightDataEnhancerService implements DataEnhancerInterface {
}; };
public async enhance({ public async enhance({
requestTimeout = DEFAULT_REQUEST_TIMEOUT,
response, response,
symbol symbol
}: { }: {
requestTimeout?: number;
response: Partial<SymbolProfile>; response: Partial<SymbolProfile>;
symbol: string; symbol: string;
}): Promise<Partial<SymbolProfile>> { }): Promise<Partial<SymbolProfile>> {
@ -35,7 +37,7 @@ export class TrackinsightDataEnhancerService implements DataEnhancerInterface {
setTimeout(() => { setTimeout(() => {
abortController.abort(); abortController.abort();
}, DEFAULT_REQUEST_TIMEOUT); }, requestTimeout);
const profile = await got( const profile = await got(
`${TrackinsightDataEnhancerService.baseUrl}/funds/${symbol}.json`, `${TrackinsightDataEnhancerService.baseUrl}/funds/${symbol}.json`,

8
apps/api/src/services/data-provider/data-enhancer/yahoo-finance/yahoo-finance.service.ts

@ -1,6 +1,10 @@
import { CryptocurrencyService } from '@ghostfolio/api/services/cryptocurrency/cryptocurrency.service'; import { CryptocurrencyService } from '@ghostfolio/api/services/cryptocurrency/cryptocurrency.service';
import { DataEnhancerInterface } from '@ghostfolio/api/services/data-provider/interfaces/data-enhancer.interface'; import { DataEnhancerInterface } from '@ghostfolio/api/services/data-provider/interfaces/data-enhancer.interface';
import { DEFAULT_CURRENCY, UNKNOWN_KEY } from '@ghostfolio/common/config'; import {
DEFAULT_CURRENCY,
DEFAULT_REQUEST_TIMEOUT,
UNKNOWN_KEY
} from '@ghostfolio/common/config';
import { isCurrency } from '@ghostfolio/common/helper'; import { isCurrency } from '@ghostfolio/common/helper';
import { Injectable, Logger } from '@nestjs/common'; import { Injectable, Logger } from '@nestjs/common';
import { import {
@ -72,9 +76,11 @@ export class YahooFinanceDataEnhancerService implements DataEnhancerInterface {
} }
public async enhance({ public async enhance({
requestTimeout = DEFAULT_REQUEST_TIMEOUT,
response, response,
symbol symbol
}: { }: {
requestTimeout?: number;
response: Partial<SymbolProfile>; response: Partial<SymbolProfile>;
symbol: string; symbol: string;
}): Promise<Partial<SymbolProfile>> { }): Promise<Partial<SymbolProfile>> {

6
apps/api/src/services/data-provider/data-provider.service.ts

@ -17,6 +17,7 @@ import { Inject, Injectable, Logger } from '@nestjs/common';
import { DataSource, MarketData, SymbolProfile } from '@prisma/client'; import { DataSource, MarketData, SymbolProfile } from '@prisma/client';
import { format, isValid } from 'date-fns'; import { format, isValid } from 'date-fns';
import { groupBy, isEmpty, isNumber } from 'lodash'; import { groupBy, isEmpty, isNumber } from 'lodash';
import ms from 'ms';
@Injectable() @Injectable()
export class DataProviderService { export class DataProviderService {
@ -52,6 +53,7 @@ export class DataProviderService {
symbol symbol
} }
], ],
requestTimeout: ms('30 seconds'),
useCache: false useCache: false
}); });
@ -236,9 +238,11 @@ export class DataProviderService {
public async getQuotes({ public async getQuotes({
items, items,
requestTimeout,
useCache = true useCache = true
}: { }: {
items: UniqueAsset[]; items: UniqueAsset[];
requestTimeout?: number;
useCache?: boolean; useCache?: boolean;
}): Promise<{ }): Promise<{
[symbol: string]: IDataProviderResponse; [symbol: string]: IDataProviderResponse;
@ -312,7 +316,7 @@ export class DataProviderService {
); );
const promise = Promise.resolve( const promise = Promise.resolve(
dataProvider.getQuotes({ symbols: symbolsChunk }) dataProvider.getQuotes({ requestTimeout, symbols: symbolsChunk })
); );
promises.push( promises.push(

4
apps/api/src/services/data-provider/eod-historical-data/eod-historical-data.service.ts

@ -132,8 +132,10 @@ export class EodHistoricalDataService implements DataProviderInterface {
} }
public async getQuotes({ public async getQuotes({
requestTimeout = DEFAULT_REQUEST_TIMEOUT,
symbols symbols
}: { }: {
requestTimeout?: number;
symbols: string[]; symbols: string[];
}): Promise<{ [symbol: string]: IDataProviderResponse }> { }): Promise<{ [symbol: string]: IDataProviderResponse }> {
let response: { [symbol: string]: IDataProviderResponse } = {}; let response: { [symbol: string]: IDataProviderResponse } = {};
@ -151,7 +153,7 @@ export class EodHistoricalDataService implements DataProviderInterface {
setTimeout(() => { setTimeout(() => {
abortController.abort(); abortController.abort();
}, DEFAULT_REQUEST_TIMEOUT); }, requestTimeout);
const realTimeResponse = await got( const realTimeResponse = await got(
`${this.URL}/real-time/${eodHistoricalDataSymbols[0]}?api_token=${ `${this.URL}/real-time/${eodHistoricalDataSymbols[0]}?api_token=${

8
apps/api/src/services/data-provider/financial-modeling-prep/financial-modeling-prep.service.ts

@ -114,8 +114,10 @@ export class FinancialModelingPrepService implements DataProviderInterface {
} }
public async getQuotes({ public async getQuotes({
requestTimeout = DEFAULT_REQUEST_TIMEOUT,
symbols symbols
}: { }: {
requestTimeout?: number;
symbols: string[]; symbols: string[];
}): Promise<{ [symbol: string]: IDataProviderResponse }> { }): Promise<{ [symbol: string]: IDataProviderResponse }> {
const response: { [symbol: string]: IDataProviderResponse } = {}; const response: { [symbol: string]: IDataProviderResponse } = {};
@ -129,9 +131,9 @@ export class FinancialModelingPrepService implements DataProviderInterface {
setTimeout(() => { setTimeout(() => {
abortController.abort(); abortController.abort();
}, DEFAULT_REQUEST_TIMEOUT); }, requestTimeout);
const response = await got( const quotes = await got(
`${this.URL}/quote/${symbols.join(',')}?apikey=${this.apiKey}`, `${this.URL}/quote/${symbols.join(',')}?apikey=${this.apiKey}`,
{ {
// @ts-ignore // @ts-ignore
@ -139,7 +141,7 @@ export class FinancialModelingPrepService implements DataProviderInterface {
} }
).json<any>(); ).json<any>();
for (const { price, symbol } of response) { for (const { price, symbol } of quotes) {
response[symbol] = { response[symbol] = {
currency: DEFAULT_CURRENCY, currency: DEFAULT_CURRENCY,
dataProviderInfo: this.getDataProviderInfo(), dataProviderInfo: this.getDataProviderInfo(),

3
apps/api/src/services/data-provider/google-sheets/google-sheets.service.ts

@ -7,6 +7,7 @@ import {
} from '@ghostfolio/api/services/interfaces/interfaces'; } from '@ghostfolio/api/services/interfaces/interfaces';
import { PrismaService } from '@ghostfolio/api/services/prisma/prisma.service'; import { PrismaService } from '@ghostfolio/api/services/prisma/prisma.service';
import { SymbolProfileService } from '@ghostfolio/api/services/symbol-profile/symbol-profile.service'; import { SymbolProfileService } from '@ghostfolio/api/services/symbol-profile/symbol-profile.service';
import { DEFAULT_REQUEST_TIMEOUT } from '@ghostfolio/common/config';
import { DATE_FORMAT, parseDate } from '@ghostfolio/common/helper'; import { DATE_FORMAT, parseDate } 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';
@ -100,8 +101,10 @@ export class GoogleSheetsService implements DataProviderInterface {
} }
public async getQuotes({ public async getQuotes({
requestTimeout = DEFAULT_REQUEST_TIMEOUT,
symbols symbols
}: { }: {
requestTimeout?: number;
symbols: string[]; symbols: string[];
}): Promise<{ [symbol: string]: IDataProviderResponse }> { }): Promise<{ [symbol: string]: IDataProviderResponse }> {
const response: { [symbol: string]: IDataProviderResponse } = {}; const response: { [symbol: string]: IDataProviderResponse } = {};

2
apps/api/src/services/data-provider/interfaces/data-enhancer.interface.ts

@ -2,9 +2,11 @@ import { SymbolProfile } from '@prisma/client';
export interface DataEnhancerInterface { export interface DataEnhancerInterface {
enhance({ enhance({
requestTimeout,
response, response,
symbol symbol
}: { }: {
requestTimeout?: number;
response: Partial<SymbolProfile>; response: Partial<SymbolProfile>;
symbol: string; symbol: string;
}): Promise<Partial<SymbolProfile>>; }): Promise<Partial<SymbolProfile>>;

2
apps/api/src/services/data-provider/interfaces/data-provider.interface.ts

@ -37,8 +37,10 @@ export interface DataProviderInterface {
getName(): DataSource; getName(): DataSource;
getQuotes({ getQuotes({
requestTimeout,
symbols symbols
}: { }: {
requestTimeout?: number;
symbols: string[]; symbols: string[];
}): Promise<{ [symbol: string]: IDataProviderResponse }>; }): Promise<{ [symbol: string]: IDataProviderResponse }>;

2
apps/api/src/services/data-provider/manual/manual.service.ts

@ -134,8 +134,10 @@ export class ManualService implements DataProviderInterface {
} }
public async getQuotes({ public async getQuotes({
requestTimeout = DEFAULT_REQUEST_TIMEOUT,
symbols symbols
}: { }: {
requestTimeout?: number;
symbols: string[]; symbols: string[];
}): Promise<{ [symbol: string]: IDataProviderResponse }> { }): Promise<{ [symbol: string]: IDataProviderResponse }> {
const response: { [symbol: string]: IDataProviderResponse } = {}; const response: { [symbol: string]: IDataProviderResponse } = {};

2
apps/api/src/services/data-provider/rapid-api/rapid-api.service.ts

@ -88,8 +88,10 @@ export class RapidApiService implements DataProviderInterface {
} }
public async getQuotes({ public async getQuotes({
requestTimeout = DEFAULT_REQUEST_TIMEOUT,
symbols symbols
}: { }: {
requestTimeout?: number;
symbols: string[]; symbols: string[];
}): Promise<{ [symbol: string]: IDataProviderResponse }> { }): Promise<{ [symbol: string]: IDataProviderResponse }> {
if (symbols.length <= 0) { if (symbols.length <= 0) {

7
apps/api/src/services/data-provider/yahoo-finance/yahoo-finance.service.ts

@ -6,7 +6,10 @@ import {
IDataProviderHistoricalResponse, IDataProviderHistoricalResponse,
IDataProviderResponse IDataProviderResponse
} from '@ghostfolio/api/services/interfaces/interfaces'; } from '@ghostfolio/api/services/interfaces/interfaces';
import { DEFAULT_CURRENCY } from '@ghostfolio/common/config'; import {
DEFAULT_CURRENCY,
DEFAULT_REQUEST_TIMEOUT
} from '@ghostfolio/common/config';
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';
@ -157,8 +160,10 @@ export class YahooFinanceService implements DataProviderInterface {
} }
public async getQuotes({ public async getQuotes({
requestTimeout = DEFAULT_REQUEST_TIMEOUT,
symbols symbols
}: { }: {
requestTimeout?: number;
symbols: string[]; symbols: string[];
}): Promise<{ [symbol: string]: IDataProviderResponse }> { }): Promise<{ [symbol: string]: IDataProviderResponse }> {
const response: { [symbol: string]: IDataProviderResponse } = {}; const response: { [symbol: string]: IDataProviderResponse } = {};

24
apps/api/src/services/exchange-rate-data/exchange-rate-data.service.ts

@ -95,6 +95,30 @@ export class ExchangeRateDataService {
const [currency1, currency2] = symbol.match(/.{1,3}/g); const [currency1, currency2] = symbol.match(/.{1,3}/g);
const [date] = Object.keys(result[symbol]); const [date] = Object.keys(result[symbol]);
// Add derived currencies
if (currency2 === 'GBP') {
resultExtended[`${currency1}GBp`] = {
[date]: {
marketPrice:
result[`${currency1}${currency2}`][date].marketPrice * 100
}
};
} else if (currency2 === 'ILS') {
resultExtended[`${currency1}ILA`] = {
[date]: {
marketPrice:
result[`${currency1}${currency2}`][date].marketPrice * 100
}
};
} else if (currency2 === 'ZAR') {
resultExtended[`${currency1}ZAc`] = {
[date]: {
marketPrice:
result[`${currency1}${currency2}`][date].marketPrice * 100
}
};
}
// Calculate the opposite direction // Calculate the opposite direction
resultExtended[`${currency2}${currency1}`] = { resultExtended[`${currency2}${currency1}`] = {
[date]: { [date]: {

17
apps/api/src/services/market-data/market-data.service.ts

@ -59,10 +59,10 @@ export class MarketDataService {
public async getRange({ public async getRange({
dateQuery, dateQuery,
symbols uniqueAssets
}: { }: {
dateQuery: DateQuery; dateQuery: DateQuery;
symbols: string[]; uniqueAssets: UniqueAsset[];
}): Promise<MarketData[]> { }): Promise<MarketData[]> {
return await this.prismaService.marketData.findMany({ return await this.prismaService.marketData.findMany({
orderBy: [ orderBy: [
@ -74,10 +74,17 @@ export class MarketDataService {
} }
], ],
where: { where: {
date: dateQuery, OR: uniqueAssets.map(({ dataSource, symbol }) => {
symbol: { return {
in: symbols AND: [
{
dataSource,
symbol,
date: dateQuery
} }
]
};
})
} }
}); });
} }

11
apps/api/src/services/symbol-profile/symbol-profile-overwrite.module.ts

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

68
apps/api/src/services/symbol-profile/symbol-profile-overwrite.service.ts

@ -0,0 +1,68 @@
import { PrismaService } from '@ghostfolio/api/services/prisma/prisma.service';
import { Injectable } from '@nestjs/common';
import { DataSource, Prisma, SymbolProfileOverrides } from '@prisma/client';
@Injectable()
export class SymbolProfileOverwriteService {
public constructor(private readonly prismaService: PrismaService) {}
public async add(
assetProfileOverwrite: Prisma.SymbolProfileOverridesCreateInput
): Promise<SymbolProfileOverrides | never> {
return this.prismaService.symbolProfileOverrides.create({
data: assetProfileOverwrite
});
}
public async delete(symbolProfileId: string) {
return this.prismaService.symbolProfileOverrides.delete({
where: { symbolProfileId: symbolProfileId }
});
}
public updateSymbolProfileOverrides({
assetClass,
assetSubClass,
name,
countries,
sectors,
url,
symbolProfileId
}: Prisma.SymbolProfileOverridesUpdateInput & { symbolProfileId: string }) {
return this.prismaService.symbolProfileOverrides.update({
data: {
assetClass,
assetSubClass,
name,
countries,
sectors,
url
},
where: { symbolProfileId: symbolProfileId }
});
}
public async GetSymbolProfileId(
Symbol: string,
datasource: DataSource
): Promise<string> {
let SymbolProfileId = await this.prismaService.symbolProfile
.findFirst({
where: {
symbol: Symbol,
dataSource: datasource
}
})
.then((s) => s.id);
let symbolProfileIdSaved = await this.prismaService.symbolProfileOverrides
.findFirst({
where: {
symbolProfileId: SymbolProfileId
}
})
.then((s) => s?.symbolProfileId);
return symbolProfileIdSaved;
}
}

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

@ -1,6 +1,6 @@
<header> <header>
<div <div
*ngIf="canCreateAccount || (info?.systemMessage && user)" *ngIf="canCreateAccount || user?.systemMessage"
class="info-message-container" class="info-message-container"
> >
<div class="info-message-inner-container position-fixed w-100"> <div class="info-message-inner-container position-fixed w-100">
@ -19,11 +19,11 @@
</div></a </div></a
> >
<div <div
*ngIf="!canCreateAccount && info?.systemMessage && user" *ngIf="!canCreateAccount && user?.systemMessage"
class="cursor-pointer d-inline-block info-message text-truncate" class="cursor-pointer d-inline-block info-message text-truncate"
(click)="onShowSystemMessage()" (click)="onClickSystemMessage()"
> >
{{ info.systemMessage }} {{ user.systemMessage.message }}
</div> </div>
</div> </div>
</div> </div>

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

@ -155,10 +155,7 @@ export class AppComponent implements OnDestroy, OnInit {
); );
this.hasInfoMessage = this.hasInfoMessage =
hasPermission( this.canCreateAccount || !!this.user?.systemMessage;
this.user?.permissions,
permissions.createUserAccount
) || !!this.info.systemMessage;
this.initializeTheme(this.user?.settings.colorScheme); this.initializeTheme(this.user?.settings.colorScheme);
@ -166,12 +163,16 @@ export class AppComponent implements OnDestroy, OnInit {
}); });
} }
public onCreateAccount() { public onClickSystemMessage() {
this.tokenStorageService.signOut(); if (this.user.systemMessage.routerLink) {
this.router.navigate(this.user.systemMessage.routerLink);
} else {
alert(this.user.systemMessage.message);
}
} }
public onShowSystemMessage() { public onCreateAccount() {
alert(this.info.systemMessage); this.tokenStorageService.signOut();
} }
public onSignOut() { public onSignOut() {

4
apps/client/src/app/components/admin-market-data/admin-market-data.component.ts

@ -20,6 +20,7 @@ import { Filter, UniqueAsset, User } from '@ghostfolio/common/interfaces';
import { AdminMarketDataItem } from '@ghostfolio/common/interfaces/admin-market-data.interface'; import { AdminMarketDataItem } from '@ghostfolio/common/interfaces/admin-market-data.interface';
import { translate } from '@ghostfolio/ui/i18n'; import { translate } from '@ghostfolio/ui/i18n';
import { AssetSubClass, DataSource, Prisma } from '@prisma/client'; import { AssetSubClass, DataSource, Prisma } from '@prisma/client';
import { isUUID } from 'class-validator';
import { DeviceDetectorService } from 'ngx-device-detector'; import { DeviceDetectorService } from 'ngx-device-detector';
import { Subject } from 'rxjs'; import { Subject } from 'rxjs';
import { distinctUntilChanged, switchMap, takeUntil } from 'rxjs/operators'; import { distinctUntilChanged, switchMap, takeUntil } from 'rxjs/operators';
@ -83,7 +84,7 @@ export class AdminMarketDataComponent
public defaultDateFormat: string; public defaultDateFormat: string;
public deviceType: string; public deviceType: string;
public displayedColumns = [ public displayedColumns = [
'symbol', 'nameWithSymbol',
'dataSource', 'dataSource',
'assetClass', 'assetClass',
'assetSubClass', 'assetSubClass',
@ -97,6 +98,7 @@ export class AdminMarketDataComponent
]; ];
public filters$ = new Subject<Filter[]>(); public filters$ = new Subject<Filter[]>();
public isLoading = false; public isLoading = false;
public isUUID = isUUID;
public placeholder = ''; public placeholder = '';
public pageSize = DEFAULT_PAGE_SIZE; public pageSize = DEFAULT_PAGE_SIZE;
public totalItems = 0; public totalItems = 0;

18
apps/client/src/app/components/admin-market-data/admin-market-data.html

@ -28,6 +28,24 @@
</td> </td>
</ng-container> </ng-container>
<ng-container matColumnDef="nameWithSymbol">
<th
*matHeaderCellDef
class="px-1"
mat-header-cell
mat-sort-header="symbol"
>
<ng-container i18n>Name</ng-container>
</th>
<td *matCellDef="let element" class="line-height-1 px-1" mat-cell>
<div class="text-truncate">{{ element.name }}</div>
<div *ngIf="!isUUID(element.symbol)">
<small class="text-muted">{{ element.symbol | gfSymbol }}</small>
</div>
</td>
<td *matFooterCellDef class="px-1" mat-footer-cell></td>
</ng-container>
<ng-container matColumnDef="dataSource"> <ng-container matColumnDef="dataSource">
<th *matHeaderCellDef class="px-1" mat-header-cell mat-sort-header> <th *matHeaderCellDef class="px-1" mat-header-cell mat-sort-header>
<ng-container i18n>Data Source</ng-container> <ng-container i18n>Data Source</ng-container>

2
apps/client/src/app/components/admin-market-data/admin-market-data.module.ts

@ -6,6 +6,7 @@ import { MatPaginatorModule } from '@angular/material/paginator';
import { MatSortModule } from '@angular/material/sort'; import { MatSortModule } from '@angular/material/sort';
import { MatTableModule } from '@angular/material/table'; import { MatTableModule } from '@angular/material/table';
import { RouterModule } from '@angular/router'; import { RouterModule } from '@angular/router';
import { GfSymbolModule } from '@ghostfolio/client/pipes/symbol/symbol.module';
import { GfActivitiesFilterModule } from '@ghostfolio/ui/activities-filter/activities-filter.module'; import { GfActivitiesFilterModule } from '@ghostfolio/ui/activities-filter/activities-filter.module';
import { NgxSkeletonLoaderModule } from 'ngx-skeleton-loader'; import { NgxSkeletonLoaderModule } from 'ngx-skeleton-loader';
@ -20,6 +21,7 @@ import { GfCreateAssetProfileDialogModule } from './create-asset-profile-dialog/
GfActivitiesFilterModule, GfActivitiesFilterModule,
GfAssetProfileDialogModule, GfAssetProfileDialogModule,
GfCreateAssetProfileDialogModule, GfCreateAssetProfileDialogModule,
GfSymbolModule,
MatButtonModule, MatButtonModule,
MatMenuModule, MatMenuModule,
MatPaginatorModule, MatPaginatorModule,

46
apps/client/src/app/components/admin-market-data/asset-profile-dialog/asset-profile-dialog.component.ts

@ -10,6 +10,7 @@ import {
} from '@angular/core'; } from '@angular/core';
import { FormBuilder, FormControl, Validators } from '@angular/forms'; import { FormBuilder, FormControl, Validators } from '@angular/forms';
import { MAT_DIALOG_DATA, MatDialogRef } from '@angular/material/dialog'; import { MAT_DIALOG_DATA, MatDialogRef } from '@angular/material/dialog';
import { MatSnackBar } from '@angular/material/snack-bar';
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 { DataService } from '@ghostfolio/client/services/data.service'; import { DataService } from '@ghostfolio/client/services/data.service';
@ -28,8 +29,8 @@ import {
} from '@prisma/client'; } from '@prisma/client';
import { format } from 'date-fns'; import { format } from 'date-fns';
import { parse as csvToJson } from 'papaparse'; import { parse as csvToJson } from 'papaparse';
import { Subject } from 'rxjs'; import { EMPTY, Subject } from 'rxjs';
import { takeUntil } from 'rxjs/operators'; import { catchError, takeUntil } from 'rxjs/operators';
import { AssetProfileDialogParams } from './interfaces/interfaces'; import { AssetProfileDialogParams } from './interfaces/interfaces';
import { MatAutocompleteSelectedEvent } from '@angular/material/autocomplete'; import { MatAutocompleteSelectedEvent } from '@angular/material/autocomplete';
@ -57,6 +58,9 @@ export class AssetProfileDialog implements OnDestroy, OnInit {
assetClass: new FormControl<AssetClass>(undefined), assetClass: new FormControl<AssetClass>(undefined),
assetSubClass: new FormControl<AssetSubClass>(undefined), assetSubClass: new FormControl<AssetSubClass>(undefined),
comment: '', comment: '',
historicalData: this.formBuilder.group({
csvString: ''
}),
name: ['', Validators.required], name: ['', Validators.required],
tags: new FormControl<Tag[]>(undefined), tags: new FormControl<Tag[]>(undefined),
scraperConfiguration: '', scraperConfiguration: '',
@ -67,7 +71,6 @@ export class AssetProfileDialog implements OnDestroy, OnInit {
public countries: { public countries: {
[code: string]: { name: string; value: number }; [code: string]: { name: string; value: number };
}; };
public historicalDataAsCsvString: string;
public isBenchmark = false; public isBenchmark = false;
public marketDataDetails: MarketData[] = []; public marketDataDetails: MarketData[] = [];
public sectors: { public sectors: {
@ -88,7 +91,8 @@ export class AssetProfileDialog implements OnDestroy, OnInit {
@Inject(MAT_DIALOG_DATA) public data: AssetProfileDialogParams, @Inject(MAT_DIALOG_DATA) public data: AssetProfileDialogParams,
private dataService: DataService, private dataService: DataService,
public dialogRef: MatDialogRef<AssetProfileDialog>, public dialogRef: MatDialogRef<AssetProfileDialog>,
private formBuilder: FormBuilder private formBuilder: FormBuilder,
private snackBar: MatSnackBar
) {} ) {}
public ngOnInit(): void { public ngOnInit(): void {
@ -98,9 +102,6 @@ export class AssetProfileDialog implements OnDestroy, OnInit {
} }
public initialize() { public initialize() {
this.historicalDataAsCsvString =
AssetProfileDialog.HISTORICAL_DATA_TEMPLATE;
this.adminService this.adminService
.fetchTags() .fetchTags()
.pipe(takeUntil(this.unsubscribeSubject)) .pipe(takeUntil(this.unsubscribeSubject))
@ -154,6 +155,9 @@ export class AssetProfileDialog implements OnDestroy, OnInit {
assetSubClass: this.assetProfile.assetSubClass ?? null, assetSubClass: this.assetProfile.assetSubClass ?? null,
comment: this.assetProfile?.comment ?? '', comment: this.assetProfile?.comment ?? '',
tags: this.assetProfile?.tags, tags: this.assetProfile?.tags,
historicalData: {
csvString: AssetProfileDialog.HISTORICAL_DATA_TEMPLATE
},
name: this.assetProfile.name ?? this.assetProfile.symbol, name: this.assetProfile.name ?? this.assetProfile.symbol,
scraperConfiguration: JSON.stringify( scraperConfiguration: JSON.stringify(
this.assetProfile?.scraperConfiguration ?? {} this.assetProfile?.scraperConfiguration ?? {}
@ -186,11 +190,16 @@ export class AssetProfileDialog implements OnDestroy, OnInit {
} }
public onImportHistoricalData() { public onImportHistoricalData() {
const marketData = csvToJson(this.historicalDataAsCsvString, { try {
const marketData = csvToJson(
this.assetProfileForm.controls['historicalData'].controls['csvString']
.value,
{
dynamicTyping: true, dynamicTyping: true,
header: true, header: true,
skipEmptyLines: true skipEmptyLines: true
}).data; }
).data;
this.adminService this.adminService
.postMarketData({ .postMarketData({
@ -202,10 +211,25 @@ export class AssetProfileDialog implements OnDestroy, OnInit {
}, },
symbol: this.data.symbol symbol: this.data.symbol
}) })
.pipe(takeUntil(this.unsubscribeSubject)) .pipe(
catchError(({ error, message }) => {
this.snackBar.open(`${error}: ${message[0]}`, undefined, {
duration: 3000
});
return EMPTY;
}),
takeUntil(this.unsubscribeSubject)
)
.subscribe(() => { .subscribe(() => {
this.initialize(); this.initialize();
}); });
} catch {
this.snackBar.open(
$localize`Oops! Could not parse historical data.`,
undefined,
{ duration: 3000 }
);
}
} }
public onMarketDataChanged(withRefresh: boolean = false) { public onMarketDataChanged(withRefresh: boolean = false) {
@ -283,6 +307,7 @@ export class AssetProfileDialog implements OnDestroy, OnInit {
return id !== aTag.id; return id !== aTag.id;
}) })
); );
this.assetProfileForm.markAsDirty();
} }
public onAddTag(event: MatAutocompleteSelectedEvent) { public onAddTag(event: MatAutocompleteSelectedEvent) {
@ -293,6 +318,7 @@ export class AssetProfileDialog implements OnDestroy, OnInit {
}) })
]); ]);
this.tagInput.nativeElement.value = ''; this.tagInput.nativeElement.value = '';
this.assetProfileForm.markAsDirty();
} }
public ngOnDestroy() { public ngOnDestroy() {

13
apps/client/src/app/components/admin-market-data/asset-profile-dialog/asset-profile-dialog.html

@ -52,7 +52,7 @@
(marketDataChanged)="onMarketDataChanged($event)" (marketDataChanged)="onMarketDataChanged($event)"
></gf-admin-market-data-detail> ></gf-admin-market-data-detail>
<div class="mt-3"> <div class="mt-3" formGroupName="historicalData">
<mat-form-field appearance="outline" class="w-100 without-hint"> <mat-form-field appearance="outline" class="w-100 without-hint">
<mat-label> <mat-label>
<ng-container i18n>Historical Data</ng-container> (CSV) <ng-container i18n>Historical Data</ng-container> (CSV)
@ -60,11 +60,9 @@
<textarea <textarea
cdkAutosizeMaxRows="5" cdkAutosizeMaxRows="5"
cdkTextareaAutosize cdkTextareaAutosize
formControlName="csvString"
matInput matInput
placeholder="e.g. 20230601;1.61"
type="text" type="text"
[ngModelOptions]="{standalone: true}"
[(ngModel)]="historicalDataAsCsvString"
(keyup.enter)="$event.stopPropagation()" (keyup.enter)="$event.stopPropagation()"
></textarea> ></textarea>
</mat-form-field> </mat-form-field>
@ -75,6 +73,7 @@
color="accent" color="accent"
mat-flat-button mat-flat-button
type="button" type="button"
[disabled]="!assetProfileForm.controls['historicalData']?.controls['csvString'].touched || assetProfileForm.controls['historicalData']?.controls['csvString']?.value === ''"
(click)="onImportHistoricalData()" (click)="onImportHistoricalData()"
> >
<ng-container i18n>Import</ng-container> <ng-container i18n>Import</ng-container>
@ -179,13 +178,13 @@
</ng-container> </ng-container>
</div> </div>
<div *ngIf="assetProfile?.dataSource === 'MANUAL'" class="mt-3"> <div *ngIf="assetProfile?.dataSource === 'MANUAL'" class="mt-3">
<mat-form-field appearance="outline" class="w-100"> <mat-form-field appearance="outline" class="w-100 without-hint">
<mat-label i18n>Name</mat-label> <mat-label i18n>Name</mat-label>
<input formControlName="name" matInput type="text" /> <input formControlName="name" matInput type="text" />
</mat-form-field> </mat-form-field>
</div> </div>
<div class="mt-3"> <div class="mt-3">
<mat-form-field appearance="outline" class="w-100"> <mat-form-field appearance="outline" class="w-100 without-hint">
<mat-label i18n>Asset Class</mat-label> <mat-label i18n>Asset Class</mat-label>
<mat-select formControlName="assetClass"> <mat-select formControlName="assetClass">
<mat-option [value]="null"></mat-option> <mat-option [value]="null"></mat-option>
@ -198,7 +197,7 @@
</mat-form-field> </mat-form-field>
</div> </div>
<div class="mt-3"> <div class="mt-3">
<mat-form-field appearance="outline" class="w-100"> <mat-form-field appearance="outline" class="w-100 without-hint">
<mat-label i18n>Asset Sub Class</mat-label> <mat-label i18n>Asset Sub Class</mat-label>
<mat-select formControlName="assetSubClass"> <mat-select formControlName="assetSubClass">
<mat-option [value]="null"></mat-option> <mat-option [value]="null"></mat-option>

2
apps/client/src/app/components/admin-market-data/asset-profile-dialog/asset-profile-dialog.module.ts

@ -8,6 +8,7 @@ import { MatDialogModule } from '@angular/material/dialog';
import { MatInputModule } from '@angular/material/input'; import { MatInputModule } from '@angular/material/input';
import { MatMenuModule } from '@angular/material/menu'; import { MatMenuModule } from '@angular/material/menu';
import { MatSelectModule } from '@angular/material/select'; import { MatSelectModule } from '@angular/material/select';
import { MatSnackBarModule } from '@angular/material/snack-bar';
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';
@ -32,6 +33,7 @@ import { MatChipsModule } from '@angular/material/chips';
MatInputModule, MatInputModule,
MatMenuModule, MatMenuModule,
MatSelectModule, MatSelectModule,
MatSnackBarModule,
ReactiveFormsModule, ReactiveFormsModule,
TextFieldModule TextFieldModule
], ],

30
apps/client/src/app/components/admin-overview/admin-overview.component.ts

@ -12,7 +12,12 @@ import {
PROPERTY_SYSTEM_MESSAGE, PROPERTY_SYSTEM_MESSAGE,
ghostfolioPrefix ghostfolioPrefix
} from '@ghostfolio/common/config'; } from '@ghostfolio/common/config';
import { Coupon, InfoItem, User } from '@ghostfolio/common/interfaces'; import {
Coupon,
InfoItem,
SystemMessage,
User
} from '@ghostfolio/common/interfaces';
import { hasPermission, permissions } from '@ghostfolio/common/permissions'; import { hasPermission, permissions } from '@ghostfolio/common/permissions';
import { import {
differenceInSeconds, differenceInSeconds,
@ -39,6 +44,7 @@ export class AdminOverviewComponent implements OnDestroy, OnInit {
public hasPermissionToToggleReadOnlyMode: boolean; public hasPermissionToToggleReadOnlyMode: boolean;
public info: InfoItem; public info: InfoItem;
public permissions = permissions; public permissions = permissions;
public systemMessage: SystemMessage;
public transactionCount: number; public transactionCount: number;
public userCount: number; public userCount: number;
public user: User; public user: User;
@ -149,8 +155,14 @@ export class AdminOverviewComponent implements OnDestroy, OnInit {
} }
public onDeleteSystemMessage() { public onDeleteSystemMessage() {
const confirmation = confirm(
$localize`Do you really want to delete this system message?`
);
if (confirmation === true) {
this.putAdminSetting({ key: PROPERTY_SYSTEM_MESSAGE, value: undefined }); this.putAdminSetting({ key: PROPERTY_SYSTEM_MESSAGE, value: undefined });
} }
}
public onFlushCache() { public onFlushCache() {
const confirmation = confirm( const confirmation = confirm(
@ -184,12 +196,21 @@ export class AdminOverviewComponent implements OnDestroy, OnInit {
} }
public onSetSystemMessage() { public onSetSystemMessage() {
const systemMessage = prompt($localize`Please set your system message:`); const systemMessage = prompt(
$localize`Please set your system message:`,
JSON.stringify(
this.systemMessage ??
<SystemMessage>{
message: '⚒️ Scheduled maintenance in progress...',
targetGroups: ['Basic', 'Premium']
}
)
);
if (systemMessage) { if (systemMessage) {
this.putAdminSetting({ this.putAdminSetting({
key: PROPERTY_SYSTEM_MESSAGE, key: PROPERTY_SYSTEM_MESSAGE,
value: systemMessage value: JSON.parse(systemMessage)
}); });
} }
} }
@ -208,6 +229,9 @@ export class AdminOverviewComponent implements OnDestroy, OnInit {
this.coupons = (settings[PROPERTY_COUPONS] as Coupon[]) ?? []; this.coupons = (settings[PROPERTY_COUPONS] as Coupon[]) ?? [];
this.customCurrencies = settings[PROPERTY_CURRENCIES] as string[]; this.customCurrencies = settings[PROPERTY_CURRENCIES] as string[];
this.exchangeRates = exchangeRates; this.exchangeRates = exchangeRates;
this.systemMessage = settings[
PROPERTY_SYSTEM_MESSAGE
] as SystemMessage;
this.transactionCount = transactionCount; this.transactionCount = transactionCount;
this.userCount = userCount; this.userCount = userCount;
this.version = version; this.version = version;

73
apps/client/src/app/components/admin-overview/admin-overview.html

@ -38,7 +38,7 @@
<div class="w-50"> <div class="w-50">
<table> <table>
<tr *ngFor="let exchangeRate of exchangeRates"> <tr *ngFor="let exchangeRate of exchangeRates">
<td class="d-flex"> <td>
<gf-value <gf-value
[locale]="user?.settings?.locale" [locale]="user?.settings?.locale"
[value]="1" [value]="1"
@ -46,8 +46,9 @@
</td> </td>
<td class="pl-1">{{ exchangeRate.label1 }}</td> <td class="pl-1">{{ exchangeRate.label1 }}</td>
<td class="px-1">=</td> <td class="px-1">=</td>
<td class="d-flex justify-content-end"> <td align="right">
<gf-value <gf-value
class="d-inline-block"
[locale]="user?.settings?.locale" [locale]="user?.settings?.locale"
[precision]="4" [precision]="4"
[value]="exchangeRate.value" [value]="exchangeRate.value"
@ -55,9 +56,21 @@
</td> </td>
<td class="pl-1">{{ exchangeRate.label2 }}</td> <td class="pl-1">{{ exchangeRate.label2 }}</td>
<td> <td>
<a <button
class="h-100 mx-1 no-min-width px-2" class="mx-1 no-min-width px-2"
mat-button mat-button
[matMenuTriggerFor]="exchangeRateActionsMenu"
(click)="$event.stopPropagation()"
>
<ion-icon name="ellipsis-horizontal"></ion-icon>
</button>
<mat-menu
#exchangeRateActionsMenu="matMenu"
class="h-100 mx-1 no-min-width px-2"
xPosition="before"
>
<a
mat-menu-item
[queryParams]="{ [queryParams]="{
assetProfileDialog: true, assetProfileDialog: true,
dataSource: exchangeRate.dataSource, dataSource: exchangeRate.dataSource,
@ -65,16 +78,28 @@
}" }"
[routerLink]="['/admin', 'market-data']" [routerLink]="['/admin', 'market-data']"
> >
<ion-icon name="create-outline"></ion-icon> <span class="align-items-center d-flex">
<ion-icon
class="mr-2"
name="create-outline"
></ion-icon>
<span i18n>Edit</span>
</span>
</a> </a>
<button <button
*ngIf="customCurrencies.includes(exchangeRate.label2)" *ngIf="customCurrencies.includes(exchangeRate.label2)"
class="h-100 mx-1 no-min-width px-2" mat-menu-item
mat-button
(click)="onDeleteCurrency(exchangeRate.label2)" (click)="onDeleteCurrency(exchangeRate.label2)"
> >
<ion-icon name="trash-outline"></ion-icon> <span class="align-items-center d-flex">
<ion-icon
class="mr-2"
name="trash-outline"
></ion-icon>
<span i18n>Delete</span>
</span>
</button> </button>
</mat-menu>
</td> </td>
</tr> </tr>
</table> </table>
@ -115,8 +140,8 @@
<div *ngIf="hasPermissionForSystemMessage" class="d-flex my-3"> <div *ngIf="hasPermissionForSystemMessage" class="d-flex my-3">
<div class="w-50" i18n>System Message</div> <div class="w-50" i18n>System Message</div>
<div class="w-50"> <div class="w-50">
<div *ngIf="info?.systemMessage"> <div *ngIf="systemMessage" class="align-items-center d-flex">
<span>{{ info.systemMessage }}</span> <div class="text-truncate">{{ systemMessage | json }}</div>
<button <button
class="h-100 mx-1 no-min-width px-2" class="h-100 mx-1 no-min-width px-2"
mat-button mat-button
@ -127,6 +152,7 @@
</div> </div>
<button <button
*ngIf="!info?.systemMessage" *ngIf="!info?.systemMessage"
class="mt-2"
color="accent" color="accent"
mat-flat-button mat-flat-button
(click)="onSetSystemMessage()" (click)="onSetSystemMessage()"
@ -148,17 +174,34 @@
<table> <table>
<tr *ngFor="let coupon of coupons"> <tr *ngFor="let coupon of coupons">
<td class="text-monospace">{{ coupon.code }}</td> <td class="text-monospace">{{ coupon.code }}</td>
<td class="d-flex justify-content-end pl-2"> <td class="pl-2 text-right">{{ coupon.duration }}</td>
{{ coupon.duration }}
</td>
<td> <td>
<button <button
class="h-100 mx-1 no-min-width px-2" class="mx-1 no-min-width px-2"
mat-button mat-button
[matMenuTriggerFor]="couponActionsMenu"
(click)="$event.stopPropagation()"
>
<ion-icon name="ellipsis-horizontal"></ion-icon>
</button>
<mat-menu
#couponActionsMenu="matMenu"
class="h-100 mx-1 no-min-width px-2"
xPosition="before"
>
<button
mat-menu-item
(click)="onDeleteCoupon(coupon.code)" (click)="onDeleteCoupon(coupon.code)"
> >
<ion-icon name="trash-outline"></ion-icon> <span class="align-items-center d-flex">
<ion-icon
class="mr-2"
name="trash-outline"
></ion-icon>
<span i18n>Delete</span>
</span>
</button> </button>
</mat-menu>
</td> </td>
</tr> </tr>
</table> </table>

2
apps/client/src/app/components/admin-overview/admin-overview.module.ts

@ -3,6 +3,7 @@ import { CUSTOM_ELEMENTS_SCHEMA, NgModule } from '@angular/core';
import { FormsModule, ReactiveFormsModule } from '@angular/forms'; import { FormsModule, ReactiveFormsModule } from '@angular/forms';
import { MatButtonModule } from '@angular/material/button'; import { MatButtonModule } from '@angular/material/button';
import { MatCardModule } from '@angular/material/card'; import { MatCardModule } from '@angular/material/card';
import { MatMenuModule } from '@angular/material/menu';
import { MatSelectModule } from '@angular/material/select'; import { MatSelectModule } from '@angular/material/select';
import { MatSlideToggleModule } from '@angular/material/slide-toggle'; import { MatSlideToggleModule } from '@angular/material/slide-toggle';
import { RouterModule } from '@angular/router'; import { RouterModule } from '@angular/router';
@ -20,6 +21,7 @@ import { AdminOverviewComponent } from './admin-overview.component';
GfValueModule, GfValueModule,
MatButtonModule, MatButtonModule,
MatCardModule, MatCardModule,
MatMenuModule,
MatSelectModule, MatSelectModule,
MatSlideToggleModule, MatSlideToggleModule,
ReactiveFormsModule, ReactiveFormsModule,

5
apps/client/src/app/components/fear-and-greed-index/fear-and-greed-index.component.ts

@ -6,6 +6,7 @@ import {
OnInit OnInit
} from '@angular/core'; } from '@angular/core';
import { resolveFearAndGreedIndex } from '@ghostfolio/common/helper'; import { resolveFearAndGreedIndex } from '@ghostfolio/common/helper';
import { translate } from '@ghostfolio/ui/i18n';
@Component({ @Component({
selector: 'gf-fear-and-greed-index', selector: 'gf-fear-and-greed-index',
@ -24,9 +25,9 @@ export class FearAndGreedIndexComponent implements OnChanges, OnInit {
public ngOnInit() {} public ngOnInit() {}
public ngOnChanges() { public ngOnChanges() {
const { emoji, text } = resolveFearAndGreedIndex(this.fearAndGreedIndex); const { emoji, key } = resolveFearAndGreedIndex(this.fearAndGreedIndex);
this.fearAndGreedIndexEmoji = emoji; this.fearAndGreedIndexEmoji = emoji;
this.fearAndGreedIndexText = text; this.fearAndGreedIndexText = translate(key);
} }
} }

3
apps/client/src/app/components/home-overview/home-overview.component.ts

@ -33,6 +33,7 @@ export class HomeOverviewComponent implements OnDestroy, OnInit {
public isLoadingPerformance = true; public isLoadingPerformance = true;
public performance: PortfolioPerformance; public performance: PortfolioPerformance;
public showDetails = false; public showDetails = false;
public unit: string;
public user: User; public user: User;
private unsubscribeSubject = new Subject<void>(); private unsubscribeSubject = new Subject<void>();
@ -76,6 +77,8 @@ export class HomeOverviewComponent implements OnDestroy, OnInit {
!this.hasImpersonationId && !this.hasImpersonationId &&
!this.user.settings.isRestrictedView && !this.user.settings.isRestrictedView &&
this.user.settings.viewMode !== 'ZEN'; this.user.settings.viewMode !== 'ZEN';
this.unit = this.showDetails ? this.user.settings.baseCurrency : '%';
} }
public onChangeDateRange(dateRange: DateRange) { public onChangeDateRange(dateRange: DateRange) {

2
apps/client/src/app/components/home-overview/home-overview.html

@ -86,7 +86,6 @@
<div class="col"> <div class="col">
<gf-portfolio-performance <gf-portfolio-performance
class="pb-4" class="pb-4"
[baseCurrency]="user?.settings?.baseCurrency"
[deviceType]="deviceType" [deviceType]="deviceType"
[errors]="errors" [errors]="errors"
[isAllTimeHigh]="isAllTimeHigh" [isAllTimeHigh]="isAllTimeHigh"
@ -95,6 +94,7 @@
[locale]="user?.settings?.locale" [locale]="user?.settings?.locale"
[performance]="performance" [performance]="performance"
[showDetails]="showDetails" [showDetails]="showDetails"
[unit]="unit"
></gf-portfolio-performance> ></gf-portfolio-performance>
<div *ngIf="showDetails" class="text-center"> <div *ngIf="showDetails" class="text-center">
<gf-toggle <gf-toggle

10
apps/client/src/app/components/portfolio-performance/portfolio-performance.component.html

@ -35,19 +35,9 @@
<span #value id="value"></span> <span #value id="value"></span>
</div> </div>
<div class="flex-grow-1 px-1"> <div class="flex-grow-1 px-1">
<ngx-skeleton-loader
*ngIf="isLoading"
animation="pulse"
[theme]="{
height: '1.3rem',
width: '2.5rem'
}"
></ngx-skeleton-loader>
<div *ngIf="!isLoading">
{{ unit }} {{ unit }}
</div> </div>
</div> </div>
</div>
<div *ngIf="showDetails" class="row"> <div *ngIf="showDetails" class="row">
<div class="d-flex col justify-content-end"> <div class="d-flex col justify-content-end">
<gf-value <gf-value

8
apps/client/src/app/components/portfolio-performance/portfolio-performance.component.ts

@ -25,7 +25,6 @@ import { isNumber } from 'lodash';
styleUrls: ['./portfolio-performance.component.scss'] styleUrls: ['./portfolio-performance.component.scss']
}) })
export class PortfolioPerformanceComponent implements OnChanges, OnInit { export class PortfolioPerformanceComponent implements OnChanges, OnInit {
@Input() baseCurrency: string;
@Input() deviceType: string; @Input() deviceType: string;
@Input() errors: ResponseError['errors']; @Input() errors: ResponseError['errors'];
@Input() isAllTimeHigh: boolean; @Input() isAllTimeHigh: boolean;
@ -34,11 +33,10 @@ export class PortfolioPerformanceComponent implements OnChanges, OnInit {
@Input() locale: string; @Input() locale: string;
@Input() performance: PortfolioPerformance; @Input() performance: PortfolioPerformance;
@Input() showDetails: boolean; @Input() showDetails: boolean;
@Input() unit: string;
@ViewChild('value') value: ElementRef; @ViewChild('value') value: ElementRef;
public unit: string;
public constructor() {} public constructor() {}
public ngOnInit() {} public ngOnInit() {}
@ -50,8 +48,6 @@ export class PortfolioPerformanceComponent implements OnChanges, OnInit {
} }
} else { } else {
if (isNumber(this.performance?.currentValue)) { if (isNumber(this.performance?.currentValue)) {
this.unit = this.baseCurrency;
new CountUp('value', this.performance?.currentValue, { new CountUp('value', this.performance?.currentValue, {
decimal: getNumberFormatDecimal(this.locale), decimal: getNumberFormatDecimal(this.locale),
decimalPlaces: decimalPlaces:
@ -63,8 +59,6 @@ export class PortfolioPerformanceComponent implements OnChanges, OnInit {
separator: getNumberFormatGroup(this.locale) separator: getNumberFormatGroup(this.locale)
}).start(); }).start();
} else if (this.performance?.currentValue === null) { } else if (this.performance?.currentValue === null) {
this.unit = '%';
new CountUp( new CountUp(
'value', 'value',
this.performance?.currentNetPerformancePercent * 100, this.performance?.currentNetPerformancePercent * 100,

3
apps/client/src/app/components/symbol-icon/symbol-icon.component.scss

@ -1,5 +1,6 @@
:host { :host {
display: block; align-items: center;
display: flex;
img { img {
border-radius: 0.2rem; border-radius: 0.2rem;

28
apps/client/src/app/pages/accounts/transfer-balance/transfer-balance-dialog.html

@ -10,9 +10,17 @@
<mat-form-field appearance="outline" class="w-100"> <mat-form-field appearance="outline" class="w-100">
<mat-label i18n>From</mat-label> <mat-label i18n>From</mat-label>
<mat-select formControlName="fromAccount"> <mat-select formControlName="fromAccount">
<mat-option *ngFor="let account of accounts" [value]="account.id" <mat-option *ngFor="let account of accounts" [value]="account.id">
>{{ account.name }}</mat-option <div class="d-flex">
> <gf-symbol-icon
*ngIf="account.Platform?.url"
class="mr-1"
[tooltip]="account.Platform?.name"
[url]="account.Platform?.url"
></gf-symbol-icon
><span>{{ account.name }}</span>
</div>
</mat-option>
</mat-select> </mat-select>
</mat-form-field> </mat-form-field>
</div> </div>
@ -20,9 +28,17 @@
<mat-form-field appearance="outline" class="w-100"> <mat-form-field appearance="outline" class="w-100">
<mat-label i18n>To</mat-label> <mat-label i18n>To</mat-label>
<mat-select formControlName="toAccount"> <mat-select formControlName="toAccount">
<mat-option *ngFor="let account of accounts" [value]="account.id" <mat-option *ngFor="let account of accounts" [value]="account.id">
>{{ account.name }}</mat-option <div class="d-flex">
> <gf-symbol-icon
*ngIf="account.Platform?.url"
class="mr-1"
[tooltip]="account.Platform?.name"
[url]="account.Platform?.url"
></gf-symbol-icon
><span>{{ account.name }}</span>
</div>
</mat-option>
</mat-select> </mat-select>
</mat-form-field> </mat-form-field>
</div> </div>

2
apps/client/src/app/pages/accounts/transfer-balance/transfer-balance-dialog.module.ts

@ -6,6 +6,7 @@ import { MatDialogModule } from '@angular/material/dialog';
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 { MatSelectModule } from '@angular/material/select'; import { MatSelectModule } from '@angular/material/select';
import { GfSymbolIconModule } from '@ghostfolio/client/components/symbol-icon/symbol-icon.module';
import { TransferBalanceDialog } from './transfer-balance-dialog.component'; import { TransferBalanceDialog } from './transfer-balance-dialog.component';
@ -13,6 +14,7 @@ import { TransferBalanceDialog } from './transfer-balance-dialog.component';
declarations: [TransferBalanceDialog], declarations: [TransferBalanceDialog],
imports: [ imports: [
CommonModule, CommonModule,
GfSymbolIconModule,
MatButtonModule, MatButtonModule,
MatDialogModule, MatDialogModule,
MatFormFieldModule, MatFormFieldModule,

2
apps/client/src/app/pages/blog/2023/11/hacktoberfest-2023/hacktoberfest-2023-debriefing-page.html

@ -28,7 +28,7 @@
year. year.
</p> </p>
<p> <p>
In this debrief, we’ll take a closer look at our journey during In this debriefing, we’ll take a closer look at our journey during
Hacktoberfest, exploring the facts and figures, key takeaways, and Hacktoberfest, exploring the facts and figures, key takeaways, and
the impact on Ghostfolio. the impact on Ghostfolio.
</p> </p>

2
apps/client/src/app/pages/landing/landing-page.html

@ -327,7 +327,7 @@
<div class="col-md-8 offset-md-2"> <div class="col-md-8 offset-md-2">
<gf-carousel [aria-label]="'Testimonials'"> <gf-carousel [aria-label]="'Testimonials'">
<div *ngFor="let testimonial of testimonials" gf-carousel-item> <div *ngFor="let testimonial of testimonials" gf-carousel-item>
<div class="d-flex px-3"> <div class="d-flex px-4">
<gf-logo <gf-logo
class="mr-3 mt-2 pt-1" class="mr-3 mt-2 pt-1"
size="medium" size="medium"

17
apps/client/src/app/pages/portfolio/activities/create-or-update-activity-dialog/create-or-update-activity-dialog.html

@ -78,9 +78,20 @@
*ngIf="!activityForm.controls['accountId'].hasValidator(Validators.required)" *ngIf="!activityForm.controls['accountId'].hasValidator(Validators.required)"
[value]="null" [value]="null"
></mat-option> ></mat-option>
<mat-option *ngFor="let account of data.accounts" [value]="account.id" <mat-option
>{{ account.name }}</mat-option *ngFor="let account of data.accounts"
> [value]="account.id"
>
<div class="d-flex">
<gf-symbol-icon
*ngIf="account.Platform?.url"
class="mr-1"
[tooltip]="account.Platform?.name"
[url]="account.Platform?.url"
></gf-symbol-icon
><span>{{ account.name }}</span>
</div>
</mat-option>
</mat-select> </mat-select>
</mat-form-field> </mat-form-field>
</div> </div>

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

@ -10,6 +10,7 @@ import { MatDialogModule } from '@angular/material/dialog';
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 { MatSelectModule } from '@angular/material/select'; import { MatSelectModule } from '@angular/material/select';
import { GfSymbolIconModule } from '@ghostfolio/client/components/symbol-icon/symbol-icon.module';
import { GfSymbolAutocompleteModule } from '@ghostfolio/ui/symbol-autocomplete/symbol-autocomplete.module'; import { GfSymbolAutocompleteModule } from '@ghostfolio/ui/symbol-autocomplete/symbol-autocomplete.module';
import { GfValueModule } from '@ghostfolio/ui/value'; import { GfValueModule } from '@ghostfolio/ui/value';
@ -21,6 +22,7 @@ import { CreateOrUpdateActivityDialog } from './create-or-update-activity-dialog
CommonModule, CommonModule,
FormsModule, FormsModule,
GfSymbolAutocompleteModule, GfSymbolAutocompleteModule,
GfSymbolIconModule,
GfValueModule, GfValueModule,
MatAutocompleteModule, MatAutocompleteModule,
MatButtonModule, MatButtonModule,

57
apps/client/src/app/pages/resources/personal-finance-tools/products.ts

@ -1,6 +1,8 @@
import { Product } from '@ghostfolio/common/interfaces'; import { Product } from '@ghostfolio/common/interfaces';
import { AllvueSystemsPageComponent } from './products/allvue-systems-page.component';
import { AltooPageComponent } from './products/altoo-page.component'; import { AltooPageComponent } from './products/altoo-page.component';
import { BasilFinancePageComponent } from './products/basil-finance-page.component';
import { BeanvestPageComponent } from './products/beanvest-page.component'; import { BeanvestPageComponent } from './products/beanvest-page.component';
import { CapitallyPageComponent } from './products/capitally-page.component'; import { CapitallyPageComponent } from './products/capitally-page.component';
import { CapMonPageComponent } from './products/capmon-page.component'; import { CapMonPageComponent } from './products/capmon-page.component';
@ -17,8 +19,10 @@ import { GoSpatzPageComponent } from './products/gospatz-page.component';
import { IntuitMintPageComponent } from './products/intuit-mint-page.component'; import { IntuitMintPageComponent } from './products/intuit-mint-page.component';
import { JustEtfPageComponent } from './products/justetf-page.component'; import { JustEtfPageComponent } from './products/justetf-page.component';
import { KuberaPageComponent } from './products/kubera-page.component'; import { KuberaPageComponent } from './products/kubera-page.component';
import { MagnifiPageComponent } from './products/magnifi-page.component';
import { MarketsShPageComponent } from './products/markets.sh-page.component'; import { MarketsShPageComponent } from './products/markets.sh-page.component';
import { MaybeFinancePageComponent } from './products/maybe-finance-page.component'; import { MaybeFinancePageComponent } from './products/maybe-finance-page.component';
import { MonarchMoneyPageComponent } from './products/monarch-money-page.component';
import { MonsePageComponent } from './products/monse-page.component'; import { MonsePageComponent } from './products/monse-page.component';
import { ParqetPageComponent } from './products/parqet-page.component'; import { ParqetPageComponent } from './products/parqet-page.component';
import { PlannixPageComponent } from './products/plannix-page.component'; import { PlannixPageComponent } from './products/plannix-page.component';
@ -37,6 +41,7 @@ import { UtlunaPageComponent } from './products/utluna-page.component';
import { VyzerPageComponent } from './products/vyzer-page.component'; import { VyzerPageComponent } from './products/vyzer-page.component';
import { WealthicaPageComponent } from './products/wealthica-page.component'; import { WealthicaPageComponent } from './products/wealthica-page.component';
import { YeekateePageComponent } from './products/yeekatee-page.component'; import { YeekateePageComponent } from './products/yeekatee-page.component';
import { YnabPageComponent } from './products/ynab-page.component';
export const products: Product[] = [ export const products: Product[] = [
{ {
@ -62,6 +67,16 @@ export const products: Product[] = [
slogan: 'Open Source Wealth Management', slogan: 'Open Source Wealth Management',
useAnonymously: true useAnonymously: true
}, },
{
component: AllvueSystemsPageComponent,
founded: 2019,
hasFreePlan: false,
hasSelfHostingAbility: false,
key: 'allvue-systems',
name: 'Allvue Systems',
origin: $localize`United States`,
slogan: 'Investment Software Suite'
},
{ {
component: AltooPageComponent, component: AltooPageComponent,
founded: 2017, founded: 2017,
@ -71,6 +86,15 @@ export const products: Product[] = [
origin: $localize`Switzerland`, origin: $localize`Switzerland`,
slogan: 'Simplicity for Complex Wealth' slogan: 'Simplicity for Complex Wealth'
}, },
{
component: BasilFinancePageComponent,
founded: 2022,
hasFreePlan: true,
hasSelfHostingAbility: false,
key: 'basil-finance',
name: 'Basil Finance',
slogan: 'The ultimate solution for tracking and managing your investments'
},
{ {
component: BeanvestPageComponent, component: BeanvestPageComponent,
founded: 2020, founded: 2020,
@ -239,6 +263,17 @@ export const products: Product[] = [
pricingPerYear: '$150', pricingPerYear: '$150',
slogan: 'The Time Machine for your Net Worth' slogan: 'The Time Machine for your Net Worth'
}, },
{
component: MagnifiPageComponent,
founded: 2018,
hasFreePlan: false,
hasSelfHostingAbility: false,
key: 'magnifi',
name: 'Magnifi',
origin: $localize`United States`,
pricingPerYear: '$132',
slogan: 'AI Investing Assistant'
},
{ {
component: MarketsShPageComponent, component: MarketsShPageComponent,
founded: 2022, founded: 2022,
@ -265,6 +300,17 @@ export const products: Product[] = [
region: $localize`United States`, region: $localize`United States`,
slogan: 'Your financial future, in your control' slogan: 'Your financial future, in your control'
}, },
{
component: MonarchMoneyPageComponent,
founded: 2019,
hasFreePlan: false,
hasSelfHostingAbility: false,
key: 'monarch-money',
name: 'Monarch Money',
origin: $localize`United States`,
pricingPerYear: '$99.99',
slogan: 'The modern way to manage your money'
},
{ {
component: MonsePageComponent, component: MonsePageComponent,
hasFreePlan: false, hasFreePlan: false,
@ -452,5 +498,16 @@ export const products: Product[] = [
origin: $localize`Switzerland`, origin: $localize`Switzerland`,
region: $localize`Switzerland`, region: $localize`Switzerland`,
slogan: 'Connect. Share. Invest.' slogan: 'Connect. Share. Invest.'
},
{
component: YnabPageComponent,
founded: 2004,
hasFreePlan: false,
hasSelfHostingAbility: false,
key: 'ynab',
name: 'YNAB (You Need a Budget)',
origin: $localize`United States`,
pricingPerYear: '$99',
slogan: 'Change Your Relationship With Money'
} }
]; ];

31
apps/client/src/app/pages/resources/personal-finance-tools/products/allvue-systems-page.component.ts

@ -0,0 +1,31 @@
import { CommonModule } from '@angular/common';
import { Component } from '@angular/core';
import { MatButtonModule } from '@angular/material/button';
import { RouterModule } from '@angular/router';
import { products } from '../products';
@Component({
host: { class: 'page' },
imports: [CommonModule, MatButtonModule, RouterModule],
selector: 'gf-allvue-systems-page',
standalone: true,
styleUrls: ['../product-page-template.scss'],
templateUrl: '../product-page-template.html'
})
export class AllvueSystemsPageComponent {
public product1 = products.find(({ key }) => {
return key === 'ghostfolio';
});
public product2 = products.find(({ key }) => {
return key === 'allvue-systems';
});
public routerLinkAbout = ['/' + $localize`about`];
public routerLinkFeatures = ['/' + $localize`features`];
public routerLinkResourcesPersonalFinanceTools = [
'/' + $localize`resources`,
'personal-finance-tools'
];
}

31
apps/client/src/app/pages/resources/personal-finance-tools/products/basil-finance-page.component.ts

@ -0,0 +1,31 @@
import { CommonModule } from '@angular/common';
import { Component } from '@angular/core';
import { MatButtonModule } from '@angular/material/button';
import { RouterModule } from '@angular/router';
import { products } from '../products';
@Component({
host: { class: 'page' },
imports: [CommonModule, MatButtonModule, RouterModule],
selector: 'gf-basil-finance-page',
standalone: true,
styleUrls: ['../product-page-template.scss'],
templateUrl: '../product-page-template.html'
})
export class BasilFinancePageComponent {
public product1 = products.find(({ key }) => {
return key === 'ghostfolio';
});
public product2 = products.find(({ key }) => {
return key === 'basil-finance';
});
public routerLinkAbout = ['/' + $localize`about`];
public routerLinkFeatures = ['/' + $localize`features`];
public routerLinkResourcesPersonalFinanceTools = [
'/' + $localize`resources`,
'personal-finance-tools'
];
}

31
apps/client/src/app/pages/resources/personal-finance-tools/products/magnifi-page.component.ts

@ -0,0 +1,31 @@
import { CommonModule } from '@angular/common';
import { Component } from '@angular/core';
import { MatButtonModule } from '@angular/material/button';
import { RouterModule } from '@angular/router';
import { products } from '../products';
@Component({
host: { class: 'page' },
imports: [CommonModule, MatButtonModule, RouterModule],
selector: 'gf-magnifi-page',
standalone: true,
styleUrls: ['../product-page-template.scss'],
templateUrl: '../product-page-template.html'
})
export class MagnifiPageComponent {
public product1 = products.find(({ key }) => {
return key === 'ghostfolio';
});
public product2 = products.find(({ key }) => {
return key === 'magnifi';
});
public routerLinkAbout = ['/' + $localize`about`];
public routerLinkFeatures = ['/' + $localize`features`];
public routerLinkResourcesPersonalFinanceTools = [
'/' + $localize`resources`,
'personal-finance-tools'
];
}

31
apps/client/src/app/pages/resources/personal-finance-tools/products/monarch-money-page.component.ts

@ -0,0 +1,31 @@
import { CommonModule } from '@angular/common';
import { Component } from '@angular/core';
import { MatButtonModule } from '@angular/material/button';
import { RouterModule } from '@angular/router';
import { products } from '../products';
@Component({
host: { class: 'page' },
imports: [CommonModule, MatButtonModule, RouterModule],
selector: 'gf-monarch-money-page',
standalone: true,
styleUrls: ['../product-page-template.scss'],
templateUrl: '../product-page-template.html'
})
export class MonarchMoneyPageComponent {
public product1 = products.find(({ key }) => {
return key === 'ghostfolio';
});
public product2 = products.find(({ key }) => {
return key === 'monarch-money';
});
public routerLinkAbout = ['/' + $localize`about`];
public routerLinkFeatures = ['/' + $localize`features`];
public routerLinkResourcesPersonalFinanceTools = [
'/' + $localize`resources`,
'personal-finance-tools'
];
}

31
apps/client/src/app/pages/resources/personal-finance-tools/products/ynab-page.component.ts

@ -0,0 +1,31 @@
import { CommonModule } from '@angular/common';
import { Component } from '@angular/core';
import { MatButtonModule } from '@angular/material/button';
import { RouterModule } from '@angular/router';
import { products } from '../products';
@Component({
host: { class: 'page' },
imports: [CommonModule, MatButtonModule, RouterModule],
selector: 'gf-ynab-page',
standalone: true,
styleUrls: ['../product-page-template.scss'],
templateUrl: '../product-page-template.html'
})
export class YnabPageComponent {
public product1 = products.find(({ key }) => {
return key === 'ghostfolio';
});
public product2 = products.find(({ key }) => {
return key === 'ynab';
});
public routerLinkAbout = ['/' + $localize`about`];
public routerLinkFeatures = ['/' + $localize`features`];
public routerLinkResourcesPersonalFinanceTools = [
'/' + $localize`resources`,
'personal-finance-tools'
];
}

5284
apps/client/src/locales/messages.de.xlf

File diff suppressed because it is too large

5284
apps/client/src/locales/messages.es.xlf

File diff suppressed because it is too large

5284
apps/client/src/locales/messages.fr.xlf

File diff suppressed because it is too large

5284
apps/client/src/locales/messages.it.xlf

File diff suppressed because it is too large

5284
apps/client/src/locales/messages.nl.xlf

File diff suppressed because it is too large

5284
apps/client/src/locales/messages.pt.xlf

File diff suppressed because it is too large

4502
apps/client/src/locales/messages.tr.xlf

File diff suppressed because it is too large

4452
apps/client/src/locales/messages.xlf

File diff suppressed because it is too large

10
libs/common/src/lib/helper.ts

@ -311,15 +311,15 @@ export function resetHours(aDate: Date) {
export function resolveFearAndGreedIndex(aValue: number) { export function resolveFearAndGreedIndex(aValue: number) {
if (aValue <= 25) { if (aValue <= 25) {
return { emoji: '🥵', text: 'Extreme Fear' }; return { emoji: '🥵', key: 'EXTREME_FEAR', text: 'Extreme Fear' };
} else if (aValue <= 45) { } else if (aValue <= 45) {
return { emoji: '😨', text: 'Fear' }; return { emoji: '😨', key: 'FEAR', text: 'Fear' };
} else if (aValue <= 55) { } else if (aValue <= 55) {
return { emoji: '😐', text: 'Neutral' }; return { emoji: '😐', key: 'NEUTRAL', text: 'Neutral' };
} else if (aValue < 75) { } else if (aValue < 75) {
return { emoji: '😜', text: 'Greed' }; return { emoji: '😜', key: 'GREED', text: 'Greed' };
} else { } else {
return { emoji: '🤪', text: 'Extreme Greed' }; return { emoji: '🤪', key: 'EXTREME_GREED', text: 'Extreme Greed' };
} }
} }

2
libs/common/src/lib/interfaces/index.ts

@ -42,6 +42,7 @@ import type { PortfolioPerformanceResponse } from './responses/portfolio-perform
import type { ScraperConfiguration } from './scraper-configuration.interface'; import type { ScraperConfiguration } from './scraper-configuration.interface';
import type { Statistics } from './statistics.interface'; import type { Statistics } from './statistics.interface';
import type { Subscription } from './subscription.interface'; import type { Subscription } from './subscription.interface';
import { SystemMessage } from './system-message.interface';
import { TabConfiguration } from './tab-configuration.interface'; import { TabConfiguration } from './tab-configuration.interface';
import type { TimelinePosition } from './timeline-position.interface'; import type { TimelinePosition } from './timeline-position.interface';
import type { UniqueAsset } from './unique-asset.interface'; import type { UniqueAsset } from './unique-asset.interface';
@ -90,6 +91,7 @@ export {
ResponseError, ResponseError,
ScraperConfiguration, ScraperConfiguration,
Statistics, Statistics,
SystemMessage,
Subscription, Subscription,
TabConfiguration, TabConfiguration,
TimelinePosition, TimelinePosition,

1
libs/common/src/lib/interfaces/info-item.interface.ts

@ -17,6 +17,5 @@ export interface InfoItem {
statistics: Statistics; statistics: Statistics;
stripePublicKey?: string; stripePublicKey?: string;
subscriptions: { [offer in SubscriptionOffer]: Subscription }; subscriptions: { [offer in SubscriptionOffer]: Subscription };
systemMessage?: string;
tags: Tag[]; tags: Tag[];
} }

7
libs/common/src/lib/interfaces/system-message.interface.ts

@ -0,0 +1,7 @@
import { SubscriptionType } from '@ghostfolio/common/types/subscription-type.type';
export interface SystemMessage {
message: string;
routerLink?: string[];
targetGroups: SubscriptionType[];
}

2
libs/common/src/lib/interfaces/user.interface.ts

@ -2,6 +2,7 @@ import { SubscriptionOffer } from '@ghostfolio/common/types';
import { SubscriptionType } from '@ghostfolio/common/types/subscription-type.type'; import { SubscriptionType } from '@ghostfolio/common/types/subscription-type.type';
import { Account, Tag } from '@prisma/client'; import { Account, Tag } from '@prisma/client';
import { SystemMessage } from './system-message.interface';
import { UserSettings } from './user-settings.interface'; import { UserSettings } from './user-settings.interface';
// TODO: Compare with UserWithSettings // TODO: Compare with UserWithSettings
@ -14,6 +15,7 @@ export interface User {
id: string; id: string;
permissions: string[]; permissions: string[];
settings: UserSettings; settings: UserSettings;
systemMessage?: SystemMessage;
subscription: { subscription: {
expiresAt?: Date; expiresAt?: Date;
offer: SubscriptionOffer; offer: SubscriptionOffer;

8
libs/ui/src/lib/carousel/carousel.component.html

@ -2,8 +2,8 @@
*ngIf="this.showPrevArrow" *ngIf="this.showPrevArrow"
aria-hidden="true" aria-hidden="true"
aria-label="previous" aria-label="previous"
class="carousel-nav carousel-nav-prev no-min-width position-absolute" class="carousel-nav carousel-nav-prev no-min-width position-absolute px-1"
mat-stroked-button mat-button
tabindex="-1" tabindex="-1"
(click)="previous()" (click)="previous()"
> >
@ -25,8 +25,8 @@
*ngIf="this.showNextArrow" *ngIf="this.showNextArrow"
aria-hidden="true" aria-hidden="true"
aria-label="next" aria-label="next"
class="carousel-nav carousel-nav-next no-min-width position-absolute" class="carousel-nav carousel-nav-next no-min-width position-absolute px-1"
mat-stroked-button mat-button
tabindex="-1" tabindex="-1"
(click)="next()" (click)="next()"
> >

5
libs/ui/src/lib/carousel/carousel.component.scss

@ -12,13 +12,14 @@
button { button {
top: 50%; top: 50%;
transform: translateY(-50%); transform: translateY(-50%);
z-index: 1;
&.carousel-nav-prev { &.carousel-nav-prev {
left: -50px; left: -0.5rem;
} }
&.carousel-nav-next { &.carousel-nav-next {
right: -50px; right: -0.5rem;
} }
} }

9
libs/ui/src/lib/i18n.ts

@ -58,7 +58,14 @@ const locales = {
Europe: $localize`Europe`, Europe: $localize`Europe`,
'North America': $localize`North America`, 'North America': $localize`North America`,
Oceania: $localize`Oceania`, Oceania: $localize`Oceania`,
'South America': $localize`South America` 'South America': $localize`South America`,
// Fear and Greed Index
EXTREME_FEAR: $localize`Extreme Fear`,
EXTREME_GREED: $localize`Extreme Greed`,
FEAR: $localize`Fear`,
GREED: $localize`Greed`,
NEUTRAL: $localize`Neutral`
}; };
export function translate(aKey: string): string { export function translate(aKey: string): string {

4
package.json

@ -1,6 +1,6 @@
{ {
"name": "ghostfolio", "name": "ghostfolio",
"version": "2.18.0", "version": "2.22.0",
"homepage": "https://ghostfol.io", "homepage": "https://ghostfol.io",
"license": "AGPL-3.0", "license": "AGPL-3.0",
"repository": "https://github.com/ghostfolio/ghostfolio", "repository": "https://github.com/ghostfolio/ghostfolio",
@ -113,7 +113,7 @@
"lodash": "4.17.21", "lodash": "4.17.21",
"marked": "4.2.12", "marked": "4.2.12",
"ms": "3.0.0-canary.1", "ms": "3.0.0-canary.1",
"ng-extract-i18n-merge": "2.7.0", "ng-extract-i18n-merge": "2.8.3",
"ngx-device-detector": "5.0.1", "ngx-device-detector": "5.0.1",
"ngx-markdown": "15.1.0", "ngx-markdown": "15.1.0",
"ngx-skeleton-loader": "7.0.0", "ngx-skeleton-loader": "7.0.0",

2
prisma/migrations/20231107080536_removed_account_type_from_account/migration.sql

@ -0,0 +1,2 @@
-- AlterTable
ALTER TABLE "Account" DROP COLUMN "accountType";

1
prisma/schema.prisma

@ -21,7 +21,6 @@ model Access {
} }
model Account { model Account {
accountType AccountType?
balance Float @default(0) balance Float @default(0)
balances AccountBalance[] balances AccountBalance[]
comment String? comment String?

137
yarn.lock

@ -28,12 +28,12 @@
"@angular-devkit/core" "16.2.9" "@angular-devkit/core" "16.2.9"
rxjs "7.8.1" rxjs "7.8.1"
"@angular-devkit/architect@^0.1600.0-next.6": "@angular-devkit/architect@^0.1301.0 || ^0.1401.0 || ^0.1501.0 || ^0.1601.0 || ^0.1700.0":
version "0.1600.6" version "0.1700.0"
resolved "https://registry.yarnpkg.com/@angular-devkit/architect/-/architect-0.1600.6.tgz#216f4d89086b8b4ef562b2066e430a44f7a2cf57" resolved "https://registry.yarnpkg.com/@angular-devkit/architect/-/architect-0.1700.0.tgz#419d59be6f8bc0068f8d495d7e28f4f47cfdb2ce"
integrity sha512-Mk/pRujuer5qRMrgC7DPwLQ88wTAEKhbs0yJ/1prm4cx+VkxX9MMf6Y4AHKRmduKmFmd2LmX21/ACiU65acH8w== integrity sha512-whi7HvOjv1J3He9f+H8xNJWKyjAmWuWNl8gxNW6EZP/XLcrOu+/5QT4bPtXQBRIL/avZuc++5sNQS+kReaNCig==
dependencies: dependencies:
"@angular-devkit/core" "16.0.6" "@angular-devkit/core" "17.0.0"
rxjs "7.8.1" rxjs "7.8.1"
"@angular-devkit/build-angular@16.2.9": "@angular-devkit/build-angular@16.2.9":
@ -127,17 +127,6 @@
rxjs "7.8.1" rxjs "7.8.1"
source-map "0.7.4" source-map "0.7.4"
"@angular-devkit/core@16.0.6":
version "16.0.6"
resolved "https://registry.yarnpkg.com/@angular-devkit/core/-/core-16.0.6.tgz#6bedee38bb070e9203e60c9eeda38247ef39f57d"
integrity sha512-pHbDUwXDMTWTnX/vafkFnzvYDQD8lz+w8FvMQE23Q/vN6/Q0BRf0PWTAGla6Wt+E4HaqqrbQS5P0YBwS4te2Pw==
dependencies:
ajv "8.12.0"
ajv-formats "2.1.1"
jsonc-parser "3.2.0"
rxjs "7.8.1"
source-map "0.7.4"
"@angular-devkit/core@16.1.0": "@angular-devkit/core@16.1.0":
version "16.1.0" version "16.1.0"
resolved "https://registry.yarnpkg.com/@angular-devkit/core/-/core-16.1.0.tgz#cb56b19e88fc936fb0b26c5ae62591f1e8906961" resolved "https://registry.yarnpkg.com/@angular-devkit/core/-/core-16.1.0.tgz#cb56b19e88fc936fb0b26c5ae62591f1e8906961"
@ -160,10 +149,10 @@
rxjs "7.8.1" rxjs "7.8.1"
source-map "0.7.4" source-map "0.7.4"
"@angular-devkit/core@16.2.8", "@angular-devkit/core@^16.0.0-next.6": "@angular-devkit/core@16.2.9":
version "16.2.8" version "16.2.9"
resolved "https://registry.yarnpkg.com/@angular-devkit/core/-/core-16.2.8.tgz#db74f3063e7fd573be7dafd022e8dc10e43140c0" resolved "https://registry.yarnpkg.com/@angular-devkit/core/-/core-16.2.9.tgz#81c5c95de8c423634bf93f616683045c6cdd4dd0"
integrity sha512-PTGozYvh1Bin5lB15PwcXa26Ayd17bWGLS3H8Rs0s+04mUDvfNofmweaX1LgumWWy3nCUTDuwHxX10M3G0wE2g== integrity sha512-dcHWjHBNGm3yCeNz19y8A1At4KgyC6XHNnbFL0y+nnZYiaESXjUoXJYKASedI6A+Bpl0HNq2URhH6bL6Af3+4w==
dependencies: dependencies:
ajv "8.12.0" ajv "8.12.0"
ajv-formats "2.1.1" ajv-formats "2.1.1"
@ -172,15 +161,15 @@
rxjs "7.8.1" rxjs "7.8.1"
source-map "0.7.4" source-map "0.7.4"
"@angular-devkit/core@16.2.9": "@angular-devkit/core@17.0.0", "@angular-devkit/core@^13.0.0 || ^14.0.0 || ^15.0.0 || ^16.0.0 || ^17.0.0":
version "16.2.9" version "17.0.0"
resolved "https://registry.yarnpkg.com/@angular-devkit/core/-/core-16.2.9.tgz#81c5c95de8c423634bf93f616683045c6cdd4dd0" resolved "https://registry.yarnpkg.com/@angular-devkit/core/-/core-17.0.0.tgz#99cd048cca37cf4d0cb60a3b6871e19449a8006a"
integrity sha512-dcHWjHBNGm3yCeNz19y8A1At4KgyC6XHNnbFL0y+nnZYiaESXjUoXJYKASedI6A+Bpl0HNq2URhH6bL6Af3+4w== integrity sha512-QUu3LnEi4A8t733v2+I0sLtyBJx3Q7zdTAhaauCbxbFhDid0cbYm8hYsyG/njor1irTPxSJbn6UoetVkwUQZxg==
dependencies: dependencies:
ajv "8.12.0" ajv "8.12.0"
ajv-formats "2.1.1" ajv-formats "2.1.1"
jsonc-parser "3.2.0" jsonc-parser "3.2.0"
picomatch "2.3.1" picomatch "3.0.1"
rxjs "7.8.1" rxjs "7.8.1"
source-map "0.7.4" source-map "0.7.4"
@ -217,17 +206,6 @@
ora "5.4.1" ora "5.4.1"
rxjs "7.8.1" rxjs "7.8.1"
"@angular-devkit/schematics@16.2.8", "@angular-devkit/schematics@^16.0.0-next.6":
version "16.2.8"
resolved "https://registry.yarnpkg.com/@angular-devkit/schematics/-/schematics-16.2.8.tgz#cc11cf6d00cd9131adbede9a99f3a617aedd5bc4"
integrity sha512-MBiKZOlR9/YMdflALr7/7w/BGAfo/BGTrlkqsIB6rDWV1dYiCgxI+033HsiNssLS6RQyCFx/e7JA2aBBzu9zEg==
dependencies:
"@angular-devkit/core" "16.2.8"
jsonc-parser "3.2.0"
magic-string "0.30.1"
ora "5.4.1"
rxjs "7.8.1"
"@angular-devkit/schematics@16.2.9": "@angular-devkit/schematics@16.2.9":
version "16.2.9" version "16.2.9"
resolved "https://registry.yarnpkg.com/@angular-devkit/schematics/-/schematics-16.2.9.tgz#71eed819c1665068d717d75f912f5ea689c201f9" resolved "https://registry.yarnpkg.com/@angular-devkit/schematics/-/schematics-16.2.9.tgz#71eed819c1665068d717d75f912f5ea689c201f9"
@ -239,6 +217,17 @@
ora "5.4.1" ora "5.4.1"
rxjs "7.8.1" rxjs "7.8.1"
"@angular-devkit/schematics@17.0.0", "@angular-devkit/schematics@^13.0.0 || ^14.0.0 || ^15.0.0 || ^16.0.0 || ^17.0.0":
version "17.0.0"
resolved "https://registry.yarnpkg.com/@angular-devkit/schematics/-/schematics-17.0.0.tgz#bfcc09a1bd145ef978f92d660df89a11e69468d4"
integrity sha512-LD7fjDORuBf139/oJ/gSwbIzQPfsm6Y67s1FD+XLi0QXaRt6dw4r7BMD08l1r//oPQofNgbEH4coGVO4NdCL/A==
dependencies:
"@angular-devkit/core" "17.0.0"
jsonc-parser "3.2.0"
magic-string "0.30.5"
ora "5.4.1"
rxjs "7.8.1"
"@angular-eslint/bundled-angular-compiler@16.2.0": "@angular-eslint/bundled-angular-compiler@16.2.0":
version "16.2.0" version "16.2.0"
resolved "https://registry.yarnpkg.com/@angular-eslint/bundled-angular-compiler/-/bundled-angular-compiler-16.2.0.tgz#09d0637d738850a2c6f0523f19632e992f790102" resolved "https://registry.yarnpkg.com/@angular-eslint/bundled-angular-compiler/-/bundled-angular-compiler-16.2.0.tgz#09d0637d738850a2c6f0523f19632e992f790102"
@ -4688,13 +4677,13 @@
"@angular-devkit/schematics" "16.2.9" "@angular-devkit/schematics" "16.2.9"
jsonc-parser "3.2.0" jsonc-parser "3.2.0"
"@schematics/angular@^16.0.0-next.6": "@schematics/angular@^13.0.0 || ^14.0.0 || ^15.0.0 || ^16.0.0 || ^17.0.0":
version "16.2.8" version "17.0.0"
resolved "https://registry.yarnpkg.com/@schematics/angular/-/angular-16.2.8.tgz#d4c236767e89c536c2c15951394cac20f07bfc1f" resolved "https://registry.yarnpkg.com/@schematics/angular/-/angular-17.0.0.tgz#63ddf8bfbb3b117fe7a355bd22b43d2c9ff7f0ee"
integrity sha512-yxfxJ2IMRIt+dQcqyJR30qd/osb5NwRsi9US3gFIHP1jfjOAs1Nk8ENNd5ycYV+yykCa78KWhmbOw4G1zpR56Q== integrity sha512-9jKU5x/WzaBsfSkUowK1X74FqtMXa6+A60XgW4ACO8i6fwKfPeS+tIrAieeYOX80/njBh7I5CvcpHmWA2SbcXQ==
dependencies: dependencies:
"@angular-devkit/core" "16.2.8" "@angular-devkit/core" "17.0.0"
"@angular-devkit/schematics" "16.2.8" "@angular-devkit/schematics" "17.0.0"
jsonc-parser "3.2.0" jsonc-parser "3.2.0"
"@sigstore/bundle@^1.1.0": "@sigstore/bundle@^1.1.0":
@ -8507,16 +8496,11 @@ commander@^6.2.1:
resolved "https://registry.yarnpkg.com/commander/-/commander-6.2.1.tgz#0792eb682dfbc325999bb2b84fddddba110ac73c" resolved "https://registry.yarnpkg.com/commander/-/commander-6.2.1.tgz#0792eb682dfbc325999bb2b84fddddba110ac73c"
integrity sha512-U7VdrJFnJgo4xjrHpTzu0yrHPGImdsmD95ZlgYSEajAn2JKzDhDTPG9kBTefmObL2w/ngeZnilk+OV9CG3d7UA== integrity sha512-U7VdrJFnJgo4xjrHpTzu0yrHPGImdsmD95ZlgYSEajAn2JKzDhDTPG9kBTefmObL2w/ngeZnilk+OV9CG3d7UA==
commander@^8.3.0, commander@~8.3.0: commander@^8.3.0:
version "8.3.0" version "8.3.0"
resolved "https://registry.yarnpkg.com/commander/-/commander-8.3.0.tgz#4837ea1b2da67b9c616a67afbb0fafee567bca66" resolved "https://registry.yarnpkg.com/commander/-/commander-8.3.0.tgz#4837ea1b2da67b9c616a67afbb0fafee567bca66"
integrity sha512-OkTL9umf+He2DZkUq8f8J9of7yL6RJKI24dVITBmNfZBmri9zYZQrKkuXiKhyfPSu8tUhnVBB1iKXevvnlR4Ww== integrity sha512-OkTL9umf+He2DZkUq8f8J9of7yL6RJKI24dVITBmNfZBmri9zYZQrKkuXiKhyfPSu8tUhnVBB1iKXevvnlR4Ww==
commander@~7.1.0:
version "7.1.0"
resolved "https://registry.yarnpkg.com/commander/-/commander-7.1.0.tgz#f2eaecf131f10e36e07d894698226e36ae0eb5ff"
integrity sha512-pRxBna3MJe6HKnBGsDyMv8ETbptw3axEdYHoqNh7gu5oDcew8fs0xnivZGm06Ogk8zGAJ9VX+OPEr2GXEQK4dg==
comment-json@4.2.3: comment-json@4.2.3:
version "4.2.3" version "4.2.3"
resolved "https://registry.yarnpkg.com/comment-json/-/comment-json-4.2.3.tgz#50b487ebbf43abe44431f575ebda07d30d015365" resolved "https://registry.yarnpkg.com/comment-json/-/comment-json-4.2.3.tgz#50b487ebbf43abe44431f575ebda07d30d015365"
@ -13271,11 +13255,6 @@ jiti@^1.18.2:
resolved "https://registry.yarnpkg.com/jiti/-/jiti-1.20.0.tgz#2d823b5852ee8963585c8dd8b7992ffc1ae83b42" resolved "https://registry.yarnpkg.com/jiti/-/jiti-1.20.0.tgz#2d823b5852ee8963585c8dd8b7992ffc1ae83b42"
integrity sha512-3TV69ZbrvV6U5DfQimop50jE9Dl6J8O1ja1dvBbMba/sZ3YBEQqJ2VZRoQPVnhlzjNtU1vaXRZVrVjU4qtm8yA== integrity sha512-3TV69ZbrvV6U5DfQimop50jE9Dl6J8O1ja1dvBbMba/sZ3YBEQqJ2VZRoQPVnhlzjNtU1vaXRZVrVjU4qtm8yA==
js-levenshtein@~1.1.6:
version "1.1.6"
resolved "https://registry.yarnpkg.com/js-levenshtein/-/js-levenshtein-1.1.6.tgz#c6cee58eb3550372df8deb85fad5ce66ce01d59d"
integrity sha512-X2BB11YZtrRqY4EnQcLX5Rh373zbK4alC1FW7D7MBhL2gtcC17cTnr6DmfHZeS0s2rTHjUTMMHfG7gO8SSdw+g==
js-sdsl@^4.1.4: js-sdsl@^4.1.4:
version "4.4.2" version "4.4.2"
resolved "https://registry.yarnpkg.com/js-sdsl/-/js-sdsl-4.4.2.tgz#2e3c031b1f47d3aca8b775532e3ebb0818e7f847" resolved "https://registry.yarnpkg.com/js-sdsl/-/js-sdsl-4.4.2.tgz#2e3c031b1f47d3aca8b775532e3ebb0818e7f847"
@ -14005,7 +13984,7 @@ magic-string@0.30.1:
dependencies: dependencies:
"@jridgewell/sourcemap-codec" "^1.4.15" "@jridgewell/sourcemap-codec" "^1.4.15"
magic-string@~0.30.2: magic-string@0.30.5, magic-string@~0.30.2:
version "0.30.5" version "0.30.5"
resolved "https://registry.yarnpkg.com/magic-string/-/magic-string-0.30.5.tgz#1994d980bd1c8835dc6e78db7cbd4ae4f24746f9" resolved "https://registry.yarnpkg.com/magic-string/-/magic-string-0.30.5.tgz#1994d980bd1c8835dc6e78db7cbd4ae4f24746f9"
integrity sha512-7xlpfBaQaP/T6Vh8MO/EqXSW5En6INHEvEXQiuff7Gku0PWjU3uf6w/j9o7O+SpB5fOAkrI5HeoNgwjEO0pFsA== integrity sha512-7xlpfBaQaP/T6Vh8MO/EqXSW5En6INHEvEXQiuff7Gku0PWjU3uf6w/j9o7O+SpB5fOAkrI5HeoNgwjEO0pFsA==
@ -14554,18 +14533,16 @@ neo-async@^2.5.0, neo-async@^2.6.2:
resolved "https://registry.yarnpkg.com/neo-async/-/neo-async-2.6.2.tgz#b4aafb93e3aeb2d8174ca53cf163ab7d7308305f" resolved "https://registry.yarnpkg.com/neo-async/-/neo-async-2.6.2.tgz#b4aafb93e3aeb2d8174ca53cf163ab7d7308305f"
integrity sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw== integrity sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw==
ng-extract-i18n-merge@2.7.0: ng-extract-i18n-merge@2.8.3:
version "2.7.0" version "2.8.3"
resolved "https://registry.yarnpkg.com/ng-extract-i18n-merge/-/ng-extract-i18n-merge-2.7.0.tgz#18e2acd1a7598100300c42887917e16c4782589d" resolved "https://registry.yarnpkg.com/ng-extract-i18n-merge/-/ng-extract-i18n-merge-2.8.3.tgz#a092f7758df7c566df7a1d710dbc709c6a8f56d1"
integrity sha512-HG0Gjg4J8GqkROQSdHeCS1jtqz3ExzswH2zA8nbJNZU5ctA25O8dpfSXVl63PWxNhYtJOnP4rEPXNiyvlHaHwA== integrity sha512-w6LdzpfjRBLpT7lnMEqduivjn6kg2oKDZBL6P9W5GKRZ4bgmFthAmwN1lvWrzkwcPHPARJR+qC4DBRVsv4vmkg==
dependencies: dependencies:
"@angular-devkit/architect" "^0.1600.0-next.6" "@angular-devkit/architect" "^0.1301.0 || ^0.1401.0 || ^0.1501.0 || ^0.1601.0 || ^0.1700.0"
"@angular-devkit/core" "^16.0.0-next.6" "@angular-devkit/core" "^13.0.0 || ^14.0.0 || ^15.0.0 || ^16.0.0 || ^17.0.0"
"@angular-devkit/schematics" "^16.0.0-next.6" "@angular-devkit/schematics" "^13.0.0 || ^14.0.0 || ^15.0.0 || ^16.0.0 || ^17.0.0"
"@schematics/angular" "^16.0.0-next.6" "@schematics/angular" "^13.0.0 || ^14.0.0 || ^15.0.0 || ^16.0.0 || ^17.0.0"
xliff-simple-merge "~1.0.1" xmldoc "^1.1.2"
xml_normalize "^1.0.0"
xmldoc "~1.1.2"
ngx-device-detector@5.0.1: ngx-device-detector@5.0.1:
version "5.0.1" version "5.0.1"
@ -15546,6 +15523,11 @@ picomatch@2.3.1, picomatch@^2.0.4, picomatch@^2.2.1, picomatch@^2.2.3, picomatch
resolved "https://registry.yarnpkg.com/picomatch/-/picomatch-2.3.1.tgz#3ba3833733646d9d3e4995946c1365a67fb07a42" resolved "https://registry.yarnpkg.com/picomatch/-/picomatch-2.3.1.tgz#3ba3833733646d9d3e4995946c1365a67fb07a42"
integrity sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA== integrity sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==
picomatch@3.0.1:
version "3.0.1"
resolved "https://registry.yarnpkg.com/picomatch/-/picomatch-3.0.1.tgz#817033161def55ec9638567a2f3bbc876b3e7516"
integrity sha512-I3EurrIQMlRc9IaAZnqRR044Phh2DXY+55o7uJ0V+hYZAcQYSuFWsc9q5PvyDHUSCe1Qxn/iBz+78s86zWnGag==
pify@^2.2.0, pify@^2.3.0: pify@^2.2.0, pify@^2.3.0:
version "2.3.0" version "2.3.0"
resolved "https://registry.yarnpkg.com/pify/-/pify-2.3.0.tgz#ed141a6ac043a849ea588498e7dca8b15330e90c" resolved "https://registry.yarnpkg.com/pify/-/pify-2.3.0.tgz#ed141a6ac043a849ea588498e7dca8b15330e90c"
@ -19091,15 +19073,6 @@ ws@^8.11.0, ws@^8.13.0, ws@^8.2.3:
resolved "https://registry.yarnpkg.com/ws/-/ws-8.14.2.tgz#6c249a806eb2db7a20d26d51e7709eab7b2e6c7f" resolved "https://registry.yarnpkg.com/ws/-/ws-8.14.2.tgz#6c249a806eb2db7a20d26d51e7709eab7b2e6c7f"
integrity sha512-wEBG1ftX4jcglPxgFCMJmZ2PLtSbJ2Peg6TmpJFTbe9GZYOQCDPdMYu/Tm0/bGZkw8paZnJY45J4K2PZrLYq8g== integrity sha512-wEBG1ftX4jcglPxgFCMJmZ2PLtSbJ2Peg6TmpJFTbe9GZYOQCDPdMYu/Tm0/bGZkw8paZnJY45J4K2PZrLYq8g==
xliff-simple-merge@~1.0.1:
version "1.0.2"
resolved "https://registry.yarnpkg.com/xliff-simple-merge/-/xliff-simple-merge-1.0.2.tgz#55f88a84630de625db2b3ddfc3d0d741ac940bfd"
integrity sha512-9Dtw/l91o0DeLkNFJrlh5nxJSS8OD+IHeq5rjA6hkVtv6SWf7rJyr4YNSQc/6opDssRI8JgAWcQlj2ZfcvW11Q==
dependencies:
commander "~8.3.0"
js-levenshtein "~1.1.6"
xmldoc "~1.1.2"
xml-name-validator@^3.0.0: xml-name-validator@^3.0.0:
version "3.0.0" version "3.0.0"
resolved "https://registry.yarnpkg.com/xml-name-validator/-/xml-name-validator-3.0.0.tgz#6ae73e06de4d8c6e47f9fb181f78d648ad457c6a" resolved "https://registry.yarnpkg.com/xml-name-validator/-/xml-name-validator-3.0.0.tgz#6ae73e06de4d8c6e47f9fb181f78d648ad457c6a"
@ -19110,23 +19083,15 @@ xml-name-validator@^4.0.0:
resolved "https://registry.yarnpkg.com/xml-name-validator/-/xml-name-validator-4.0.0.tgz#79a006e2e63149a8600f15430f0a4725d1524835" resolved "https://registry.yarnpkg.com/xml-name-validator/-/xml-name-validator-4.0.0.tgz#79a006e2e63149a8600f15430f0a4725d1524835"
integrity sha512-ICP2e+jsHvAj2E2lIHxa5tjXRlKDJo4IdvPvCXbXQGdzSfmSpNVyIKMvoZHjDY9DP0zV17iI85o90vRFXNccRw== integrity sha512-ICP2e+jsHvAj2E2lIHxa5tjXRlKDJo4IdvPvCXbXQGdzSfmSpNVyIKMvoZHjDY9DP0zV17iI85o90vRFXNccRw==
xml_normalize@^1.0.0:
version "1.0.0"
resolved "https://registry.yarnpkg.com/xml_normalize/-/xml_normalize-1.0.0.tgz#e844d8abae27b64fcb4eb0d567ecff278e0b166c"
integrity sha512-VzDbw9DW849WoLor6CP1eIPiVWwbq8CV3dlSrfVfsMqBqvp3VVkmLxA8J55WyLf6CnAf2sV29TQO77BKM/cxBw==
dependencies:
commander "~7.1.0"
xmldoc "~1.1.2"
xmlchars@^2.2.0: xmlchars@^2.2.0:
version "2.2.0" version "2.2.0"
resolved "https://registry.yarnpkg.com/xmlchars/-/xmlchars-2.2.0.tgz#060fe1bcb7f9c76fe2a17db86a9bc3ab894210cb" resolved "https://registry.yarnpkg.com/xmlchars/-/xmlchars-2.2.0.tgz#060fe1bcb7f9c76fe2a17db86a9bc3ab894210cb"
integrity sha512-JZnDKK8B0RCDw84FNdDAIpZK+JuJw+s7Lz8nksI7SIuU3UXJJslUthsi+uWBUYOwPFwW7W7PRLRfUKpxjtjFCw== integrity sha512-JZnDKK8B0RCDw84FNdDAIpZK+JuJw+s7Lz8nksI7SIuU3UXJJslUthsi+uWBUYOwPFwW7W7PRLRfUKpxjtjFCw==
xmldoc@~1.1.2: xmldoc@^1.1.2:
version "1.1.4" version "1.3.0"
resolved "https://registry.yarnpkg.com/xmldoc/-/xmldoc-1.1.4.tgz#ea4e26dca76b1d218a2f777018bce404ba374a86" resolved "https://registry.yarnpkg.com/xmldoc/-/xmldoc-1.3.0.tgz#7823225b096c74036347c9ec5924d06b6a3cebab"
integrity sha512-rQshsBGR5s7pUNENTEncpI2LTCuzicri0DyE4SCV5XmS0q81JS8j1iPijP0Q5c4WLGbKh3W92hlOwY6N9ssW1w== integrity sha512-y7IRWW6PvEnYQZNZFMRLNJw+p3pezM4nKYPfr15g4OOW9i8VpeydycFuipE2297OvZnh3jSb2pxOt9QpkZUVng==
dependencies: dependencies:
sax "^1.2.4" sax "^1.2.4"

Loading…
Cancel
Save