diff --git a/CHANGELOG.md b/CHANGELOG.md index 39634dfc4..f64e6dc12 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,14 +7,26 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## Unreleased +### Changed + +- Upgraded `stripe` from version `20.1.0` to `20.3.0` + +## 2.235.0 - 2026-02-03 + ### Added - Added the ability to fetch top holdings for ETF and mutual fund assets from _Yahoo Finance_ +- Added support for the impersonation mode in the endpoint `GET api/v1/account/:id/balances` +- Added an action menu to the user detail dialog in the users section of the admin control panel ### Changed +- Optimized the value redaction interceptor for the impersonation mode by introducing `fast-redact` +- Refactored `showTransactions` in favor of `showActivitiesCount` in the accounts table component +- Refactored `transactionCount` in favor of `activitiesCount` in the accounts table component - Deprecated `transactionCount` in favor of `activitiesCount` in the endpoint `GET api/v1/admin` - Removed the deprecated `firstBuyDate` in the portfolio calculator +- Upgraded `yahoo-finance2` from version `3.11.2` to `3.13.0` ## 2.234.0 - 2026-01-30 diff --git a/apps/api/src/app/account/account.controller.ts b/apps/api/src/app/account/account.controller.ts index 542b199fd..052720176 100644 --- a/apps/api/src/app/account/account.controller.ts +++ b/apps/api/src/app/account/account.controller.ts @@ -132,12 +132,16 @@ export class AccountController { @UseGuards(AuthGuard('jwt'), HasPermissionGuard) @UseInterceptors(RedactValuesInResponseInterceptor) public async getAccountBalancesById( + @Headers(HEADER_KEY_IMPERSONATION.toLowerCase()) impersonationId: string, @Param('id') id: string ): Promise { + const impersonationUserId = + await this.impersonationService.validateImpersonationId(impersonationId); + return this.accountBalanceService.getAccountBalances({ filters: [{ id, type: 'ACCOUNT' }], userCurrency: this.request.user.settings.settings.baseCurrency, - userId: this.request.user.id + userId: impersonationUserId || this.request.user.id }); } diff --git a/apps/api/src/app/portfolio/portfolio.controller.ts b/apps/api/src/app/portfolio/portfolio.controller.ts index a5a1d95ee..b8aefe0ac 100644 --- a/apps/api/src/app/portfolio/portfolio.controller.ts +++ b/apps/api/src/app/portfolio/portfolio.controller.ts @@ -195,11 +195,13 @@ export class PortfolioController { 'excludedAccountsAndActivities', 'fees', 'filteredValueInBaseCurrency', + 'fireWealth', 'grossPerformance', 'grossPerformanceWithCurrencyEffect', 'interestInBaseCurrency', 'items', 'liabilities', + 'liabilitiesInBaseCurrency', 'netPerformance', 'netPerformanceWithCurrencyEffect', 'totalBuy', diff --git a/apps/api/src/app/subscription/subscription.service.ts b/apps/api/src/app/subscription/subscription.service.ts index b38b07bb4..2c0226937 100644 --- a/apps/api/src/app/subscription/subscription.service.ts +++ b/apps/api/src/app/subscription/subscription.service.ts @@ -35,7 +35,7 @@ export class SubscriptionService { this.stripe = new Stripe( this.configurationService.get('STRIPE_SECRET_KEY'), { - apiVersion: '2025-12-15.clover' + apiVersion: '2026-01-28.clover' } ); } diff --git a/apps/api/src/helper/object.helper.spec.ts b/apps/api/src/helper/object.helper.spec.ts index e1ec81b8f..5ddff164b 100644 --- a/apps/api/src/helper/object.helper.spec.ts +++ b/apps/api/src/helper/object.helper.spec.ts @@ -1,4 +1,6 @@ -import { query, redactAttributes } from './object.helper'; +import { DEFAULT_REDACTED_PATHS } from '@ghostfolio/common/config'; + +import { query, redactPaths } from './object.helper'; describe('query', () => { it('should get market price from stock API response', () => { @@ -22,46 +24,38 @@ describe('query', () => { describe('redactAttributes', () => { it('should redact provided attributes', () => { - expect(redactAttributes({ object: {}, options: [] })).toStrictEqual({}); + expect(redactPaths({ object: {}, paths: [] })).toStrictEqual({}); - expect( - redactAttributes({ object: { value: 1000 }, options: [] }) - ).toStrictEqual({ value: 1000 }); + expect(redactPaths({ object: { value: 1000 }, paths: [] })).toStrictEqual({ + value: 1000 + }); expect( - redactAttributes({ + redactPaths({ object: { value: 1000 }, - options: [{ attribute: 'value', valueMap: { '*': null } }] + paths: ['value'] }) ).toStrictEqual({ value: null }); expect( - redactAttributes({ + redactPaths({ object: { value: 'abc' }, - options: [{ attribute: 'value', valueMap: { abc: 'xyz' } }] + paths: ['value'], + valueMap: { abc: 'xyz' } }) ).toStrictEqual({ value: 'xyz' }); expect( - redactAttributes({ + redactPaths({ object: { data: [{ value: 'a' }, { value: 'b' }] }, - options: [{ attribute: 'value', valueMap: { a: 1, b: 2 } }] + paths: ['data[*].value'], + valueMap: { a: 1, b: 2 } }) ).toStrictEqual({ data: [{ value: 1 }, { value: 2 }] }); - expect( - redactAttributes({ - object: { value1: 'a', value2: 'b' }, - options: [ - { attribute: 'value1', valueMap: { a: 'x' } }, - { attribute: 'value2', valueMap: { '*': 'y' } } - ] - }) - ).toStrictEqual({ value1: 'x', value2: 'y' }); - console.time('redactAttributes execution time'); expect( - redactAttributes({ + redactPaths({ object: { accounts: { '2e937c05-657c-4de9-8fb3-0813a2245f26': { @@ -1564,34 +1558,7 @@ describe('redactAttributes', () => { currentNetWorth: null } }, - options: [ - 'balance', - 'balanceInBaseCurrency', - 'comment', - 'convertedBalance', - 'dividendInBaseCurrency', - 'fee', - 'feeInBaseCurrency', - 'grossPerformance', - 'grossPerformanceWithCurrencyEffect', - 'investment', - 'netPerformance', - 'netPerformanceWithCurrencyEffect', - 'quantity', - 'symbolMapping', - 'totalBalanceInBaseCurrency', - 'totalValueInBaseCurrency', - 'unitPrice', - 'value', - 'valueInBaseCurrency' - ].map((attribute) => { - return { - attribute, - valueMap: { - '*': null - } - }; - }) + paths: DEFAULT_REDACTED_PATHS }) ).toStrictEqual({ accounts: { @@ -1681,7 +1648,7 @@ describe('redactAttributes', () => { ], dataSource: 'EOD_HISTORICAL_DATA', dateOfFirstActivity: '2021-11-30T23:00:00.000Z', - dividend: 0, + dividend: null, grossPerformance: null, grossPerformancePercent: 0.3183066634822068, grossPerformancePercentWithCurrencyEffect: 0.3183066634822068, @@ -1728,7 +1695,7 @@ describe('redactAttributes', () => { ], dataSource: 'YAHOO', dateOfFirstActivity: '2021-04-22T22:00:00.000Z', - dividend: 192, + dividend: null, grossPerformance: null, grossPerformancePercent: 0.3719230057375532, grossPerformancePercentWithCurrencyEffect: 0.2650716044872953, @@ -1780,7 +1747,7 @@ describe('redactAttributes', () => { ], dataSource: 'YAHOO', dateOfFirstActivity: '2018-09-30T22:00:00.000Z', - dividend: 0, + dividend: null, grossPerformance: null, grossPerformancePercent: 0.8594552890963852, grossPerformancePercentWithCurrencyEffect: 0.8594552890963852, @@ -1831,7 +1798,7 @@ describe('redactAttributes', () => { countries: [], dataSource: 'COINGECKO', dateOfFirstActivity: '2017-08-15T22:00:00.000Z', - dividend: 0, + dividend: null, grossPerformance: null, grossPerformancePercent: 17.4925166352, grossPerformancePercentWithCurrencyEffect: 17.4925166352, @@ -1882,7 +1849,7 @@ describe('redactAttributes', () => { countries: [], dataSource: 'MANUAL', dateOfFirstActivity: '2021-01-31T23:00:00.000Z', - dividend: 11.45, + dividend: null, grossPerformance: null, grossPerformancePercent: 0, grossPerformancePercentWithCurrencyEffect: -0.06153834320225245, @@ -1986,7 +1953,7 @@ describe('redactAttributes', () => { ], dataSource: 'MANUAL', dateOfFirstActivity: '2021-03-31T22:00:00.000Z', - dividend: 0, + dividend: null, grossPerformance: null, grossPerformancePercent: 0.27579517683678895, grossPerformancePercentWithCurrencyEffect: 0.458553421589667, @@ -2038,7 +2005,7 @@ describe('redactAttributes', () => { ], dataSource: 'YAHOO', dateOfFirstActivity: '2023-01-02T23:00:00.000Z', - dividend: 0, + dividend: null, grossPerformance: null, grossPerformancePercent: 0.7865431171216295, grossPerformancePercentWithCurrencyEffect: 0.7865431171216295, @@ -2090,7 +2057,7 @@ describe('redactAttributes', () => { ], dataSource: 'YAHOO', dateOfFirstActivity: '2017-01-02T23:00:00.000Z', - dividend: 0, + dividend: null, grossPerformance: null, grossPerformancePercent: 17.184314638161936, grossPerformancePercentWithCurrencyEffect: 17.184314638161936, @@ -2172,7 +2139,7 @@ describe('redactAttributes', () => { ], dataSource: 'YAHOO', dateOfFirstActivity: '2019-02-28T23:00:00.000Z', - dividend: 0, + dividend: null, grossPerformance: null, grossPerformancePercent: 0.8832083851170418, grossPerformancePercentWithCurrencyEffect: 0.8832083851170418, @@ -2567,7 +2534,7 @@ describe('redactAttributes', () => { ], dataSource: 'YAHOO', dateOfFirstActivity: '2018-02-28T23:00:00.000Z', - dividend: 0, + dividend: null, grossPerformance: null, grossPerformancePercent: 0.3683200415015591, grossPerformancePercentWithCurrencyEffect: 0.5806366182968891, @@ -2846,7 +2813,7 @@ describe('redactAttributes', () => { ], dataSource: 'YAHOO', dateOfFirstActivity: '2021-08-18T22:00:00.000Z', - dividend: 0, + dividend: null, grossPerformance: null, grossPerformancePercent: 0.3474381850624522, grossPerformancePercentWithCurrencyEffect: 0.28744846894552306, @@ -2964,7 +2931,7 @@ describe('redactAttributes', () => { assetClass: 'LIQUIDITY', assetSubClass: 'CASH', countries: [], - dividend: 0, + dividend: null, grossPerformance: null, grossPerformancePercent: 0, grossPerformancePercentWithCurrencyEffect: 0, diff --git a/apps/api/src/helper/object.helper.ts b/apps/api/src/helper/object.helper.ts index 6bb6579d2..350d5fe04 100644 --- a/apps/api/src/helper/object.helper.ts +++ b/apps/api/src/helper/object.helper.ts @@ -1,6 +1,6 @@ -import { Big } from 'big.js'; +import fastRedact from 'fast-redact'; import jsonpath from 'jsonpath'; -import { cloneDeep, isArray, isObject } from 'lodash'; +import { cloneDeep, isObject } from 'lodash'; export function hasNotDefinedValuesInObject(aObject: Object): boolean { for (const key in aObject) { @@ -42,60 +42,29 @@ export function query({ return jsonpath.query(object, pathExpression); } -export function redactAttributes({ - isFirstRun = true, +export function redactPaths({ object, - options + paths, + valueMap }: { - isFirstRun?: boolean; object: any; - options: { attribute: string; valueMap: { [key: string]: any } }[]; + paths: fastRedact.RedactOptions['paths']; + valueMap?: { [key: string]: any }; }): any { - if (!object || !options?.length) { - return object; - } - - // Create deep clone - const redactedObject = isFirstRun - ? JSON.parse(JSON.stringify(object)) - : object; - - for (const option of options) { - if (redactedObject.hasOwnProperty(option.attribute)) { - if (option.valueMap['*'] || option.valueMap['*'] === null) { - redactedObject[option.attribute] = option.valueMap['*']; - } else if (option.valueMap[redactedObject[option.attribute]]) { - redactedObject[option.attribute] = - option.valueMap[redactedObject[option.attribute]]; - } - } else { - // If the attribute is not present on the current object, - // check if it exists on any nested objects - for (const property in redactedObject) { - if (isArray(redactedObject[property])) { - redactedObject[property] = redactedObject[property].map( - (currentObject) => { - return redactAttributes({ - options, - isFirstRun: false, - object: currentObject - }); - } - ); - } else if ( - isObject(redactedObject[property]) && - !(redactedObject[property] instanceof Big) - ) { - // Recursively call the function on the nested object - redactedObject[property] = redactAttributes({ - options, - isFirstRun: false, - object: redactedObject[property] - }); + const redact = fastRedact({ + paths, + censor: (value) => { + if (valueMap) { + if (valueMap[value]) { + return valueMap[value]; + } else { + return value; } + } else { + return null; } } - } + }); - return redactedObject; + return JSON.parse(redact(object)); } diff --git a/apps/api/src/interceptors/redact-values-in-response/redact-values-in-response.interceptor.ts b/apps/api/src/interceptors/redact-values-in-response/redact-values-in-response.interceptor.ts index 5ecf7c48d..60b994cac 100644 --- a/apps/api/src/interceptors/redact-values-in-response/redact-values-in-response.interceptor.ts +++ b/apps/api/src/interceptors/redact-values-in-response/redact-values-in-response.interceptor.ts @@ -1,5 +1,8 @@ -import { redactAttributes } from '@ghostfolio/api/helper/object.helper'; -import { HEADER_KEY_IMPERSONATION } from '@ghostfolio/common/config'; +import { redactPaths } from '@ghostfolio/api/helper/object.helper'; +import { + DEFAULT_REDACTED_PATHS, + HEADER_KEY_IMPERSONATION +} from '@ghostfolio/common/config'; import { hasReadRestrictedAccessPermission, isRestrictedView @@ -39,40 +42,9 @@ export class RedactValuesInResponseInterceptor implements NestInterceptor< }) || isRestrictedView(user) ) { - data = redactAttributes({ + data = redactPaths({ object: data, - options: [ - 'balance', - 'balanceInBaseCurrency', - 'comment', - 'convertedBalance', - 'dividendInBaseCurrency', - 'fee', - 'feeInBaseCurrency', - 'grossPerformance', - 'grossPerformanceWithCurrencyEffect', - 'interestInBaseCurrency', - 'investment', - 'netPerformance', - 'netPerformanceWithCurrencyEffect', - 'quantity', - 'symbolMapping', - 'totalBalanceInBaseCurrency', - 'totalDividendInBaseCurrency', - 'totalInterestInBaseCurrency', - 'totalValueInBaseCurrency', - 'unitPrice', - 'unitPriceInAssetProfileCurrency', - 'value', - 'valueInBaseCurrency' - ].map((attribute) => { - return { - attribute, - valueMap: { - '*': null - } - }; - }) + paths: DEFAULT_REDACTED_PATHS }); } diff --git a/apps/api/src/interceptors/transform-data-source-in-response/transform-data-source-in-response.interceptor.ts b/apps/api/src/interceptors/transform-data-source-in-response/transform-data-source-in-response.interceptor.ts index 9af256671..eaa6dd08c 100644 --- a/apps/api/src/interceptors/transform-data-source-in-response/transform-data-source-in-response.interceptor.ts +++ b/apps/api/src/interceptors/transform-data-source-in-response/transform-data-source-in-response.interceptor.ts @@ -1,4 +1,4 @@ -import { redactAttributes } from '@ghostfolio/api/helper/object.helper'; +import { redactPaths } from '@ghostfolio/api/helper/object.helper'; import { ConfigurationService } from '@ghostfolio/api/services/configuration/configuration.service'; import { encodeDataSource } from '@ghostfolio/common/helper'; @@ -58,13 +58,18 @@ export class TransformDataSourceInResponseInterceptor< } } - data = redactAttributes({ + data = redactPaths({ + valueMap, object: data, - options: [ - { - valueMap, - attribute: 'dataSource' - } + paths: [ + 'activities[*].SymbolProfile.dataSource', + 'benchmarks[*].dataSource', + 'fearAndGreedIndex.CRYPTOCURRENCIES.dataSource', + 'fearAndGreedIndex.STOCKS.dataSource', + 'holdings[*].dataSource', + 'items[*].dataSource', + 'SymbolProfile.dataSource', + 'watchlist[*].dataSource' ] }); } diff --git a/apps/client/src/app/components/admin-users/admin-users.component.ts b/apps/client/src/app/components/admin-users/admin-users.component.ts index 2ae3b1a57..d479f2037 100644 --- a/apps/client/src/app/components/admin-users/admin-users.component.ts +++ b/apps/client/src/app/components/admin-users/admin-users.component.ts @@ -57,7 +57,7 @@ import { import { DeviceDetectorService } from 'ngx-device-detector'; import { NgxSkeletonLoaderModule } from 'ngx-skeleton-loader'; import { Subject } from 'rxjs'; -import { takeUntil } from 'rxjs/operators'; +import { switchMap, takeUntil, tap } from 'rxjs/operators'; @Component({ imports: [ @@ -139,8 +139,25 @@ export class GfAdminUsersComponent implements OnDestroy, OnInit { ]; } - this.route.paramMap - .pipe(takeUntil(this.unsubscribeSubject)) + this.userService.stateChanged + .pipe( + takeUntil(this.unsubscribeSubject), + tap((state) => { + if (state?.user) { + this.user = state.user; + + this.defaultDateFormat = getDateFormatString( + this.user.settings.locale + ); + + this.hasPermissionToImpersonateAllUsers = hasPermission( + this.user.permissions, + permissions.impersonateAllUsers + ); + } + }), + switchMap(() => this.route.paramMap) + ) .subscribe((params) => { const userId = params.get('userId'); @@ -149,23 +166,6 @@ export class GfAdminUsersComponent implements OnDestroy, OnInit { } }); - this.userService.stateChanged - .pipe(takeUntil(this.unsubscribeSubject)) - .subscribe((state) => { - if (state?.user) { - this.user = state.user; - - this.defaultDateFormat = getDateFormatString( - this.user.settings.locale - ); - - this.hasPermissionToImpersonateAllUsers = hasPermission( - this.user.permissions, - permissions.impersonateAllUsers - ); - } - }); - addIcons({ contractOutline, ellipsisHorizontal, @@ -208,10 +208,13 @@ export class GfAdminUsersComponent implements OnDestroy, OnInit { .deleteUser(aId) .pipe(takeUntil(this.unsubscribeSubject)) .subscribe(() => { - this.fetchUsers(); + this.router.navigate(['..'], { relativeTo: this.route }); }); }, confirmType: ConfirmationDialogType.Warn, + discardFn: () => { + this.router.navigate(['..'], { relativeTo: this.route }); + }, title: $localize`Do you really want to delete this user?` }); } @@ -293,6 +296,7 @@ export class GfAdminUsersComponent implements OnDestroy, OnInit { >(GfUserDetailDialogComponent, { autoFocus: false, data: { + currentUserId: this.user?.id, deviceType: this.deviceType, hasPermissionForSubscription: this.hasPermissionForSubscription, locale: this.user?.settings?.locale, @@ -305,10 +309,14 @@ export class GfAdminUsersComponent implements OnDestroy, OnInit { dialogRef .afterClosed() .pipe(takeUntil(this.unsubscribeSubject)) - .subscribe(() => { - this.router.navigate( - internalRoutes.adminControl.subRoutes.users.routerLink - ); + .subscribe((data) => { + if (data?.action === 'delete' && data?.userId) { + this.onDeleteUser(data.userId); + } else { + this.router.navigate( + internalRoutes.adminControl.subRoutes.users.routerLink + ); + } }); } } diff --git a/apps/client/src/app/components/benchmark-comparator/benchmark-comparator.component.ts b/apps/client/src/app/components/benchmark-comparator/benchmark-comparator.component.ts index d6df5071e..2ecefc311 100644 --- a/apps/client/src/app/components/benchmark-comparator/benchmark-comparator.component.ts +++ b/apps/client/src/app/components/benchmark-comparator/benchmark-comparator.component.ts @@ -14,6 +14,7 @@ import { LineChartItem, User } from '@ghostfolio/common/interfaces'; import { hasPermission, permissions } from '@ghostfolio/common/permissions'; import { internalRoutes } from '@ghostfolio/common/routes/routes'; import { ColorScheme } from '@ghostfolio/common/types'; +import { registerChartConfiguration } from '@ghostfolio/ui/chart'; import { GfPremiumIndicatorComponent } from '@ghostfolio/ui/premium-indicator'; import { CommonModule } from '@angular/common'; @@ -96,6 +97,8 @@ export class GfBenchmarkComparatorComponent implements OnChanges, OnDestroy { Tooltip ); + registerChartConfiguration(); + addIcons({ arrowForwardOutline }); } @@ -154,8 +157,10 @@ export class GfBenchmarkComparatorComponent implements OnChanges, OnDestroy { if (this.chartCanvas) { if (this.chart) { this.chart.data = data; + this.chart.options.plugins ??= {}; this.chart.options.plugins.tooltip = this.getTooltipPluginConfiguration(); + this.chart.update(); } else { this.chart = new Chart(this.chartCanvas.nativeElement, { diff --git a/apps/client/src/app/components/holding-detail-dialog/holding-detail-dialog.html b/apps/client/src/app/components/holding-detail-dialog/holding-detail-dialog.html index f9329dbfb..27df91a17 100644 --- a/apps/client/src/app/components/holding-detail-dialog/holding-detail-dialog.html +++ b/apps/client/src/app/components/holding-detail-dialog/holding-detail-dialog.html @@ -380,10 +380,10 @@ [deviceType]="data.deviceType" [hasPermissionToOpenDetails]="false" [locale]="user?.settings?.locale" + [showActivitiesCount]="false" [showAllocationInPercentage]="user?.settings?.isExperimentalFeatures" [showBalance]="false" [showFooter]="false" - [showTransactions]="false" [showValue]="false" [showValueInBaseCurrency]="false" /> diff --git a/apps/client/src/app/components/investment-chart/investment-chart.component.ts b/apps/client/src/app/components/investment-chart/investment-chart.component.ts index 98a52d4f9..53d4f5693 100644 --- a/apps/client/src/app/components/investment-chart/investment-chart.component.ts +++ b/apps/client/src/app/components/investment-chart/investment-chart.component.ts @@ -14,6 +14,7 @@ import { import { LineChartItem } from '@ghostfolio/common/interfaces'; import { InvestmentItem } from '@ghostfolio/common/interfaces/investment-item.interface'; import { ColorScheme, GroupBy } from '@ghostfolio/common/types'; +import { registerChartConfiguration } from '@ghostfolio/ui/chart'; import { CommonModule } from '@angular/common'; import { @@ -34,12 +35,15 @@ import { LineController, LineElement, PointElement, + type ScriptableLineSegmentContext, TimeScale, Tooltip, type TooltipOptions } from 'chart.js'; import 'chartjs-adapter-date-fns'; -import annotationPlugin from 'chartjs-plugin-annotation'; +import annotationPlugin, { + type AnnotationOptions +} from 'chartjs-plugin-annotation'; import { isAfter } from 'date-fns'; import { NgxSkeletonLoaderModule } from 'ngx-skeleton-loader'; @@ -80,6 +84,8 @@ export class GfInvestmentChartComponent implements OnChanges, OnDestroy { TimeScale, Tooltip ); + + registerChartConfiguration(); } public ngOnChanges() { @@ -154,17 +160,14 @@ export class GfInvestmentChartComponent implements OnChanges, OnDestroy { if (this.chartCanvas) { if (this.chart) { this.chart.data = chartData; + this.chart.options.plugins ??= {}; this.chart.options.plugins.tooltip = this.getTooltipPluginConfiguration(); - if ( - this.savingsRate && - // @ts-ignore - this.chart.options.plugins.annotation.annotations.savingsRate - ) { - // @ts-ignore - this.chart.options.plugins.annotation.annotations.savingsRate.value = - this.savingsRate; + const annotations = this.chart.options.plugins.annotation + .annotations as Record>; + if (this.savingsRate && annotations.savingsRate) { + annotations.savingsRate.value = this.savingsRate; } this.chart.update(); @@ -301,7 +304,7 @@ export class GfInvestmentChartComponent implements OnChanges, OnDestroy { }; } - private isInFuture(aContext: any, aValue: T) { + private isInFuture(aContext: ScriptableLineSegmentContext, aValue: T) { return isAfter(new Date(aContext?.p1?.parsed?.x), new Date()) ? aValue : undefined; diff --git a/apps/client/src/app/components/user-detail-dialog/interfaces/interfaces.ts b/apps/client/src/app/components/user-detail-dialog/interfaces/interfaces.ts index b922e7a54..ed46e8a02 100644 --- a/apps/client/src/app/components/user-detail-dialog/interfaces/interfaces.ts +++ b/apps/client/src/app/components/user-detail-dialog/interfaces/interfaces.ts @@ -1,4 +1,5 @@ export interface UserDetailDialogParams { + currentUserId: string; deviceType: string; hasPermissionForSubscription: boolean; locale: string; diff --git a/apps/client/src/app/components/user-detail-dialog/user-detail-dialog.component.ts b/apps/client/src/app/components/user-detail-dialog/user-detail-dialog.component.ts index cdf977058..6f7f4ead6 100644 --- a/apps/client/src/app/components/user-detail-dialog/user-detail-dialog.component.ts +++ b/apps/client/src/app/components/user-detail-dialog/user-detail-dialog.component.ts @@ -1,6 +1,4 @@ import { AdminUserResponse } from '@ghostfolio/common/interfaces'; -import { GfDialogFooterComponent } from '@ghostfolio/ui/dialog-footer'; -import { GfDialogHeaderComponent } from '@ghostfolio/ui/dialog-header'; import { AdminService } from '@ghostfolio/ui/services'; import { GfValueComponent } from '@ghostfolio/ui/value'; @@ -16,6 +14,10 @@ import { import { MatButtonModule } from '@angular/material/button'; import { MAT_DIALOG_DATA, MatDialogRef } from '@angular/material/dialog'; import { MatDialogModule } from '@angular/material/dialog'; +import { MatMenuModule } from '@angular/material/menu'; +import { IonIcon } from '@ionic/angular/standalone'; +import { addIcons } from 'ionicons'; +import { ellipsisVertical } from 'ionicons/icons'; import { EMPTY, Subject } from 'rxjs'; import { catchError, takeUntil } from 'rxjs/operators'; @@ -25,11 +27,11 @@ import { UserDetailDialogParams } from './interfaces/interfaces'; changeDetection: ChangeDetectionStrategy.OnPush, host: { class: 'd-flex flex-column h-100' }, imports: [ - GfDialogFooterComponent, - GfDialogHeaderComponent, GfValueComponent, + IonIcon, MatButtonModule, - MatDialogModule + MatDialogModule, + MatMenuModule ], schemas: [CUSTOM_ELEMENTS_SCHEMA], selector: 'gf-user-detail-dialog', @@ -46,7 +48,11 @@ export class GfUserDetailDialogComponent implements OnDestroy, OnInit { private changeDetectorRef: ChangeDetectorRef, @Inject(MAT_DIALOG_DATA) public data: UserDetailDialogParams, public dialogRef: MatDialogRef - ) {} + ) { + addIcons({ + ellipsisVertical + }); + } public ngOnInit() { this.adminService @@ -66,6 +72,13 @@ export class GfUserDetailDialogComponent implements OnDestroy, OnInit { }); } + public deleteUser() { + this.dialogRef.close({ + action: 'delete', + userId: this.data.userId + }); + } + public onClose() { this.dialogRef.close(); } diff --git a/apps/client/src/app/components/user-detail-dialog/user-detail-dialog.html b/apps/client/src/app/components/user-detail-dialog/user-detail-dialog.html index 60f6a2585..570dcf4d6 100644 --- a/apps/client/src/app/components/user-detail-dialog/user-detail-dialog.html +++ b/apps/client/src/app/components/user-detail-dialog/user-detail-dialog.html @@ -1,9 +1,28 @@ - - +
+ + + + +
@@ -103,7 +122,8 @@
- +
+ +
diff --git a/apps/client/src/app/pages/accounts/accounts-page.component.ts b/apps/client/src/app/pages/accounts/accounts-page.component.ts index 6c8146f77..f7e6541b5 100644 --- a/apps/client/src/app/pages/accounts/accounts-page.component.ts +++ b/apps/client/src/app/pages/accounts/accounts-page.component.ts @@ -38,6 +38,7 @@ import { GfTransferBalanceDialogComponent } from './transfer-balance/transfer-ba }) export class GfAccountsPageComponent implements OnDestroy, OnInit { public accounts: AccountModel[]; + public activitiesCount = 0; public deviceType: string; public hasImpersonationId: boolean; public hasPermissionToCreateAccount: boolean; @@ -45,7 +46,6 @@ export class GfAccountsPageComponent implements OnDestroy, OnInit { public routeQueryParams: Subscription; public totalBalanceInBaseCurrency = 0; public totalValueInBaseCurrency = 0; - public transactionCount = 0; public user: User; private unsubscribeSubject = new Subject(); @@ -128,14 +128,14 @@ export class GfAccountsPageComponent implements OnDestroy, OnInit { .subscribe( ({ accounts, + activitiesCount, totalBalanceInBaseCurrency, - totalValueInBaseCurrency, - transactionCount + totalValueInBaseCurrency }) => { this.accounts = accounts; + this.activitiesCount = activitiesCount; this.totalBalanceInBaseCurrency = totalBalanceInBaseCurrency; this.totalValueInBaseCurrency = totalValueInBaseCurrency; - this.transactionCount = transactionCount; if (this.accounts?.length <= 0) { this.router.navigate([], { queryParams: { createDialog: true } }); @@ -358,8 +358,8 @@ export class GfAccountsPageComponent implements OnDestroy, OnInit { private reset() { this.accounts = undefined; + this.activitiesCount = 0; this.totalBalanceInBaseCurrency = 0; this.totalValueInBaseCurrency = 0; - this.transactionCount = 0; } } diff --git a/apps/client/src/app/pages/accounts/accounts-page.html b/apps/client/src/app/pages/accounts/accounts-page.html index 6f29a4f7c..0c6b7b8f3 100644 --- a/apps/client/src/app/pages/accounts/accounts-page.html +++ b/apps/client/src/app/pages/accounts/accounts-page.html @@ -4,6 +4,7 @@

Accounts

({ bodyColor: `rgb(${getTextColor(colorScheme)})`, borderWidth: 1, borderColor: `rgba(${getTextColor(colorScheme)}, 0.1)`, - // @ts-expect-error: no need to set all attributes in callbacks. + // @ts-expect-error: no need to set all attributes in callbacks callbacks: { label: (context) => { let label = (context.dataset as ControllerDatasetOptions).label ?? ''; + if (label) { label += ': '; } const yPoint = (context.parsed as Point).y; + if (yPoint !== null) { if (currency) { label += `${yPoint.toLocaleString(locale, { @@ -75,10 +77,12 @@ export function getTooltipOptions({ label += yPoint.toFixed(2); } } + return label; }, title: (contexts) => { const xPoint = (contexts[0].parsed as Point).x; + if (groupBy && xPoint !== null) { return formatGroupedDate({ groupBy, date: xPoint }); } @@ -105,6 +109,7 @@ export function getTooltipPositionerMapTop( if (!position || !chart?.chartArea) { return false; } + return { x: position.x, y: chart.chartArea.top @@ -132,6 +137,7 @@ export function getVerticalHoverLinePlugin( const xValue = active[0].element.x; const context = chartCanvas.nativeElement.getContext('2d'); + if (context) { context.lineWidth = width; context.strokeStyle = color; diff --git a/libs/common/src/lib/config.ts b/libs/common/src/lib/config.ts index a10a828e1..b558ccc42 100644 --- a/libs/common/src/lib/config.ts +++ b/libs/common/src/lib/config.ts @@ -78,6 +78,58 @@ export const DEFAULT_PROCESSOR_GATHER_HISTORICAL_MARKET_DATA_CONCURRENCY = 1; export const DEFAULT_PROCESSOR_PORTFOLIO_SNAPSHOT_COMPUTATION_CONCURRENCY = 1; export const DEFAULT_PROCESSOR_PORTFOLIO_SNAPSHOT_COMPUTATION_TIMEOUT = 30000; +export const DEFAULT_REDACTED_PATHS = [ + 'accounts[*].balance', + 'accounts[*].valueInBaseCurrency', + 'activities[*].account.balance', + 'activities[*].account.comment', + 'activities[*].comment', + 'activities[*].fee', + 'activities[*].feeInAssetProfileCurrency', + 'activities[*].feeInBaseCurrency', + 'activities[*].quantity', + 'activities[*].SymbolProfile.symbolMapping', + 'activities[*].SymbolProfile.watchedByCount', + 'activities[*].value', + 'activities[*].valueInBaseCurrency', + 'balance', + 'balanceInBaseCurrency', + 'balances[*].account.balance', + 'balances[*].account.comment', + 'balances[*].value', + 'balances[*].valueInBaseCurrency', + 'comment', + 'dividendInBaseCurrency', + 'feeInBaseCurrency', + 'grossPerformance', + 'grossPerformanceWithCurrencyEffect', + 'historicalData[*].quantity', + 'holdings[*].dividend', + 'holdings[*].grossPerformance', + 'holdings[*].grossPerformanceWithCurrencyEffect', + 'holdings[*].holdings[*].valueInBaseCurrency', + 'holdings[*].investment', + 'holdings[*].netPerformance', + 'holdings[*].netPerformanceWithCurrencyEffect', + 'holdings[*].quantity', + 'holdings[*].valueInBaseCurrency', + 'interestInBaseCurrency', + 'investmentInBaseCurrencyWithCurrencyEffect', + 'netPerformance', + 'netPerformanceWithCurrencyEffect', + 'platforms[*].balance', + 'platforms[*].valueInBaseCurrency', + 'quantity', + 'SymbolProfile.symbolMapping', + 'SymbolProfile.watchedByCount', + 'totalBalanceInBaseCurrency', + 'totalDividendInBaseCurrency', + 'totalInterestInBaseCurrency', + 'totalValueInBaseCurrency', + 'value', + 'valueInBaseCurrency' +]; + // USX is handled separately export const DERIVED_CURRENCIES = [ { diff --git a/libs/ui/src/lib/accounts-table/accounts-table.component.html b/libs/ui/src/lib/accounts-table/accounts-table.component.html index f76a5d676..c9124820c 100644 --- a/libs/ui/src/lib/accounts-table/accounts-table.component.html +++ b/libs/ui/src/lib/accounts-table/accounts-table.component.html @@ -115,7 +115,7 @@ > - + - {{ transactionCount }} + {{ activitiesCount }} diff --git a/libs/ui/src/lib/accounts-table/accounts-table.component.stories.ts b/libs/ui/src/lib/accounts-table/accounts-table.component.stories.ts index 53c59a95f..96da4419d 100644 --- a/libs/ui/src/lib/accounts-table/accounts-table.component.stories.ts +++ b/libs/ui/src/lib/accounts-table/accounts-table.component.stories.ts @@ -115,10 +115,10 @@ export const Loading: Story = { hasPermissionToOpenDetails: false, locale: 'en-US', showActions: false, + showActivitiesCount: true, showAllocationInPercentage: false, showBalance: true, showFooter: true, - showTransactions: true, showValue: true, showValueInBaseCurrency: true } @@ -127,39 +127,39 @@ export const Loading: Story = { export const Default: Story = { args: { accounts, + activitiesCount: 12, baseCurrency: 'USD', deviceType: 'desktop', hasPermissionToOpenDetails: false, locale: 'en-US', showActions: false, + showActivitiesCount: true, showAllocationInPercentage: false, showBalance: true, showFooter: true, - showTransactions: true, showValue: true, showValueInBaseCurrency: true, totalBalanceInBaseCurrency: 12428.2, - totalValueInBaseCurrency: 107971.70321466809, - transactionCount: 12 + totalValueInBaseCurrency: 107971.70321466809 } }; export const WithoutFooter: Story = { args: { accounts, + activitiesCount: 12, baseCurrency: 'USD', deviceType: 'desktop', hasPermissionToOpenDetails: false, locale: 'en-US', showActions: false, + showActivitiesCount: true, showAllocationInPercentage: false, showBalance: true, showFooter: false, - showTransactions: true, showValue: true, showValueInBaseCurrency: true, totalBalanceInBaseCurrency: 12428.2, - totalValueInBaseCurrency: 107971.70321466809, - transactionCount: 12 + totalValueInBaseCurrency: 107971.70321466809 } }; diff --git a/libs/ui/src/lib/accounts-table/accounts-table.component.ts b/libs/ui/src/lib/accounts-table/accounts-table.component.ts index 8566c122d..0c0cab070 100644 --- a/libs/ui/src/lib/accounts-table/accounts-table.component.ts +++ b/libs/ui/src/lib/accounts-table/accounts-table.component.ts @@ -55,20 +55,20 @@ import { Subject, Subscription } from 'rxjs'; }) export class GfAccountsTableComponent implements OnChanges, OnDestroy { @Input() accounts: Account[]; + @Input() activitiesCount: number; @Input() baseCurrency: string; @Input() deviceType: string; @Input() hasPermissionToOpenDetails = true; @Input() locale = getLocale(); @Input() showActions: boolean; + @Input() showActivitiesCount = true; @Input() showAllocationInPercentage: boolean; @Input() showBalance = true; @Input() showFooter = true; - @Input() showTransactions = true; @Input() showValue = true; @Input() showValueInBaseCurrency = true; @Input() totalBalanceInBaseCurrency: number; @Input() totalValueInBaseCurrency: number; - @Input() transactionCount: number; @Output() accountDeleted = new EventEmitter(); @Output() accountToUpdate = new EventEmitter(); @@ -101,8 +101,8 @@ export class GfAccountsTableComponent implements OnChanges, OnDestroy { public ngOnChanges() { this.displayedColumns = ['status', 'account', 'platform']; - if (this.showTransactions) { - this.displayedColumns.push('transactions'); + if (this.showActivitiesCount) { + this.displayedColumns.push('activitiesCount'); } if (this.showBalance) { diff --git a/libs/ui/src/lib/fire-calculator/fire-calculator.component.ts b/libs/ui/src/lib/fire-calculator/fire-calculator.component.ts index a6057f124..dc800155f 100644 --- a/libs/ui/src/lib/fire-calculator/fire-calculator.component.ts +++ b/libs/ui/src/lib/fire-calculator/fire-calculator.component.ts @@ -290,8 +290,7 @@ export class GfFireCalculatorComponent implements OnChanges, OnDestroy { callbacks: { footer: (items) => { const totalAmount = items.reduce( - // @ts-ignore - (a, b) => a + b.parsed.y, + (a, b) => a + (b.parsed.y ?? 0), 0 ); @@ -313,8 +312,6 @@ export class GfFireCalculatorComponent implements OnChanges, OnDestroy { if (context.parsed.y !== null) { label += new Intl.NumberFormat(this.locale, { currency: this.currency, - // eslint-disable-next-line @typescript-eslint/ban-ts-comment - // @ts-ignore: Only supported from ES2020 or later currencyDisplay: 'code', style: 'currency' }).format(context.parsed.y); diff --git a/libs/ui/src/lib/line-chart/line-chart.component.ts b/libs/ui/src/lib/line-chart/line-chart.component.ts index 926f32896..f74fb48a1 100644 --- a/libs/ui/src/lib/line-chart/line-chart.component.ts +++ b/libs/ui/src/lib/line-chart/line-chart.component.ts @@ -25,7 +25,7 @@ import { ViewChild } from '@angular/core'; import { - type AnimationSpec, + type AnimationsSpec, Chart, Filler, LinearScale, @@ -39,6 +39,8 @@ import { import 'chartjs-adapter-date-fns'; import { NgxSkeletonLoaderModule } from 'ngx-skeleton-loader'; +import { registerChartConfiguration } from '../chart'; + @Component({ changeDetection: ChangeDetectionStrategy.OnPush, imports: [CommonModule, NgxSkeletonLoaderModule], @@ -85,6 +87,8 @@ export class GfLineChartComponent TimeScale, Tooltip ); + + registerChartConfiguration(); } public ngAfterViewInit() { @@ -131,8 +135,10 @@ export class GfLineChartComponent 0, 0, 0, - // @ts-ignore - (this.chartCanvas.nativeElement.parentNode.offsetHeight * 4) / 5 + ((this.chartCanvas.nativeElement.parentNode as HTMLElement) + .offsetHeight * + 4) / + 5 ); if (gradient && this.showGradient) { @@ -175,12 +181,12 @@ export class GfLineChartComponent if (this.chart) { this.chart.data = data; - this.chart.options.animations = this.isAnimated - ? animations - : undefined; this.chart.options.plugins ??= {}; this.chart.options.plugins.tooltip = this.getTooltipPluginConfiguration(); + this.chart.options.animations = this.isAnimated + ? animations + : undefined; this.chart.update(); } else { @@ -296,7 +302,7 @@ export class GfLineChartComponent }: { axis: 'x' | 'y'; labels: string[]; - }): AnimationSpec<'line'> { + }): Partial[string]> { const delayBetweenPoints = this.ANIMATION_DURATION / labels.length; return { @@ -306,8 +312,7 @@ export class GfLineChartComponent } context[`${axis}Started`] = true; - // @ts-ignore - return context.index * delayBetweenPoints; + return context.dataIndex * delayBetweenPoints; }, duration: delayBetweenPoints, easing: 'linear', diff --git a/libs/ui/src/lib/portfolio-proportion-chart/portfolio-proportion-chart.component.ts b/libs/ui/src/lib/portfolio-proportion-chart/portfolio-proportion-chart.component.ts index fa50f1b51..5ecedc4f9 100644 --- a/libs/ui/src/lib/portfolio-proportion-chart/portfolio-proportion-chart.component.ts +++ b/libs/ui/src/lib/portfolio-proportion-chart/portfolio-proportion-chart.component.ts @@ -29,9 +29,7 @@ import { type ChartDataset, DoughnutController, LinearScale, - type Plugin, Tooltip, - type TooltipItem, type TooltipOptions } from 'chart.js'; import ChartDataLabels from 'chartjs-plugin-datalabels'; @@ -357,23 +355,22 @@ export class GfPortfolioProportionChartComponent layout: { padding: this.showLabels === true ? 100 : 0 }, - onClick: (event, activeElements) => { + onClick: (_, activeElements, chart) => { try { const dataIndex = activeElements[0].index; - // @ts-ignore - const symbol: string = event.chart.data.labels[dataIndex]; + const symbol = chart.data.labels?.[dataIndex] as string; - const dataSource = this.data[symbol].dataSource!; + const dataSource = this.data[symbol].dataSource; - this.proportionChartClicked.emit({ dataSource, symbol }); + if (dataSource) { + this.proportionChartClicked.emit({ dataSource, symbol }); + } } catch {} }, onHover: (event, chartElement) => { if (this.cursor) { - // @ts-ignore - event.native.target.style.cursor = chartElement[0] - ? this.cursor - : 'default'; + (event.native?.target as HTMLElement).style.cursor = + chartElement[0] ? this.cursor : 'default'; } }, plugins: { @@ -407,7 +404,7 @@ export class GfPortfolioProportionChartComponent tooltip: this.getTooltipPluginConfiguration(data) } }, - plugins: [ChartDataLabels as Plugin<'doughnut'>], + plugins: [ChartDataLabels], type: 'doughnut' }); } @@ -442,13 +439,15 @@ export class GfPortfolioProportionChartComponent currency: this.baseCurrency, locale: this.locale }), - // @ts-ignore + // @ts-expect-error: no need to set all attributes in callbacks callbacks: { label: (context: TooltipItem<'doughnut'>) => { const labelIndex = (data.datasets[context.datasetIndex - 1]?.data?.length ?? 0) + context.dataIndex; - let symbol = context.chart.data.labels?.[labelIndex] ?? ''; + + let symbol = + (context.chart.data.labels?.[labelIndex] as string) ?? ''; if (symbol === this.OTHER_KEY) { symbol = $localize`Other`; @@ -456,9 +455,10 @@ export class GfPortfolioProportionChartComponent symbol = $localize`No data available`; } - const name = translate(this.data[symbol as string]?.name); + const name = translate(this.data[symbol]?.name); let sum = 0; + for (const item of context.dataset.data) { sum += item; } @@ -471,6 +471,7 @@ export class GfPortfolioProportionChartComponent return [`${name ?? symbol}`, `${percentage.toFixed(2)}%`]; } else { const value = context.raw as number; + return [ `${name ?? symbol}`, `${value.toLocaleString(this.locale, { diff --git a/libs/ui/src/lib/treemap-chart/interfaces/interfaces.ts b/libs/ui/src/lib/treemap-chart/interfaces/interfaces.ts index 0b567a8f5..e8d182adb 100644 --- a/libs/ui/src/lib/treemap-chart/interfaces/interfaces.ts +++ b/libs/ui/src/lib/treemap-chart/interfaces/interfaces.ts @@ -1,9 +1,7 @@ import { PortfolioPosition } from '@ghostfolio/common/interfaces'; -import { - TreemapDataPoint, - TreemapScriptableContext -} from 'chartjs-chart-treemap'; +import { ScriptableContext, TooltipItem } from 'chart.js'; +import { TreemapDataPoint } from 'chartjs-chart-treemap'; export interface GetColorParams { annualizedNetPerformancePercent: number; @@ -11,8 +9,13 @@ export interface GetColorParams { positiveNetPerformancePercentsRange: { max: number; min: number }; } -export interface GfTreemapChartTooltipContext extends TreemapScriptableContext { - raw: TreemapDataPoint & { - _data: PortfolioPosition; - }; +interface GfTreemapDataPoint extends TreemapDataPoint { + _data: PortfolioPosition; +} + +export interface GfTreemapScriptableContext extends ScriptableContext<'treemap'> { + raw: GfTreemapDataPoint; +} +export interface GfTreemapTooltipItem extends TooltipItem<'treemap'> { + raw: GfTreemapDataPoint; } diff --git a/libs/ui/src/lib/treemap-chart/treemap-chart.component.ts b/libs/ui/src/lib/treemap-chart/treemap-chart.component.ts index a25ab3f8d..a80876f6a 100644 --- a/libs/ui/src/lib/treemap-chart/treemap-chart.component.ts +++ b/libs/ui/src/lib/treemap-chart/treemap-chart.component.ts @@ -35,9 +35,10 @@ import { orderBy } from 'lodash'; import { NgxSkeletonLoaderModule } from 'ngx-skeleton-loader'; import OpenColor from 'open-color'; -import { +import type { GetColorParams, - GfTreemapChartTooltipContext + GfTreemapScriptableContext, + GfTreemapTooltipItem } from './interfaces/interfaces'; const { gray, green, red } = OpenColor; @@ -204,7 +205,7 @@ export class GfTreemapChartComponent const data: ChartData<'treemap'> = { datasets: [ { - backgroundColor: (context: GfTreemapChartTooltipContext) => { + backgroundColor: (context: GfTreemapScriptableContext) => { let annualizedNetPerformancePercent = getAnnualizedPerformancePercent({ daysInMarket: differenceInDays( @@ -235,7 +236,7 @@ export class GfTreemapChartComponent key: 'allocationInPercentage', labels: { align: 'left', - color: (context: GfTreemapChartTooltipContext) => { + color: (context: GfTreemapScriptableContext) => { let annualizedNetPerformancePercent = getAnnualizedPerformancePercent({ daysInMarket: differenceInDays( @@ -264,7 +265,7 @@ export class GfTreemapChartComponent }, display: true, font: [{ size: 16 }, { lineHeight: 1.5, size: 14 }], - formatter: ({ raw }: GfTreemapChartTooltipContext) => { + formatter: ({ raw }: GfTreemapScriptableContext) => { // Round to 4 decimal places let netPerformancePercentWithCurrencyEffect = Math.round( @@ -289,7 +290,7 @@ export class GfTreemapChartComponent position: 'top' }, spacing: 1, - // @ts-ignore + // @ts-expect-error: should be PortfolioPosition[] tree: this.holdings } ] @@ -308,17 +309,16 @@ export class GfTreemapChartComponent data, options: { animation: false, - onClick: (event, activeElements) => { + onClick: (_, activeElements, chart: Chart<'treemap'>) => { try { const dataIndex = activeElements[0].index; const datasetIndex = activeElements[0].datasetIndex; const dataset = orderBy( - // @ts-ignore - event.chart.data.datasets[datasetIndex].tree, + chart.data.datasets[datasetIndex].tree, ['allocationInPercentage'], ['desc'] - ); + ) as PortfolioPosition[]; const dataSource: DataSource = dataset[dataIndex].dataSource; const symbol: string = dataset[dataIndex].symbol; @@ -328,10 +328,8 @@ export class GfTreemapChartComponent }, onHover: (event, chartElement) => { if (this.cursor) { - // @ts-ignore - event.native.target.style.cursor = chartElement[0] - ? this.cursor - : 'default'; + (event.native?.target as HTMLElement).style.cursor = + chartElement[0] ? this.cursor : 'default'; } }, plugins: { @@ -353,9 +351,9 @@ export class GfTreemapChartComponent currency: this.baseCurrency, locale: this.locale }), + // @ts-expect-error: no need to set all attributes in callbacks callbacks: { - // @ts-ignore - label: ({ raw }: GfTreemapChartTooltipContext) => { + label: ({ raw }: GfTreemapTooltipItem) => { const allocationInPercentage = `${(raw._data.allocationInPercentage * 100).toFixed(2)}%`; const name = raw._data.name; const sign = diff --git a/package-lock.json b/package-lock.json index b8d343edd..25cee635d 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "ghostfolio", - "version": "2.234.0", + "version": "2.235.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "ghostfolio", - "version": "2.234.0", + "version": "2.235.0", "hasInstallScript": true, "license": "AGPL-3.0", "dependencies": { @@ -62,6 +62,7 @@ "dotenv": "17.2.3", "dotenv-expand": "12.0.3", "envalid": "8.1.1", + "fast-redact": "3.5.0", "fuse.js": "7.1.0", "google-spreadsheet": "3.2.0", "helmet": "7.0.0", @@ -84,11 +85,11 @@ "passport-openidconnect": "0.1.2", "reflect-metadata": "0.2.2", "rxjs": "7.8.1", - "stripe": "20.1.0", + "stripe": "20.3.0", "svgmap": "2.14.0", "tablemark": "4.1.0", "twitter-api-v2": "1.27.0", - "yahoo-finance2": "3.11.2", + "yahoo-finance2": "3.13.0", "zone.js": "0.16.0" }, "devDependencies": { @@ -122,6 +123,7 @@ "@storybook/angular": "10.1.10", "@trivago/prettier-plugin-sort-imports": "5.2.2", "@types/big.js": "6.2.2", + "@types/fast-redact": "3.0.4", "@types/google-spreadsheet": "3.1.5", "@types/jest": "30.0.0", "@types/jsonpath": "0.2.4", @@ -12936,6 +12938,13 @@ "@types/send": "*" } }, + "node_modules/@types/fast-redact": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/@types/fast-redact/-/fast-redact-3.0.4.tgz", + "integrity": "sha512-tgGJaXucrCH4Yx2l/AI6e/JQksZhKGIQsVwBMTh+nxUhQDv5tXScTs5DHTw+qSKDXnHL2dTAh1e2rd5pcFQyNQ==", + "dev": true, + "license": "MIT" + }, "node_modules/@types/geojson": { "version": "7946.0.16", "resolved": "https://registry.npmjs.org/@types/geojson/-/geojson-7946.0.16.tgz", @@ -19940,6 +19949,15 @@ "integrity": "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==", "license": "MIT" }, + "node_modules/fast-redact": { + "version": "3.5.0", + "resolved": "https://registry.npmjs.org/fast-redact/-/fast-redact-3.5.0.tgz", + "integrity": "sha512-dwsoQlS7h9hMeYUq1W++23NDcBLV4KqONnITDV9DjfS3q1SgDGVrBdvvTLUotWtPSD7asWDV9/CmsZPy8Hf70A==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, "node_modules/fast-safe-stringify": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/fast-safe-stringify/-/fast-safe-stringify-2.1.1.tgz", @@ -32087,13 +32105,10 @@ } }, "node_modules/stripe": { - "version": "20.1.0", - "resolved": "https://registry.npmjs.org/stripe/-/stripe-20.1.0.tgz", - "integrity": "sha512-o1VNRuMkY76ZCq92U3EH3/XHm/WHp7AerpzDs4Zyo8uE5mFL4QUcv/2SudWsSnhBSp4moO2+ZoGCZ7mT8crPmQ==", + "version": "20.3.0", + "resolved": "https://registry.npmjs.org/stripe/-/stripe-20.3.0.tgz", + "integrity": "sha512-DYzcmV1MfYhycr1GwjCjeQVYk9Gu8dpxyTlu7qeDCsuguug7oUTxPsUQuZeSf/OPzK7pofqobvOKVqAwlpgf/Q==", "license": "MIT", - "dependencies": { - "qs": "^6.11.0" - }, "engines": { "node": ">=16" }, @@ -35361,9 +35376,9 @@ } }, "node_modules/yahoo-finance2": { - "version": "3.11.2", - "resolved": "https://registry.npmjs.org/yahoo-finance2/-/yahoo-finance2-3.11.2.tgz", - "integrity": "sha512-SIvMXjrOktBRD8m+qXAGCK+vR1vwBKuMgCnvmbxv29+t6LTDu0vAUxNYfbigsMRTmBzS4F9TQwbYF90g3Om4HA==", + "version": "3.13.0", + "resolved": "https://registry.npmjs.org/yahoo-finance2/-/yahoo-finance2-3.13.0.tgz", + "integrity": "sha512-czBj2q/MD68YEsB7aXNnGhJvWxYZn01O5r/i7VYiQV2m2sWwhca6tKgjwf/LT7zHHEVxhKNiGLB46glLnmq9Ag==", "license": "MIT", "dependencies": { "@deno/shim-deno": "~0.18.0", diff --git a/package.json b/package.json index 44df5228a..9d7a37979 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "ghostfolio", - "version": "2.234.0", + "version": "2.235.0", "homepage": "https://ghostfol.io", "license": "AGPL-3.0", "repository": "https://github.com/ghostfolio/ghostfolio", @@ -106,6 +106,7 @@ "dotenv": "17.2.3", "dotenv-expand": "12.0.3", "envalid": "8.1.1", + "fast-redact": "3.5.0", "fuse.js": "7.1.0", "google-spreadsheet": "3.2.0", "helmet": "7.0.0", @@ -128,11 +129,11 @@ "passport-openidconnect": "0.1.2", "reflect-metadata": "0.2.2", "rxjs": "7.8.1", - "stripe": "20.1.0", + "stripe": "20.3.0", "svgmap": "2.14.0", "tablemark": "4.1.0", "twitter-api-v2": "1.27.0", - "yahoo-finance2": "3.11.2", + "yahoo-finance2": "3.13.0", "zone.js": "0.16.0" }, "devDependencies": { @@ -166,6 +167,7 @@ "@storybook/angular": "10.1.10", "@trivago/prettier-plugin-sort-imports": "5.2.2", "@types/big.js": "6.2.2", + "@types/fast-redact": "3.0.4", "@types/google-spreadsheet": "3.1.5", "@types/jest": "30.0.0", "@types/jsonpath": "0.2.4",