Compare commits

...

26 Commits

Author SHA1 Message Date
6f11627006 Release 2.24.0 (#2661) 2023-11-16 20:29:49 +01:00
215098e418 Feature/improve language localization for german 20231116 (#2660)
* Update locales

* Update changelog
2023-11-16 20:28:09 +01:00
781496383b Bugfix/improve get range query in market data service (#2659)
* Attempt to fix "too many bind variables in prepared statement, expected maximum of 32767"

* Update changelog
2023-11-16 20:22:56 +01:00
f0f304c012 Change tweet to post (#2658) 2023-11-16 20:22:18 +01:00
4bf97c104b Update changelog (#2656) 2023-11-15 21:45:38 +01:00
0b35a3c7a7 Release 2.23.0 (#2655) 2023-11-15 21:22:20 +01:00
1586cd3a59 Feature/change twitter to x (#2654)
* Change Twitter to X

* Update changelog
2023-11-15 21:20:51 +01:00
ae763cbb87 Improve style of sub title (#2652) 2023-11-15 21:11:10 +01:00
aa72287d54 Extend benchmarks in the markets overview by 50-Day and 200-Day trends (#2575)
* Extend benchmarks in the markets overview by 50-Day and 200-Day trends

* Update changelog

---------

Co-authored-by: Thomas <4159106+dtslvr@users.noreply.github.com>
2023-11-15 20:25:16 +01:00
d155ab6f28 Feature/improve data source validation in activities import (#2645)
* Improve data source validation

* Update changelog
2023-11-14 19:15:57 +01:00
913ca71aa5 Feature/upgrade prettier to version 3.1.0 (#2649)
* Upgrade prettier to version 3.1.0

* Update changelog
2023-11-13 20:40:25 +01:00
1ffde2a27e Feature/setup polish (#2650)
* Set up polski

* Update changelog
2023-11-13 20:35:15 +01:00
fcf0cea982 Change BTC to BTCUSD (#2646) 2023-11-13 20:22:47 +01:00
ae1968aadf Feature/extract locales 20231112 (#2643)
* Update locales

* Update changelog
2023-11-12 19:03:11 +01:00
3e6333ef95 Feature/upgrade ng extract i18n merge to version 2.8.3 (#2642)
* Upgrade ng-extract-i18n-merge to version 2.8.3

* Update changelog
2023-11-12 18:28:26 +01:00
c69686651e Release 2.22.0 (#2638) 2023-11-11 18:59:43 +01:00
93b6011ddc Feature/refactor get range in market data service (#2631)
* Refactor to unique asset in getRange()

* Update changelog
2023-11-11 18:57:41 +01:00
f567e25f27 Feature/introduce action menus in overview of action control panel (#2637)
* Introduce action menus

* Exchange rates management
* Coupons management

* Update changelog
2023-11-11 18:48:23 +01:00
5dc538bafb Feature/optimize testimonial carousel style on mobile (#2634)
* Optimize style on mobile

* Update changelog
2023-11-11 17:29:59 +01:00
b4de06fcf0 Feature/add platform icons to account selectors (#2633)
* Add platform icons to account selectors

* Update changelog
2023-11-11 17:27:29 +01:00
27da0eb26e Feature/harmonize name column of historical market data table (#2632)
* Harmonize name column

* Update changelog
2023-11-11 09:02:12 +01:00
8ff80c10e5 Feature/extend personal finance tools pages 20231110 (#2630)
* Add Magnifi

* Add Basil Finance
2023-11-11 09:01:58 +01:00
5db5d5e79a Release 2.21.0 (#2629) 2023-11-09 19:25:14 +01:00
12aac101bd Feature/extend system message (#2628)
* Extend system message

* Update changelog
2023-11-09 19:23:36 +01:00
3a66ccdebe Bugfix/fix unit in overview of home page (#2626)
* Fix unit

* Update changelog
2023-11-09 19:22:15 +01:00
6a722d1bb7 Bugfix/fix get quotes in financial modeling prep service (#2627)
* Fix get quotes

* Update changelog
2023-11-09 18:03:09 +01:00
70 changed files with 23313 additions and 6996 deletions

View File

@ -5,11 +5,62 @@ All notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
## 2.24.0 - 2023-11-16
### Changed
- Improved the language localization for German (`de`)
### Fixed
- Fixed the "too many bind variables in prepared statement" issue of the data range functionality (`getRange()`) in the market data service
## 2.23.0 - 2023-11-15
### Added
- Extended the benchmarks in the markets overview by 50-Day and 200-Day trends (experimental)
- Set up the language localization for Polski (`pl`)
### Changed
- Improved the data source validation in the activities import
- Changed _Twitter_ to _𝕏_
- Improved the selection in the twitter bot service
- Improved the language localization for German (`de`)
- Upgraded `ng-extract-i18n-merge` from version `2.7.0` to `2.8.3`
- Upgraded `prettier` from version `3.0.3` to `3.1.0`
## 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 ## 2.20.0 - 2023-11-08
### Changed ### Changed
- Removed the loading indicator of the unit on the overview tab of the home page - 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 - 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 enhancers
- Increased the timeout in the health check endpoint for data providers - Increased the timeout in the health check endpoint for data providers
@ -159,7 +210,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
### Added ### Added
- Added support to transfer a part of the cash balance from one to another account - Added support to transfer a part of the cash balance from one to another account
- Extended the markets overview by benchmarks (date of last all time high) - Extended the benchmarks in the markets overview by the date of the last all time high
- Added support to import historical market data in the admin control panel - Added support to import historical market data in the admin control panel
### Changed ### Changed
@ -2399,7 +2450,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
### Added ### Added
- Added the _Ghostfolio_ trailer to the landing page - Added the _Ghostfolio_ trailer to the landing page
- Extended the markets overview by benchmarks (current change to the all time high) - Extended the benchmarks in the markets overview by the current change to the all time high
## 1.151.0 - 24.05.2022 ## 1.151.0 - 24.05.2022

View File

@ -9,17 +9,21 @@ import {
MAX_CHART_ITEMS, MAX_CHART_ITEMS,
PROPERTY_BENCHMARKS PROPERTY_BENCHMARKS
} from '@ghostfolio/common/config'; } from '@ghostfolio/common/config';
import { DATE_FORMAT } from '@ghostfolio/common/helper'; import {
DATE_FORMAT,
calculateBenchmarkTrend
} from '@ghostfolio/common/helper';
import { import {
BenchmarkMarketDataDetails, BenchmarkMarketDataDetails,
BenchmarkProperty, BenchmarkProperty,
BenchmarkResponse, BenchmarkResponse,
UniqueAsset UniqueAsset
} from '@ghostfolio/common/interfaces'; } from '@ghostfolio/common/interfaces';
import { BenchmarkTrend } from '@ghostfolio/common/types';
import { Injectable } from '@nestjs/common'; import { Injectable } from '@nestjs/common';
import { SymbolProfile } from '@prisma/client'; import { SymbolProfile } from '@prisma/client';
import Big from 'big.js'; import Big from 'big.js';
import { format } from 'date-fns'; import { format, subDays } from 'date-fns';
import { uniqBy } from 'lodash'; import { uniqBy } from 'lodash';
import ms from 'ms'; import ms from 'ms';
@ -45,9 +49,34 @@ export class BenchmarkService {
return 0; return 0;
} }
public async getBenchmarks({ useCache = true } = {}): Promise< public async getBenchmarkTrends({ dataSource, symbol }: UniqueAsset) {
BenchmarkResponse['benchmarks'] const historicalData = await this.marketDataService.marketDataItems({
> { orderBy: {
date: 'desc'
},
where: {
dataSource,
symbol,
date: { gte: subDays(new Date(), 400) }
}
});
const fiftyDayAverage = calculateBenchmarkTrend({
historicalData,
days: 50
});
const twoHundredDayAverage = calculateBenchmarkTrend({
historicalData,
days: 200
});
return { trend50d: fiftyDayAverage, trend200d: twoHundredDayAverage };
}
public async getBenchmarks({
enableSharing = false,
useCache = true
} = {}): Promise<BenchmarkResponse['benchmarks']> {
let benchmarks: BenchmarkResponse['benchmarks']; let benchmarks: BenchmarkResponse['benchmarks'];
if (useCache) { if (useCache) {
@ -62,9 +91,16 @@ export class BenchmarkService {
} catch {} } catch {}
} }
const benchmarkAssetProfiles = await this.getBenchmarkAssetProfiles(); const benchmarkAssetProfiles = await this.getBenchmarkAssetProfiles({
enableSharing
});
const promises: Promise<{ date: Date; marketPrice: number }>[] = []; const promisesAllTimeHighs: Promise<{ date: Date; marketPrice: number }>[] =
[];
const promisesBenchmarkTrends: Promise<{
trend50d: BenchmarkTrend;
trend200d: BenchmarkTrend;
}>[] = [];
const quotes = await this.dataProviderService.getQuotes({ const quotes = await this.dataProviderService.getQuotes({
items: benchmarkAssetProfiles.map(({ dataSource, symbol }) => { items: benchmarkAssetProfiles.map(({ dataSource, symbol }) => {
@ -73,10 +109,18 @@ export class BenchmarkService {
}); });
for (const { dataSource, symbol } of benchmarkAssetProfiles) { for (const { dataSource, symbol } of benchmarkAssetProfiles) {
promises.push(this.marketDataService.getMax({ dataSource, symbol })); promisesAllTimeHighs.push(
this.marketDataService.getMax({ dataSource, symbol })
);
promisesBenchmarkTrends.push(
this.getBenchmarkTrends({ dataSource, symbol })
);
} }
const allTimeHighs = await Promise.all(promises); const [allTimeHighs, benchmarkTrends] = await Promise.all([
Promise.all(promisesAllTimeHighs),
Promise.all(promisesBenchmarkTrends)
]);
let storeInCache = true; let storeInCache = true;
benchmarks = allTimeHighs.map((allTimeHigh, index) => { benchmarks = allTimeHighs.map((allTimeHigh, index) => {
@ -93,6 +137,7 @@ export class BenchmarkService {
} else { } else {
storeInCache = false; storeInCache = false;
} }
return { return {
marketCondition: this.getMarketCondition( marketCondition: this.getMarketCondition(
performancePercentFromAllTimeHigh performancePercentFromAllTimeHigh
@ -100,10 +145,12 @@ export class BenchmarkService {
name: benchmarkAssetProfiles[index].name, name: benchmarkAssetProfiles[index].name,
performances: { performances: {
allTimeHigh: { allTimeHigh: {
date: allTimeHigh.date, date: allTimeHigh?.date,
performancePercent: performancePercentFromAllTimeHigh performancePercent: performancePercentFromAllTimeHigh
} }
} },
trend50d: benchmarkTrends[index].trend50d,
trend200d: benchmarkTrends[index].trend200d
}; };
}); });
@ -118,12 +165,22 @@ export class BenchmarkService {
return benchmarks; return benchmarks;
} }
public async getBenchmarkAssetProfiles(): Promise<Partial<SymbolProfile>[]> { public async getBenchmarkAssetProfiles({
enableSharing = false
} = {}): Promise<Partial<SymbolProfile>[]> {
const symbolProfileIds: string[] = ( const symbolProfileIds: string[] = (
((await this.propertyService.getByKey( ((await this.propertyService.getByKey(
PROPERTY_BENCHMARKS PROPERTY_BENCHMARKS
)) as BenchmarkProperty[]) ?? [] )) as BenchmarkProperty[]) ?? []
).map(({ symbolProfileId }) => { )
.filter((benchmark) => {
if (enableSharing) {
return benchmark.enableSharing;
}
return true;
})
.map(({ symbolProfileId }) => {
return symbolProfileId; return symbolProfileId;
}); });

View File

@ -8,6 +8,7 @@ import {
import { OrderService } from '@ghostfolio/api/app/order/order.service'; import { OrderService } from '@ghostfolio/api/app/order/order.service';
import { PlatformService } from '@ghostfolio/api/app/platform/platform.service'; import { PlatformService } from '@ghostfolio/api/app/platform/platform.service';
import { PortfolioService } from '@ghostfolio/api/app/portfolio/portfolio.service'; import { PortfolioService } from '@ghostfolio/api/app/portfolio/portfolio.service';
import { ConfigurationService } from '@ghostfolio/api/services/configuration/configuration.service';
import { DataGatheringService } from '@ghostfolio/api/services/data-gathering/data-gathering.service'; import { DataGatheringService } from '@ghostfolio/api/services/data-gathering/data-gathering.service';
import { DataProviderService } from '@ghostfolio/api/services/data-provider/data-provider.service'; import { DataProviderService } from '@ghostfolio/api/services/data-provider/data-provider.service';
import { ExchangeRateDataService } from '@ghostfolio/api/services/exchange-rate-data/exchange-rate-data.service'; import { ExchangeRateDataService } from '@ghostfolio/api/services/exchange-rate-data/exchange-rate-data.service';
@ -33,6 +34,7 @@ import { v4 as uuidv4 } from 'uuid';
export class ImportService { export class ImportService {
public constructor( public constructor(
private readonly accountService: AccountService, private readonly accountService: AccountService,
private readonly configurationService: ConfigurationService,
private readonly dataGatheringService: DataGatheringService, private readonly dataGatheringService: DataGatheringService,
private readonly dataProviderService: DataProviderService, private readonly dataProviderService: DataProviderService,
private readonly exchangeRateDataService: ExchangeRateDataService, private readonly exchangeRateDataService: ExchangeRateDataService,
@ -570,6 +572,12 @@ export class ImportService {
index, index,
{ currency, dataSource, symbol } { currency, dataSource, symbol }
] of uniqueActivitiesDto.entries()) { ] of uniqueActivitiesDto.entries()) {
if (!this.configurationService.get('DATA_SOURCES').includes(dataSource)) {
throw new Error(
`activities.${index}.dataSource ("${dataSource}") is not valid`
);
}
if (dataSource !== 'MANUAL') { if (dataSource !== 'MANUAL') {
const assetProfile = ( const assetProfile = (
await this.dataProviderService.getAssetProfiles([ await this.dataProviderService.getAssetProfiles([

View File

@ -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()

View File

@ -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

View File

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

View File

@ -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

View File

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

View File

@ -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 }) => {

View File

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

View File

@ -66,6 +66,10 @@
<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>
@ -126,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>
@ -368,6 +376,10 @@
<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>
@ -428,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>
@ -694,6 +710,10 @@
<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>
@ -754,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>
@ -866,6 +890,10 @@
<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>
@ -926,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>
@ -1060,6 +1092,10 @@
<loc>https://ghostfol.io/nl/veelgestelde-vragen</loc> <loc>https://ghostfol.io/nl/veelgestelde-vragen</loc>
<lastmod>${currentDate}T00:00:00+00:00</lastmod> <lastmod>${currentDate}T00:00:00+00:00</lastmod>
</url> </url>
<url>
<loc>https://ghostfol.io/pl</loc>
<lastmod>${currentDate}T00:00:00+00:00</lastmod>
</url>
<url> <url>
<loc>https://ghostfol.io/pt</loc> <loc>https://ghostfol.io/pt</loc>
<lastmod>${currentDate}T00:00:00+00:00</lastmod> <lastmod>${currentDate}T00:00:00+00:00</lastmod>

View File

@ -133,7 +133,7 @@ export class FinancialModelingPrepService implements DataProviderInterface {
abortController.abort(); abortController.abort();
}, requestTimeout); }, 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
@ -141,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(),

View File

@ -59,12 +59,12 @@ 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 this.prismaService.marketData.findMany({
orderBy: [ orderBy: [
{ {
date: 'asc' date: 'asc'
@ -74,24 +74,33 @@ export class MarketDataService {
} }
], ],
where: { where: {
dataSource: {
in: uniqueAssets.map(({ dataSource }) => {
return dataSource;
})
},
date: dateQuery, date: dateQuery,
symbol: { symbol: {
in: symbols in: uniqueAssets.map(({ symbol }) => {
return symbol;
})
} }
} }
}); });
} }
public async marketDataItems(params: { public async marketDataItems(params: {
select?: Prisma.MarketDataSelectScalar;
skip?: number; skip?: number;
take?: number; take?: number;
cursor?: Prisma.MarketDataWhereUniqueInput; cursor?: Prisma.MarketDataWhereUniqueInput;
where?: Prisma.MarketDataWhereInput; where?: Prisma.MarketDataWhereInput;
orderBy?: Prisma.MarketDataOrderByWithRelationInput; orderBy?: Prisma.MarketDataOrderByWithRelationInput;
}): Promise<MarketData[]> { }): Promise<MarketData[]> {
const { skip, take, cursor, where, orderBy } = params; const { select, skip, take, cursor, where, orderBy } = params;
return this.prismaService.marketData.findMany({ return this.prismaService.marketData.findMany({
select,
cursor, cursor,
orderBy, orderBy,
skip, skip,

View File

@ -57,7 +57,7 @@ export class TwitterBotService {
symbolItem.marketPrice symbolItem.marketPrice
}/100)`; }/100)`;
const benchmarkListing = await this.getBenchmarkListing(3); const benchmarkListing = await this.getBenchmarkListing();
if (benchmarkListing?.length > 1) { if (benchmarkListing?.length > 1) {
status += '\n\n'; status += '\n\n';
@ -78,29 +78,22 @@ export class TwitterBotService {
} }
} }
private async getBenchmarkListing(aMax: number) { private async getBenchmarkListing() {
const benchmarks = await this.benchmarkService.getBenchmarks({ const benchmarks = await this.benchmarkService.getBenchmarks({
enableSharing: true,
useCache: false useCache: false
}); });
const benchmarkListing: string[] = []; return benchmarks
.map(({ marketCondition, name, performances }) => {
for (const [index, benchmark] of benchmarks.entries()) { return `${name} ${(
if (index > aMax - 1) { performances.allTimeHigh.performancePercent * 100
break;
}
benchmarkListing.push(
`${benchmark.name} ${(
benchmark.performances.allTimeHigh.performancePercent * 100
).toFixed(1)}%${ ).toFixed(1)}%${
benchmark.marketCondition !== 'NEUTRAL_MARKET' marketCondition !== 'NEUTRAL_MARKET'
? ' ' + resolveMarketCondition(benchmark.marketCondition).emoji ? ' ' + resolveMarketCondition(marketCondition).emoji
: '' : ''
}` }`;
); })
} .join('\n');
return benchmarkListing.join('\n');
} }
} }

View File

@ -60,6 +60,10 @@
"baseHref": "/nl/", "baseHref": "/nl/",
"localize": ["nl"] "localize": ["nl"]
}, },
"development-pl": {
"baseHref": "/pl/",
"localize": ["pl"]
},
"development-pt": { "development-pt": {
"baseHref": "/pt/", "baseHref": "/pt/",
"localize": ["pt"] "localize": ["pt"]
@ -170,6 +174,9 @@
"development-nl": { "development-nl": {
"browserTarget": "client:build:development-nl" "browserTarget": "client:build:development-nl"
}, },
"development-pl": {
"browserTarget": "client:build:development-pl"
},
"development-pt": { "development-pt": {
"browserTarget": "client:build:development-pt" "browserTarget": "client:build:development-pt"
}, },
@ -193,6 +200,7 @@
"messages.fr.xlf", "messages.fr.xlf",
"messages.it.xlf", "messages.it.xlf",
"messages.nl.xlf", "messages.nl.xlf",
"messages.pl.xlf",
"messages.pt.xlf", "messages.pt.xlf",
"messages.tr.xlf" "messages.tr.xlf"
] ]
@ -235,6 +243,10 @@
"baseHref": "/nl/", "baseHref": "/nl/",
"translation": "apps/client/src/locales/messages.nl.xlf" "translation": "apps/client/src/locales/messages.nl.xlf"
}, },
"pl": {
"baseHref": "/pl/",
"translation": "apps/client/src/locales/messages.pl.xlf"
},
"pt": { "pt": {
"baseHref": "/pt/", "baseHref": "/pt/",
"translation": "apps/client/src/locales/messages.pt.xlf" "translation": "apps/client/src/locales/messages.pt.xlf"

View File

@ -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>
@ -127,8 +127,11 @@
class="align-items-baseline d-flex" class="align-items-baseline d-flex"
href="https://twitter.com/ghostfolio_" href="https://twitter.com/ghostfolio_"
target="_blank" target="_blank"
title="Follow Ghostfolio on Twitter" title="Follow Ghostfolio on X (formerly Twitter)"
>Twitter<ion-icon class="ml-1" name="open-outline"></ion-icon >X (formerly Twitter)<ion-icon
class="ml-1"
name="open-outline"
></ion-icon
></a> ></a>
</li> </li>
<li>&nbsp;</li> <li>&nbsp;</li>
@ -150,6 +153,11 @@
<li> <li>
<a href="../nl" title="Ghostfolio in Nederlands">Nederlands</a> <a href="../nl" title="Ghostfolio in Nederlands">Nederlands</a>
</li> </li>
<!--
<li>
<a href="../pl" title="Ghostfolio in Polski">Polski</a>
</li>
-->
<li> <li>
<a href="../pt" title="Ghostfolio in Português">Português</a> <a href="../pt" title="Ghostfolio in Português">Português</a>
</li> </li>

View File

@ -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() {

View File

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

View File

@ -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>

View File

@ -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,

View File

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

View File

@ -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>

View File

@ -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,

View File

@ -31,6 +31,7 @@
<gf-benchmark <gf-benchmark
[benchmarks]="benchmarks" [benchmarks]="benchmarks"
[locale]="user?.settings?.locale" [locale]="user?.settings?.locale"
[user]="user"
></gf-benchmark> ></gf-benchmark>
<ngx-skeleton-loader <ngx-skeleton-loader
*ngIf="isLoading" *ngIf="isLoading"

View File

@ -70,10 +70,6 @@ export class HomeOverviewComponent implements OnDestroy, OnInit {
.subscribe((impersonationId) => { .subscribe((impersonationId) => {
this.hasImpersonationId = !!impersonationId; this.hasImpersonationId = !!impersonationId;
this.unit = this.hasImpersonationId
? '%'
: this.user?.settings?.baseCurrency;
this.changeDetectorRef.markForCheck(); this.changeDetectorRef.markForCheck();
}); });
@ -81,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) {

View File

@ -13,6 +13,7 @@
<div class="d-flex mr-2"> <div class="d-flex mr-2">
<gf-trend-indicator <gf-trend-indicator
class="d-flex" class="d-flex"
size="large"
[isLoading]="isLoading" [isLoading]="isLoading"
[marketState]="position?.marketState" [marketState]="position?.marketState"
[range]="range" [range]="range"

View File

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

View File

@ -44,6 +44,7 @@ export class UserAccountSettingsComponent implements OnDestroy, OnInit {
'fr', 'fr',
'it', 'it',
'nl', 'nl',
'pl',
'pt', 'pt',
'tr' 'tr'
]; ];

View File

@ -74,6 +74,10 @@
>Nederlands (<ng-container i18n>Community</ng-container >Nederlands (<ng-container i18n>Community</ng-container
>)</mat-option >)</mat-option
> >
<mat-option value="pl"
>Polski (<ng-container i18n>Community</ng-container
>)</mat-option
>
<mat-option value="pt" <mat-option value="pt"
>Português (<ng-container i18n>Community</ng-container >Português (<ng-container i18n>Community</ng-container
>)</mat-option >)</mat-option

View File

@ -52,10 +52,10 @@
title="Join the Ghostfolio Slack community" title="Join the Ghostfolio Slack community"
>Slack</a >Slack</a
> >
community, tweet to community, post to
<a <a
href="https://twitter.com/ghostfolio_" href="https://twitter.com/ghostfolio_"
title="Tweet to Ghostfolio on Twitter" title="Post to Ghostfolio on X (formerly Twitter)"
>@ghostfolio_</a >@ghostfolio_</a
><ng-container *ngIf="user?.subscription?.type === 'Premium'" ><ng-container *ngIf="user?.subscription?.type === 'Premium'"
>, send an e-mail to >, send an e-mail to
@ -70,14 +70,14 @@
>GitHub</a >GitHub</a
>. >.
</p> </p>
<p class="text-center"> <p class="align-items-center d-flex justify-content-center">
<a <a
class="mx-2" class="mx-2"
href="https://twitter.com/ghostfolio_" href="https://twitter.com/ghostfolio_"
mat-icon-button mat-icon-button
title="Follow Ghostfolio on Twitter" title="Follow Ghostfolio on X (formerly Twitter)"
> >
<ion-icon name="logo-twitter"></ion-icon> <span class="line-height-1 text-center w-100">𝕏</span>
</a> </a>
<a <a
*ngIf="user?.subscription?.type === 'Premium'" *ngIf="user?.subscription?.type === 'Premium'"

View File

@ -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>

View File

@ -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,

View File

@ -1,7 +1,7 @@
<div class="container"> <div class="container">
<div class="mb-5 row"> <div class="mb-5 row">
<div class="col"> <div class="col">
<h1 class="h3 mb-4 text-center"> <h1 class="h3 line-height-1 mb-4 text-center">
<span class="d-none d-sm-block" i18n>Blog</span> <span class="d-none d-sm-block" i18n>Blog</span>
<small class="text-muted" i18n <small class="text-muted" i18n
>Discover the latest Ghostfolio updates and insights on personal >Discover the latest Ghostfolio updates and insights on personal

View File

@ -233,7 +233,7 @@
community, community,
<a <a
href="https://twitter.com/ghostfolio_" href="https://twitter.com/ghostfolio_"
title="Tweet to Ghostfolio on Twitter" title="Post to Ghostfolio on X (formerly Twitter)"
>@ghostfolio_</a >@ghostfolio_</a
><ng-container *ngIf="user?.subscription?.type === 'Premium'" ><ng-container *ngIf="user?.subscription?.type === 'Premium'"
>, >,
@ -259,10 +259,10 @@
href="https://join.slack.com/t/ghostfolio/shared_invite/zt-vsaan64h-F_I0fEo5M0P88lP9ibCxFg" href="https://join.slack.com/t/ghostfolio/shared_invite/zt-vsaan64h-F_I0fEo5M0P88lP9ibCxFg"
title="Join the Ghostfolio Slack community" title="Join the Ghostfolio Slack community"
>Slack </a >Slack </a
>community, tweet to >community, post to
<a <a
href="https://twitter.com/ghostfolio_" href="https://twitter.com/ghostfolio_"
title="Tweet to Ghostfolio on Twitter" title="Post to Ghostfolio on X (formerly Twitter)"
>@ghostfolio_</a >@ghostfolio_</a
><ng-container *ngIf="user?.subscription?.type === 'Premium'" ><ng-container *ngIf="user?.subscription?.type === 'Premium'"
>, send an e-mail to >, send an e-mail to

View File

@ -1,7 +1,7 @@
<div class="container"> <div class="container">
<div class="row"> <div class="row">
<div class="col"> <div class="col">
<h1 class="h3 mb-4 text-center"> <h1 class="h3 line-height-1 mb-4 text-center">
<span class="d-none d-sm-block" i18n>Features</span> <span class="d-none d-sm-block" i18n>Features</span>
<small class="text-muted" i18n> <small class="text-muted" i18n>
Check out the numerous features of Ghostfolio to manage your wealth Check out the numerous features of Ghostfolio to manage your wealth
@ -245,7 +245,8 @@
<h4 i18n>Multi-Language</h4> <h4 i18n>Multi-Language</h4>
<p class="m-0"> <p class="m-0">
Use Ghostfolio in multiple languages: English, Dutch, French, Use Ghostfolio in multiple languages: English, Dutch, French,
German, Italian, Portuguese, Spanish and Turkish are currently German, Italian,
<!-- Polish, -->Portuguese, Spanish and Turkish are currently
supported. supported.
</p> </p>
</div> </div>

View File

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

View File

@ -72,9 +72,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>

View File

@ -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,

View File

@ -2,6 +2,7 @@ import { Product } from '@ghostfolio/common/interfaces';
import { AllvueSystemsPageComponent } from './products/allvue-systems-page.component'; 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';
@ -18,6 +19,7 @@ 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 { MonarchMoneyPageComponent } from './products/monarch-money-page.component';
@ -84,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,
@ -252,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,

View File

@ -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'
];
}

View File

@ -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'
];
}

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@ -101,6 +101,7 @@ export const SUPPORTED_LANGUAGE_CODES = [
'fr', 'fr',
'it', 'it',
'nl', 'nl',
'pl',
'pt', 'pt',
'tr' 'tr'
]; ];

View File

@ -1,5 +1,5 @@
import * as currencies from '@dinero.js/currencies'; import * as currencies from '@dinero.js/currencies';
import { DataSource } from '@prisma/client'; import { DataSource, MarketData } from '@prisma/client';
import Big from 'big.js'; import Big from 'big.js';
import { import {
getDate, getDate,
@ -10,11 +10,11 @@ import {
parseISO, parseISO,
subDays subDays
} from 'date-fns'; } from 'date-fns';
import { de, es, fr, it, nl, pt, tr } from 'date-fns/locale'; import { de, es, fr, it, nl, pl, pt, tr } from 'date-fns/locale';
import { ghostfolioScraperApiSymbolPrefix, locale } from './config'; import { ghostfolioScraperApiSymbolPrefix, locale } from './config';
import { Benchmark, UniqueAsset } from './interfaces'; import { Benchmark, UniqueAsset } from './interfaces';
import { ColorScheme } from './types'; import { BenchmarkTrend, ColorScheme } from './types';
export const DATE_FORMAT = 'yyyy-MM-dd'; export const DATE_FORMAT = 'yyyy-MM-dd';
export const DATE_FORMAT_MONTHLY = 'MMMM yyyy'; export const DATE_FORMAT_MONTHLY = 'MMMM yyyy';
@ -22,6 +22,59 @@ export const DATE_FORMAT_YEARLY = 'yyyy';
const NUMERIC_REGEXP = /[-]{0,1}[\d]*[.,]{0,1}[\d]+/g; const NUMERIC_REGEXP = /[-]{0,1}[\d]*[.,]{0,1}[\d]+/g;
export function calculateBenchmarkTrend({
days,
historicalData
}: {
days: number;
historicalData: MarketData[];
}): BenchmarkTrend {
const hasEnoughData = historicalData.length >= 2 * days;
if (!hasEnoughData) {
return 'UNKNOWN';
}
const recentPeriodAverage = calculateMovingAverage({
days,
prices: historicalData.slice(0, days).map(({ marketPrice }) => {
return new Big(marketPrice);
})
});
const pastPeriodAverage = calculateMovingAverage({
days,
prices: historicalData.slice(days, 2 * days).map(({ marketPrice }) => {
return new Big(marketPrice);
})
});
if (recentPeriodAverage > pastPeriodAverage) {
return 'UP';
}
if (recentPeriodAverage < pastPeriodAverage) {
return 'DOWN';
}
return 'NEUTRAL';
}
export function calculateMovingAverage({
days,
prices
}: {
days: number;
prices: Big[];
}) {
return prices
.reduce((previous, current) => {
return previous.add(current);
}, new Big(0))
.div(days)
.toNumber();
}
export function capitalize(aString: string) { export function capitalize(aString: string) {
return aString.charAt(0).toUpperCase() + aString.slice(1).toLowerCase(); return aString.charAt(0).toUpperCase() + aString.slice(1).toLowerCase();
} }
@ -106,6 +159,8 @@ export function getDateFnsLocale(aLanguageCode: string) {
return it; return it;
} else if (aLanguageCode === 'nl') { } else if (aLanguageCode === 'nl') {
return nl; return nl;
} else if (aLanguageCode === 'pl') {
return pl;
} else if (aLanguageCode === 'pt') { } else if (aLanguageCode === 'pt') {
return pt; return pt;
} else if (aLanguageCode === 'tr') { } else if (aLanguageCode === 'tr') {

View File

@ -1,3 +1,4 @@
export interface BenchmarkProperty { export interface BenchmarkProperty {
enableSharing?: boolean;
symbolProfileId: string; symbolProfileId: string;
} }

View File

@ -1,3 +1,5 @@
import { BenchmarkTrend } from '@ghostfolio/common/types/';
import { EnhancedSymbolProfile } from './enhanced-symbol-profile.interface'; import { EnhancedSymbolProfile } from './enhanced-symbol-profile.interface';
export interface Benchmark { export interface Benchmark {
@ -9,4 +11,6 @@ export interface Benchmark {
performancePercent: number; performancePercent: number;
}; };
}; };
trend50d: BenchmarkTrend;
trend200d: BenchmarkTrend;
} }

View File

@ -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,

View File

@ -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[];
} }

View File

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

View File

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

View File

@ -0,0 +1 @@
export type BenchmarkTrend = 'DOWN' | 'NEUTRAL' | 'UNKNOWN' | 'UP';

View File

@ -1,6 +1,7 @@
import type { AccessWithGranteeUser } from './access-with-grantee-user.type'; import type { AccessWithGranteeUser } from './access-with-grantee-user.type';
import type { AccountWithPlatform } from './account-with-platform.type'; import type { AccountWithPlatform } from './account-with-platform.type';
import type { AccountWithValue } from './account-with-value.type'; import type { AccountWithValue } from './account-with-value.type';
import type { BenchmarkTrend } from './benchmark-trend.type';
import type { ColorScheme } from './color-scheme.type'; import type { ColorScheme } from './color-scheme.type';
import type { DateRange } from './date-range.type'; import type { DateRange } from './date-range.type';
import type { Granularity } from './granularity.type'; import type { Granularity } from './granularity.type';
@ -20,6 +21,7 @@ export type {
AccessWithGranteeUser, AccessWithGranteeUser,
AccountWithPlatform, AccountWithPlatform,
AccountWithValue, AccountWithValue,
BenchmarkTrend,
ColorScheme, ColorScheme,
DateRange, DateRange,
Granularity, Granularity,

View File

@ -6,6 +6,54 @@
</td> </td>
</ng-container> </ng-container>
<ng-container matColumnDef="trend50d">
<th
*matHeaderCellDef
class="d-none d-lg-table-cell px-2 text-right"
mat-header-cell
>
<ng-container i18n>50-Day Trend</ng-container>
</th>
<td *matCellDef="let element" class="d-none d-lg-table-cell px-2" mat-cell>
<div class="d-flex justify-content-end">
<gf-trend-indicator
*ngIf="element?.trend50d !== 'UNKNOWN'"
[value]="
element?.trend50d === 'UP'
? 0.001
: element?.trend50d === 'DOWN'
? -0.001
: 0
"
/>
</div>
</td>
</ng-container>
<ng-container matColumnDef="trend200d">
<th
*matHeaderCellDef
class="d-none d-lg-table-cell px-2 text-right"
mat-header-cell
>
<ng-container i18n>200-Day Trend</ng-container>
</th>
<td *matCellDef="let element" class="d-none d-lg-table-cell px-2" mat-cell>
<div class="d-flex justify-content-end">
<gf-trend-indicator
*ngIf="element?.trend200d !== 'UNKNOWN'"
[value]="
element?.trend200d === 'UP'
? 0.001
: element?.trend200d === 'DOWN'
? -0.001
: 0
"
/>
</div>
</td>
</ng-container>
<ng-container matColumnDef="date"> <ng-container matColumnDef="date">
<th <th
*matHeaderCellDef *matHeaderCellDef
@ -20,7 +68,7 @@
[isDate]="true" [isDate]="true"
[locale]="locale" [locale]="locale"
[value]="element?.performances?.allTimeHigh?.date" [value]="element?.performances?.allTimeHigh?.date"
></gf-value> />
</div> </div>
</td> </td>
</ng-container> </ng-container>
@ -35,7 +83,6 @@
<td *matCellDef="let element" class="px-2 text-right" mat-cell> <td *matCellDef="let element" class="px-2 text-right" mat-cell>
<gf-value <gf-value
class="d-inline-block justify-content-end" class="d-inline-block justify-content-end"
size="medium"
[isPercent]="true" [isPercent]="true"
[locale]="locale" [locale]="locale"
[ngClass]="{ [ngClass]="{
@ -45,7 +92,7 @@
element?.performances?.allTimeHigh?.performancePercent > 0 element?.performances?.allTimeHigh?.performancePercent > 0
}" }"
[value]="element?.performances?.allTimeHigh?.performancePercent" [value]="element?.performances?.allTimeHigh?.performancePercent"
></gf-value> />
</td> </td>
</ng-container> </ng-container>

View File

@ -4,9 +4,8 @@ import {
Input, Input,
OnChanges OnChanges
} from '@angular/core'; } from '@angular/core';
import { locale } from '@ghostfolio/common/config';
import { resolveMarketCondition } from '@ghostfolio/common/helper'; import { resolveMarketCondition } from '@ghostfolio/common/helper';
import { Benchmark } from '@ghostfolio/common/interfaces'; import { Benchmark, User } from '@ghostfolio/common/interfaces';
@Component({ @Component({
selector: 'gf-benchmark', selector: 'gf-benchmark',
@ -17,6 +16,7 @@ import { Benchmark } from '@ghostfolio/common/interfaces';
export class BenchmarkComponent implements OnChanges { export class BenchmarkComponent implements OnChanges {
@Input() benchmarks: Benchmark[]; @Input() benchmarks: Benchmark[];
@Input() locale: string; @Input() locale: string;
@Input() user: User;
public displayedColumns = ['name', 'date', 'change', 'marketCondition']; public displayedColumns = ['name', 'date', 'change', 'marketCondition'];
public resolveMarketCondition = resolveMarketCondition; public resolveMarketCondition = resolveMarketCondition;
@ -24,8 +24,15 @@ export class BenchmarkComponent implements OnChanges {
public constructor() {} public constructor() {}
public ngOnChanges() { public ngOnChanges() {
if (!this.locale) { if (this.user?.settings?.isExperimentalFeatures) {
this.locale = locale; this.displayedColumns = [
'name',
'trend50d',
'trend200d',
'date',
'change',
'marketCondition'
];
} }
} }
} }

View File

@ -3,6 +3,7 @@ import { CUSTOM_ELEMENTS_SCHEMA, NgModule } from '@angular/core';
import { MatTableModule } from '@angular/material/table'; import { MatTableModule } from '@angular/material/table';
import { NgxSkeletonLoaderModule } from 'ngx-skeleton-loader'; import { NgxSkeletonLoaderModule } from 'ngx-skeleton-loader';
import { GfTrendIndicatorModule } from '../trend-indicator';
import { GfValueModule } from '../value'; import { GfValueModule } from '../value';
import { BenchmarkComponent } from './benchmark.component'; import { BenchmarkComponent } from './benchmark.component';
@ -11,6 +12,7 @@ import { BenchmarkComponent } from './benchmark.component';
exports: [BenchmarkComponent], exports: [BenchmarkComponent],
imports: [ imports: [
CommonModule, CommonModule,
GfTrendIndicatorModule,
GfValueModule, GfValueModule,
MatTableModule, MatTableModule,
NgxSkeletonLoaderModule NgxSkeletonLoaderModule

View File

@ -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()"
> >

View File

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

View File

@ -13,7 +13,7 @@
*ngIf="marketState === 'closed' && range === '1d'; else delayed" *ngIf="marketState === 'closed' && range === '1d'; else delayed"
class="text-muted" class="text-muted"
name="pause-circle-outline" name="pause-circle-outline"
size="large" [size]="size"
> >
</ion-icon> </ion-icon>
<ng-template #delayed> <ng-template #delayed>
@ -21,7 +21,7 @@
*ngIf="marketState === 'delayed' && range === '1d'; else trend" *ngIf="marketState === 'delayed' && range === '1d'; else trend"
class="text-muted" class="text-muted"
name="time-outline" name="time-outline"
size="large" [size]="size"
> >
</ion-icon> </ion-icon>
</ng-template> </ng-template>
@ -31,21 +31,21 @@
*ngIf="value <= -0.0005" *ngIf="value <= -0.0005"
class="text-danger" class="text-danger"
name="arrow-down-circle-outline" name="arrow-down-circle-outline"
size="large"
[ngClass]="{ 'rotate-45-down': value > -0.01 }" [ngClass]="{ 'rotate-45-down': value > -0.01 }"
[size]="size"
></ion-icon> ></ion-icon>
<ion-icon <ion-icon
*ngIf="value > -0.0005 && value < 0.0005" *ngIf="value > -0.0005 && value < 0.0005"
class="text-muted" class="text-muted"
name="arrow-forward-circle-outline" name="arrow-forward-circle-outline"
size="large" [size]="size"
></ion-icon> ></ion-icon>
<ion-icon <ion-icon
*ngIf="value >= 0.0005" *ngIf="value >= 0.0005"
class="text-success" class="text-success"
name="arrow-up-circle-outline" name="arrow-up-circle-outline"
size="large"
[ngClass]="{ 'rotate-45-up': value < 0.01 }" [ngClass]="{ 'rotate-45-up': value < 0.01 }"
[size]="size"
></ion-icon> ></ion-icon>
</ng-container> </ng-container>
</ng-template> </ng-template>

View File

@ -11,6 +11,7 @@ export class TrendIndicatorComponent {
@Input() isLoading = false; @Input() isLoading = false;
@Input() marketState: MarketState = 'open'; @Input() marketState: MarketState = 'open';
@Input() range: DateRange = 'max'; @Input() range: DateRange = 'max';
@Input() size: 'large' | 'medium' | 'small' = 'small';
@Input() value = 0; @Input() value = 0;
public constructor() {} public constructor() {}

View File

@ -1,6 +1,6 @@
{ {
"name": "ghostfolio", "name": "ghostfolio",
"version": "2.20.0", "version": "2.24.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",
@ -177,7 +177,7 @@
"codelyzer": "6.0.1", "codelyzer": "6.0.1",
"cypress": "6.2.1", "cypress": "6.2.1",
"eslint": "8.33.0", "eslint": "8.33.0",
"eslint-config-prettier": "8.6.0", "eslint-config-prettier": "9.0.0",
"eslint-plugin-cypress": "2.14.0", "eslint-plugin-cypress": "2.14.0",
"eslint-plugin-import": "2.27.5", "eslint-plugin-import": "2.27.5",
"eslint-plugin-storybook": "0.6.12", "eslint-plugin-storybook": "0.6.12",
@ -188,7 +188,7 @@
"jest-environment-jsdom": "29.4.3", "jest-environment-jsdom": "29.4.3",
"jest-preset-angular": "13.1.1", "jest-preset-angular": "13.1.1",
"nx": "17.0.2", "nx": "17.0.2",
"prettier": "3.0.3", "prettier": "3.1.0",
"prettier-plugin-organize-attributes": "1.0.0", "prettier-plugin-organize-attributes": "1.0.0",
"react": "18.2.0", "react": "18.2.0",
"react-dom": "18.2.0", "react-dom": "18.2.0",

View File

@ -1,2 +1,2 @@
Date,Code,Currency,Price,Quantity,Action,Fee Date,Code,Currency,Price,Quantity,Action,Fee
12/12/2021,BTC,<invalid>,44558.42,1,buy,0 12/12/2021,BTCUSD,<invalid>,44558.42,1,buy,0

1 Date Code Currency Price Quantity Action Fee
2 12/12/2021 BTC BTCUSD <invalid> 44558.42 1 buy 0

159
yarn.lock
View File

@ -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,18 +149,6 @@
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":
version "16.2.8"
resolved "https://registry.yarnpkg.com/@angular-devkit/core/-/core-16.2.8.tgz#db74f3063e7fd573be7dafd022e8dc10e43140c0"
integrity sha512-PTGozYvh1Bin5lB15PwcXa26Ayd17bWGLS3H8Rs0s+04mUDvfNofmweaX1LgumWWy3nCUTDuwHxX10M3G0wE2g==
dependencies:
ajv "8.12.0"
ajv-formats "2.1.1"
jsonc-parser "3.2.0"
picomatch "2.3.1"
rxjs "7.8.1"
source-map "0.7.4"
"@angular-devkit/core@16.2.9": "@angular-devkit/core@16.2.9":
version "16.2.9" version "16.2.9"
resolved "https://registry.yarnpkg.com/@angular-devkit/core/-/core-16.2.9.tgz#81c5c95de8c423634bf93f616683045c6cdd4dd0" resolved "https://registry.yarnpkg.com/@angular-devkit/core/-/core-16.2.9.tgz#81c5c95de8c423634bf93f616683045c6cdd4dd0"
@ -184,6 +161,18 @@
rxjs "7.8.1" rxjs "7.8.1"
source-map "0.7.4" source-map "0.7.4"
"@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 "17.0.0"
resolved "https://registry.yarnpkg.com/@angular-devkit/core/-/core-17.0.0.tgz#99cd048cca37cf4d0cb60a3b6871e19449a8006a"
integrity sha512-QUu3LnEi4A8t733v2+I0sLtyBJx3Q7zdTAhaauCbxbFhDid0cbYm8hYsyG/njor1irTPxSJbn6UoetVkwUQZxg==
dependencies:
ajv "8.12.0"
ajv-formats "2.1.1"
jsonc-parser "3.2.0"
picomatch "3.0.1"
rxjs "7.8.1"
source-map "0.7.4"
"@angular-devkit/schematics@16.0.1": "@angular-devkit/schematics@16.0.1":
version "16.0.1" version "16.0.1"
resolved "https://registry.yarnpkg.com/@angular-devkit/schematics/-/schematics-16.0.1.tgz#d49387e9e41c9cce98b155da51b0e193333dd178" resolved "https://registry.yarnpkg.com/@angular-devkit/schematics/-/schematics-16.0.1.tgz#d49387e9e41c9cce98b155da51b0e193333dd178"
@ -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"
@ -10230,10 +10214,10 @@ escodegen@^2.0.0:
optionalDependencies: optionalDependencies:
source-map "~0.6.1" source-map "~0.6.1"
eslint-config-prettier@8.6.0: eslint-config-prettier@9.0.0:
version "8.6.0" version "9.0.0"
resolved "https://registry.yarnpkg.com/eslint-config-prettier/-/eslint-config-prettier-8.6.0.tgz#dec1d29ab728f4fa63061774e1672ac4e363d207" resolved "https://registry.yarnpkg.com/eslint-config-prettier/-/eslint-config-prettier-9.0.0.tgz#eb25485946dd0c66cd216a46232dc05451518d1f"
integrity sha512-bAF0eLpLVqP5oEVUFKpMA+NnRFICwn9X8B5jrR9FcqnYBuPbqWEjTEspPWMj5ye6czoSLDweCzSo3Ko7gGrZaA== integrity sha512-IcJsTkJae2S35pRsRAwoCE+925rJJStOdkKnLVgtE+tEpqU0EVVM7OqrwxqgptKdX29NUwC82I5pXsGFIgSevw==
eslint-import-resolver-node@^0.3.7: eslint-import-resolver-node@^0.3.7:
version "0.3.9" version "0.3.9"
@ -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"
@ -15919,10 +15901,10 @@ prettier-plugin-organize-attributes@1.0.0:
resolved "https://registry.yarnpkg.com/prettier-plugin-organize-attributes/-/prettier-plugin-organize-attributes-1.0.0.tgz#037870ee3111b3c1d6371f677b64888de353cc63" resolved "https://registry.yarnpkg.com/prettier-plugin-organize-attributes/-/prettier-plugin-organize-attributes-1.0.0.tgz#037870ee3111b3c1d6371f677b64888de353cc63"
integrity sha512-+NmameaLxbCcylEXsKPmawtzla5EE6ECqvGkpfQz4KM847fXDifB1gFnPQEpoADAq6IXg+cMI8Z0ISJEXa6fhg== integrity sha512-+NmameaLxbCcylEXsKPmawtzla5EE6ECqvGkpfQz4KM847fXDifB1gFnPQEpoADAq6IXg+cMI8Z0ISJEXa6fhg==
prettier@3.0.3: prettier@3.1.0:
version "3.0.3" version "3.1.0"
resolved "https://registry.yarnpkg.com/prettier/-/prettier-3.0.3.tgz#432a51f7ba422d1469096c0fdc28e235db8f9643" resolved "https://registry.yarnpkg.com/prettier/-/prettier-3.1.0.tgz#c6d16474a5f764ea1a4a373c593b779697744d5e"
integrity sha512-L/4pUDMxcNa8R/EthV08Zt42WBO4h1rarVtK0K+QJG0X187OLo7l699jWw0GKuwzkPQ//jMFA/8Xm6Fh3J/DAg== integrity sha512-TQLvXjq5IAibjh8EpBIkNKxO749UEWABoiIZehEPiY4GNpVdhaFKqSTu+QrlU6D2dPAfubRmtJTi4K4YkQ5eXw==
prettier@^2.8.0: prettier@^2.8.0:
version "2.8.8" version "2.8.8"
@ -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"