Browse Source

Merge remote-tracking branch 'origin/main' into task/improve-language-localization-for-zh-20261010

pull/8100/head
KenTandrian 5 days ago
parent
commit
d5ca88d8f1
  1. 12
      CHANGELOG.md
  2. 13
      apps/api/src/app/portfolio/calculator/portfolio-calculator.ts
  3. 44
      apps/api/src/app/portfolio/calculator/roai/portfolio-calculator.spec.ts
  4. 26
      apps/api/src/app/user/user.controller.ts
  5. 14
      apps/api/src/app/user/user.service.ts
  6. 9
      apps/api/src/assets/cryptocurrencies/cryptocurrencies.json
  7. 20
      apps/client/src/app/components/user-account-settings/user-account-settings.component.ts
  8. 14
      apps/client/src/app/components/user-account-settings/user-account-settings.html
  9. 16
      apps/client/src/app/pages/portfolio/analysis/analysis-page.html
  10. 8
      apps/client/src/app/pages/portfolio/analysis/analysis-page.scss
  11. 12
      apps/client/src/app/pages/resources/personal-finance-tools/interfaces/interfaces.ts
  12. 39
      apps/client/src/app/pages/resources/personal-finance-tools/product-page.component.ts
  13. 32
      apps/client/src/app/pages/resources/personal-finance-tools/product-page.html
  14. 2
      libs/common/src/lib/config.ts
  15. 5
      libs/common/src/lib/dtos/delete-own-user.dto.ts
  16. 4
      package-lock.json
  17. 2
      package.json
  18. 2
      prisma/migrations/20261005010000_removed_index_for_data_source_from_market_data/migration.sql
  19. 1
      prisma/schema.prisma

12
CHANGELOG.md

@ -7,19 +7,29 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
## Unreleased ## Unreleased
### Changed
- Improved the language localization for Chinese (`zh`)
## 3.82.0 - 2026-10-09
### Added ### Added
- Extended the content of the _General_ section by information about the import of activities from _Interactive Brokers_ on the Frequently Asked Questions (FAQ) page - Extended the content of the _General_ section by information about the import of activities from _Interactive Brokers_ on the Frequently Asked Questions (FAQ) page
### Changed ### Changed
- Extended the user account deletion flow in the user settings of the user account page to users without a _Security Token_ within 14 days after the registration
- Improved the _Storybook_ stories of the account selector, accounts table, activities table and holdings table components - Improved the _Storybook_ stories of the account selector, accounts table, activities table and holdings table components
- Improved the language localization for Chinese (`zh`) - Removed an unnecessary index from the market data database table
- Refreshed the cryptocurrencies list
- Improved the language localization for Spanish (`es`) - Improved the language localization for Spanish (`es`)
### Fixed ### Fixed
- Fixed the missing list numbers in the top and bottom holdings on the analysis page in Safari
- Fixed the missing first data point in the portfolio evolution chart on the analysis page - Fixed the missing first data point in the portfolio evolution chart on the analysis page
- Fixed the end date of calendar year date ranges in the portfolio performance calculation for instances in time zones with a negative UTC offset
- Fixed the missing mapping for Korea in the country weightings of the _Financial Modeling Prep_ service - Fixed the missing mapping for Korea in the country weightings of the _Financial Modeling Prep_ service
## 3.81.0 - 2026-10-07 ## 3.81.0 - 2026-10-07

13
apps/api/src/app/portfolio/calculator/portfolio-calculator.ts

@ -1407,7 +1407,7 @@ export abstract class PortfolioCalculator {
return this.snapshot.totalLiabilitiesWithCurrencyEffect; return this.snapshot.totalLiabilitiesWithCurrencyEffect;
} }
public async getPerformance({ end, start }) { public async getPerformance({ end, start }: { end: Date; start: Date }) {
await this.snapshotPromise; await this.snapshotPromise;
const { historicalData } = this.snapshot; const { historicalData } = this.snapshot;
@ -1418,14 +1418,13 @@ export abstract class PortfolioCalculator {
let netPerformanceAtStartDate: number; let netPerformanceAtStartDate: number;
let netPerformanceWithCurrencyEffectAtStartDate: number; let netPerformanceWithCurrencyEffectAtStartDate: number;
const startDate = isEqual(start, endOfDay(start)) const endDate = this.toChartDate(end);
? resetHours(start) const startDate = this.toChartDate(start);
: start;
for (const historicalDataItem of historicalData) { for (const historicalDataItem of historicalData) {
const date = resetHours(parseDate(historicalDataItem.date)); const date = resetHours(parseDate(historicalDataItem.date));
if (!isBefore(date, startDate) && !isAfter(date, end)) { if (!isBefore(date, startDate) && !isAfter(date, endDate)) {
// Take the values at the start date from the first day of the date // Take the values at the start date from the first day of the date
// range // range
if (historicalDataItemsOfDateRange.length === 0) { if (historicalDataItemsOfDateRange.length === 0) {
@ -1915,4 +1914,8 @@ export abstract class PortfolioCalculator {
await this.initialize(attempt + 1); await this.initialize(attempt + 1);
} }
} }
private toChartDate(aDate: Date) {
return isEqual(aDate, endOfDay(aDate)) ? resetHours(aDate) : aDate;
}
} }

44
apps/api/src/app/portfolio/calculator/roai/portfolio-calculator.spec.ts

@ -1,3 +1,7 @@
/**
* @jest-environment <rootDir>/jest-environment-tz.js
* @jest-environment-options {"timeZone": "America/New_York"}
*/
import { PortfolioCalculator } from '@ghostfolio/api/app/portfolio/calculator/portfolio-calculator'; import { PortfolioCalculator } from '@ghostfolio/api/app/portfolio/calculator/portfolio-calculator';
import { userDummyData } from '@ghostfolio/api/app/portfolio/calculator/portfolio-calculator-test-utils'; import { userDummyData } from '@ghostfolio/api/app/portfolio/calculator/portfolio-calculator-test-utils';
import { PortfolioCalculatorFactory } from '@ghostfolio/api/app/portfolio/calculator/portfolio-calculator.factory'; import { PortfolioCalculatorFactory } from '@ghostfolio/api/app/portfolio/calculator/portfolio-calculator.factory';
@ -9,6 +13,7 @@ import { ConfigurationService } from '@ghostfolio/api/services/configuration/con
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 { PortfolioSnapshotService } from '@ghostfolio/api/services/queues/portfolio-snapshot/portfolio-snapshot.service'; import { PortfolioSnapshotService } from '@ghostfolio/api/services/queues/portfolio-snapshot/portfolio-snapshot.service';
import { PortfolioSnapshotServiceMock } from '@ghostfolio/api/services/queues/portfolio-snapshot/portfolio-snapshot.service.mock'; import { PortfolioSnapshotServiceMock } from '@ghostfolio/api/services/queues/portfolio-snapshot/portfolio-snapshot.service.mock';
import { getIntervalFromDateRange } from '@ghostfolio/common/calculation-helper';
import { HistoricalDataItem } from '@ghostfolio/common/interfaces'; import { HistoricalDataItem } from '@ghostfolio/common/interfaces';
import { PerformanceCalculationType } from '@ghostfolio/common/types/performance-calculation-type.type'; import { PerformanceCalculationType } from '@ghostfolio/common/types/performance-calculation-type.type';
@ -183,4 +188,43 @@ describe('PortfolioCalculator', () => {
).toBeCloseTo(0.23, 10); ).toBeCloseTo(0.23, 10);
}); });
}); });
describe('get performance', () => {
it('with a calendar year date range', async () => {
const snapshot = await portfolioCalculator.getSnapshot();
snapshot.historicalData = [
getHistoricalDataItem({
date: '2016-12-31',
investment: 100,
netPerformance: 0
}),
getHistoricalDataItem({
date: '2017-12-31',
investment: 100,
netPerformance: 10
}),
getHistoricalDataItem({
date: '2018-01-01',
investment: 100,
netPerformance: 20
})
];
const { endDate, startDate } = getIntervalFromDateRange({
dateRange: '2017'
});
const { chart } = await portfolioCalculator.getPerformance({
end: endDate,
start: startDate
});
expect(
chart.map(({ date }) => {
return date;
})
).toEqual(['2016-12-31', '2017-12-31']);
});
});
}); });

26
apps/api/src/app/user/user.controller.ts

@ -71,14 +71,21 @@ export class UserController {
@Delete() @Delete()
@HasPermission(permissions.deleteOwnUser) @HasPermission(permissions.deleteOwnUser)
@UseGuards(AuthGuard('jwt'), HasPermissionGuard) @UseGuards(AuthGuard('jwt'), HasPermissionGuard, ImpersonationGuard)
public async deleteOwnUser( public async deleteOwnUser(
@Body() data: DeleteOwnUserDto @Body() data: DeleteOwnUserDto,
@Impersonation() { isActive }: ImpersonationContext
): Promise<UserModel> { ): Promise<UserModel> {
const user = await this.validateAccessToken( const user = this.request.user;
data.accessToken,
this.request.user.id if (user.provider === 'ANONYMOUS') {
); await this.validateAccessToken(data.accessToken, user.id);
} else if (isActive) {
throw new HttpException(
getReasonPhrase(StatusCodes.FORBIDDEN),
StatusCodes.FORBIDDEN
);
}
return this.userService.deleteUser({ return this.userService.deleteUser({
id: user.id id: user.id
@ -260,6 +267,13 @@ export class UserController {
accessToken: string, accessToken: string,
userId: string userId: string
): Promise<UserModel> { ): Promise<UserModel> {
if (!accessToken) {
throw new HttpException(
getReasonPhrase(StatusCodes.FORBIDDEN),
StatusCodes.FORBIDDEN
);
}
const hashedAccessToken = this.userService.createAccessToken({ const hashedAccessToken = this.userService.createAccessToken({
password: accessToken, password: accessToken,
salt: this.configurationService.get('ACCESS_TOKEN_SALT') salt: this.configurationService.get('ACCESS_TOKEN_SALT')

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

@ -14,6 +14,7 @@ import {
DEFAULT_CURRENCY, DEFAULT_CURRENCY,
DEFAULT_DATE_RANGE, DEFAULT_DATE_RANGE,
DEFAULT_LOCALE, DEFAULT_LOCALE,
DELETE_OWN_USER_PERIOD,
PROPERTY_API_KEY_GHOSTFOLIO, PROPERTY_API_KEY_GHOSTFOLIO,
PROPERTY_IS_READ_ONLY_MODE, PROPERTY_IS_READ_ONLY_MODE,
PROPERTY_MAX_DAILY_REQUESTS, PROPERTY_MAX_DAILY_REQUESTS,
@ -45,7 +46,7 @@ import { Injectable, Logger } from '@nestjs/common';
import { EventEmitter2 } from '@nestjs/event-emitter'; import { EventEmitter2 } from '@nestjs/event-emitter';
import { InjectThrottlerStorage, ThrottlerStorage } from '@nestjs/throttler'; import { InjectThrottlerStorage, ThrottlerStorage } from '@nestjs/throttler';
import { Prisma, Role, User } from '@prisma/client'; import { Prisma, Role, User } from '@prisma/client';
import { differenceInDays, subDays } from 'date-fns'; import { addMilliseconds, differenceInDays, isBefore, subDays } from 'date-fns';
import { isNil, without } from 'lodash-es'; import { isNil, without } from 'lodash-es';
import { createHmac } from 'node:crypto'; import { createHmac } from 'node:crypto';
@ -438,6 +439,17 @@ export class UserService {
currentPermissions.push(permissions.enableSubscriptionInterstitial); currentPermissions.push(permissions.enableSubscriptionInterstitial);
} }
if (
!hasRole(user, Role.DEMO) &&
user.provider !== 'ANONYMOUS' &&
isBefore(
new Date(),
addMilliseconds(user.createdAt, DELETE_OWN_USER_PERIOD)
)
) {
currentPermissions.push(permissions.deleteOwnUser);
}
currentPermissions = without( currentPermissions = without(
currentPermissions, currentPermissions,
permissions.accessHoldingsChart, permissions.accessHoldingsChart,

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

@ -3367,6 +3367,7 @@
"CARV": "CARV", "CARV": "CARV",
"CAS": "Cashaa", "CAS": "Cashaa",
"CASH": "Litecash", "CASH": "Litecash",
"CASHCAT": "Cash Cat",
"CASHCOIN": "CashCoin", "CASHCOIN": "CashCoin",
"CASHIO": "Cashio Dollar", "CASHIO": "Cashio Dollar",
"CASHLY": "Cashly", "CASHLY": "Cashly",
@ -10790,7 +10791,7 @@
"MARS": "Marscoin", "MARS": "Marscoin",
"MARS4": "MARS4", "MARS4": "MARS4",
"MARSC": "MarsCoin", "MARSC": "MarsCoin",
"MARSCOIN": "Marscoin", "MARSCOIN": "MarsCoin",
"MARSERC": "Mars", "MARSERC": "Mars",
"MARSH": "UnMarshal", "MARSH": "UnMarshal",
"MARSMI": "MarsMi", "MARSMI": "MarsMi",
@ -13919,6 +13920,7 @@
"PONKE": "Ponke", "PONKE": "Ponke",
"PONKEBNB": "Ponke BNB", "PONKEBNB": "Ponke BNB",
"PONKEI": "Chinese Ponkei the Original", "PONKEI": "Chinese Ponkei the Original",
"PONS": "Pons",
"PONTEM": "Pontem Liquidswap", "PONTEM": "Pontem Liquidswap",
"PONYO": "Ponyo Impact", "PONYO": "Ponyo Impact",
"PONZI": "PonziCoin", "PONZI": "PonziCoin",
@ -14660,6 +14662,7 @@
"RECORD": "Music Protocol", "RECORD": "Music Protocol",
"RECT": "ReflectionAI", "RECT": "ReflectionAI",
"RED": "RED", "RED": "RED",
"RED21707": "RedStone",
"REDC": "RedCab", "REDC": "RedCab",
"REDCO": "Redcoin", "REDCO": "Redcoin",
"REDDIT": "Reddit", "REDDIT": "Reddit",
@ -16488,7 +16491,7 @@
"SQUAWK": "Squawk", "SQUAWK": "Squawk",
"SQUEEZER": "Squeezer", "SQUEEZER": "Squeezer",
"SQUIBONK": "SQUIBONK", "SQUIBONK": "SQUIBONK",
"SQUID": "Squid Game", "SQUID": "SQUIDGames Token",
"SQUID2": "SquidDao", "SQUID2": "SquidDao",
"SQUIDGROW": "SquidGrow", "SQUIDGROW": "SquidGrow",
"SQUIDGROWV1": "SquidGrow v1", "SQUIDGROWV1": "SquidGrow v1",
@ -20250,7 +20253,7 @@
"ZPTC": "Zeptacoin", "ZPTC": "Zeptacoin",
"ZRC": "Zircuit", "ZRC": "Zircuit",
"ZRCOIN": "ZrCoin", "ZRCOIN": "ZrCoin",
"ZRO": "Carb0n.fi", "ZRO": "LayerZero",
"ZRO26997": "LayerZero", "ZRO26997": "LayerZero",
"ZRPY": "Zerpaay", "ZRPY": "Zerpaay",
"ZRS": "Zaros", "ZRS": "Zaros",

20
apps/client/src/app/components/user-account-settings/user-account-settings.component.ts

@ -94,6 +94,7 @@ export class GfUserAccountSettingsComponent implements OnInit {
protected hasImpersonationId: boolean; protected hasImpersonationId: boolean;
protected hasPermissionToDeleteOwnUser: boolean; protected hasPermissionToDeleteOwnUser: boolean;
protected hasPermissionToRequestOwnUserDeletion: boolean; protected hasPermissionToRequestOwnUserDeletion: boolean;
protected hasPermissionToUpdateOwnAccessToken: boolean;
protected hasPermissionToUpdateViewMode: boolean; protected hasPermissionToUpdateViewMode: boolean;
protected hasPermissionToUpdateUserSettings: boolean; protected hasPermissionToUpdateUserSettings: boolean;
protected isAccessTokenHidden = true; protected isAccessTokenHidden = true;
@ -194,6 +195,11 @@ export class GfUserAccountSettingsComponent implements OnInit {
permissions.requestOwnUserDeletion permissions.requestOwnUserDeletion
); );
this.hasPermissionToUpdateOwnAccessToken = hasPermission(
this.user.permissions,
permissions.updateOwnAccessToken
);
this.hasPermissionToUpdateUserSettings = hasPermission( this.hasPermissionToUpdateUserSettings = hasPermission(
this.user.permissions, this.user.permissions,
permissions.updateUserSettings permissions.updateUserSettings
@ -271,13 +277,19 @@ export class GfUserAccountSettingsComponent implements OnInit {
this.notificationService.confirm({ this.notificationService.confirm({
confirmFn: () => { confirmFn: () => {
this.dataService this.dataService
.deleteOwnUser({ .deleteOwnUser(
accessToken: this.deleteOwnUserForm.controls.accessToken.value this.hasPermissionToUpdateOwnAccessToken
}) ? {
accessToken: this.deleteOwnUserForm.controls.accessToken.value
}
: {}
)
.pipe( .pipe(
catchError(() => { catchError(() => {
this.notificationService.alert({ this.notificationService.alert({
title: $localize`Oops! Incorrect Security Token.` title: this.hasPermissionToUpdateOwnAccessToken
? $localize`Oops! Incorrect Security Token.`
: $localize`Oops! Something went wrong.`
}); });
return EMPTY; return EMPTY;

14
apps/client/src/app/components/user-account-settings/user-account-settings.html

@ -318,7 +318,10 @@
<div class="d-flex py-1"> <div class="d-flex py-1">
<div class="pr-1 text-danger w-50" i18n>Danger Zone</div> <div class="pr-1 text-danger w-50" i18n>Danger Zone</div>
<div class="pl-1 w-50"> <div class="pl-1 w-50">
@if (hasPermissionToDeleteOwnUser) { @if (
hasPermissionToDeleteOwnUser &&
hasPermissionToUpdateOwnAccessToken
) {
<form <form
class="w-100" class="w-100"
[formGroup]="deleteOwnUserForm" [formGroup]="deleteOwnUserForm"
@ -363,6 +366,15 @@
Close Account Close Account
</button> </button>
</form> </form>
} @else if (hasPermissionToDeleteOwnUser && !hasImpersonationId) {
<button
color="warn"
i18n
mat-flat-button
(click)="onCloseAccount()"
>
Close Account
</button>
} @else if ( } @else if (
hasPermissionToRequestOwnUserDeletion && hasPermissionToRequestOwnUserDeletion &&
(user?.accounts?.length > 0 || user?.activitiesCount > 0) (user?.accounts?.length > 0 || user?.activitiesCount > 0)

16
apps/client/src/app/pages/portfolio/analysis/analysis-page.html

@ -330,7 +330,7 @@
> >
</mat-card-header> </mat-card-header>
<mat-card-content> <mat-card-content>
<ol class="mb-0 ml-1 pl-3"> <ol class="holdings mb-0 ml-1 pl-3">
@for (holding of top3; track holding) { @for (holding of top3; track holding) {
<li class="py-1"> <li class="py-1">
<a <a
@ -342,8 +342,10 @@
}" }"
[routerLink]="[]" [routerLink]="[]"
> >
<div class="flex-grow-1 mr-2 text-truncate"> <div class="flex-grow-1 mr-2 name">
{{ holding.assetProfile.name }} <span class="align-top d-inline-block mw-100 text-truncate">
{{ holding.assetProfile.name }}
</span>
</div> </div>
<div class="d-flex justify-content-end"> <div class="d-flex justify-content-end">
<gf-value <gf-value
@ -381,7 +383,7 @@
> >
</mat-card-header> </mat-card-header>
<mat-card-content> <mat-card-content>
<ol class="mb-0 ml-1 pl-3"> <ol class="holdings mb-0 ml-1 pl-3">
@for (holding of bottom3; track holding) { @for (holding of bottom3; track holding) {
<li class="py-1"> <li class="py-1">
<a <a
@ -393,8 +395,10 @@
}" }"
[routerLink]="[]" [routerLink]="[]"
> >
<div class="flex-grow-1 mr-2 text-truncate"> <div class="flex-grow-1 mr-2 name">
{{ holding.assetProfile.name }} <span class="align-top d-inline-block mw-100 text-truncate">
{{ holding.assetProfile.name }}
</span>
</div> </div>
<div class="d-flex justify-content-end"> <div class="d-flex justify-content-end">
<gf-value <gf-value

8
apps/client/src/app/pages/portfolio/analysis/analysis-page.scss

@ -4,4 +4,12 @@
.chart-container { .chart-container {
aspect-ratio: 16 / 9; aspect-ratio: 16 / 9;
} }
.holdings {
.name {
// Safari puts the list marker into this element, so the truncation is
// on the inner element (a hidden overflow here hides the marker)
min-width: 0;
}
}
} }

12
apps/client/src/app/pages/resources/personal-finance-tools/interfaces/interfaces.ts

@ -1,6 +1,16 @@
import type { Product } from '@ghostfolio/common/interfaces'; import type { Product } from '@ghostfolio/common/interfaces';
export type ResolvedProduct = Omit<Product, 'categories' | 'platforms'> & { export interface ResolvedRegion {
emojiFlag?: string;
name: string;
}
export type ResolvedProduct = Omit<
Product,
'categories' | 'origin' | 'platforms' | 'regions'
> & {
categories?: string[]; categories?: string[];
origin?: ResolvedRegion;
platforms?: string[]; platforms?: string[];
regions?: ResolvedRegion[];
}; };

39
apps/client/src/app/pages/resources/personal-finance-tools/product-page.component.ts

@ -1,4 +1,4 @@
import { getCountryName } from '@ghostfolio/common/helper'; import { getCountryName, getEmojiFlag } from '@ghostfolio/common/helper';
import { Product } from '@ghostfolio/common/interfaces'; import { Product } from '@ghostfolio/common/interfaces';
import { personalFinanceTools } from '@ghostfolio/common/personal-finance-tools'; import { personalFinanceTools } from '@ghostfolio/common/personal-finance-tools';
import { publicRoutes } from '@ghostfolio/common/routes/routes'; import { publicRoutes } from '@ghostfolio/common/routes/routes';
@ -14,7 +14,7 @@ import {
import { MatButtonModule } from '@angular/material/button'; import { MatButtonModule } from '@angular/material/button';
import { ActivatedRoute, RouterModule } from '@angular/router'; import { ActivatedRoute, RouterModule } from '@angular/router';
import { ResolvedProduct } from './interfaces/interfaces'; import { ResolvedProduct, ResolvedRegion } from './interfaces/interfaces';
@Component({ @Component({
changeDetection: ChangeDetectionStrategy.OnPush, changeDetection: ChangeDetectionStrategy.OnPush,
@ -58,9 +58,9 @@ export class GfProductPageComponent {
// 'Ukrainian (Українська)' // 'Ukrainian (Українська)'
], ],
name: 'Ghostfolio', name: 'Ghostfolio',
origin: getCountryName({ code: 'CH' }), origin: this.getResolvedCountry('CH'),
platforms: this.getSortedTranslations(['ANDROID', 'WEB']), platforms: this.getSortedTranslations(['ANDROID', 'WEB']),
regions: [$localize`Global`], regions: [{ name: $localize`Global` }],
slogan: 'Open Source Wealth Management', slogan: 'Open Source Wealth Management',
useAnonymously: true useAnonymously: true
})); }));
@ -75,20 +75,16 @@ export class GfProductPageComponent {
name: product?.name ?? '', name: product?.name ?? '',
...product, ...product,
categories: this.getSortedTranslations(product?.categories), categories: this.getSortedTranslations(product?.categories),
platforms: this.getSortedTranslations(product?.platforms) origin: product?.origin
}; ? this.getResolvedCountry(product.origin)
: undefined,
if (mappedProduct.origin) { platforms: this.getSortedTranslations(product?.platforms),
mappedProduct.origin = getCountryName({ code: mappedProduct.origin }); regions: product?.regions?.map((region) => {
}
if (mappedProduct.regions) {
mappedProduct.regions = mappedProduct.regions.map((region) => {
return region === 'Global' return region === 'Global'
? translate(region) ? { name: translate(region) }
: getCountryName({ code: region }); : this.getResolvedCountry(region);
}); })
} };
return mappedProduct; return mappedProduct;
}); });
@ -122,7 +118,7 @@ export class GfProductPageComponent {
...(categories ?? []), ...(categories ?? []),
...(platforms ?? []), ...(platforms ?? []),
name, name,
origin origin?.name
]; ];
} }
), ),
@ -175,6 +171,13 @@ export class GfProductPageComponent {
}); });
} }
private getResolvedCountry(code: string): ResolvedRegion {
return {
emojiFlag: getEmojiFlag(code),
name: getCountryName({ code })
};
}
private getSortedTranslations(values?: string[]) { private getSortedTranslations(values?: string[]) {
return values return values
?.map((value) => { ?.map((value) => {

32
apps/client/src/app/pages/resources/personal-finance-tools/product-page.html

@ -100,27 +100,47 @@
</tr> </tr>
<tr class="mat-mdc-row"> <tr class="mat-mdc-row">
<td class="mat-mdc-cell px-3 py-2 text-right" i18n>Origin</td> <td class="mat-mdc-cell px-3 py-2 text-right" i18n>Origin</td>
<td class="mat-mdc-cell px-1 py-2">{{ product1().origin }}</td> <td class="mat-mdc-cell px-1 py-2">
<td class="mat-mdc-cell px-1 py-2">{{ product2().origin }}</td> @if (product1().origin; as origin) {
<span aria-hidden="true">{{ origin.emojiFlag }}</span
>&nbsp;{{ origin.name }}
}
</td>
<td class="mat-mdc-cell px-1 py-2">
@if (product2().origin; as origin) {
<span aria-hidden="true">{{ origin.emojiFlag }}</span
>&nbsp;{{ origin.name }}
}
</td>
</tr> </tr>
<tr class="mat-mdc-row"> <tr class="mat-mdc-row">
<td class="mat-mdc-cell px-3 py-2 text-right" i18n>Region</td> <td class="mat-mdc-cell px-3 py-2 text-right" i18n>Region</td>
<td class="mat-mdc-cell px-1 py-2"> <td class="mat-mdc-cell px-1 py-2">
@for ( @for (
region of product1().regions; region of product1().regions;
track region; track region.name;
let isLast = $last let isLast = $last
) { ) {
{{ region }}{{ isLast ? '' : ', ' }} @if (region.emojiFlag) {
<span aria-hidden="true">{{ region.emojiFlag }}</span
>&nbsp;{{ region.name }}{{ isLast ? '' : ', ' }}
} @else {
{{ region.name }}{{ isLast ? '' : ', ' }}
}
} }
</td> </td>
<td class="mat-mdc-cell px-1 py-2"> <td class="mat-mdc-cell px-1 py-2">
@for ( @for (
region of product2().regions; region of product2().regions;
track region; track region.name;
let isLast = $last let isLast = $last
) { ) {
{{ region }}{{ isLast ? '' : ', ' }} @if (region.emojiFlag) {
<span aria-hidden="true">{{ region.emojiFlag }}</span
>&nbsp;{{ region.name }}{{ isLast ? '' : ', ' }}
} @else {
{{ region.name }}{{ isLast ? '' : ', ' }}
}
} }
</td> </td>
</tr> </tr>

2
libs/common/src/lib/config.ts

@ -198,6 +198,8 @@ export const DEFAULT_REDACTED_PATHS = [
'valueInBaseCurrency' 'valueInBaseCurrency'
]; ];
export const DELETE_OWN_USER_PERIOD = ms('2 weeks');
// USX is handled separately // USX is handled separately
export const DERIVED_CURRENCIES = [ export const DERIVED_CURRENCIES = [
{ {

5
libs/common/src/lib/dtos/delete-own-user.dto.ts

@ -1,6 +1,7 @@
import { IsString } from 'class-validator'; import { IsOptional, IsString } from 'class-validator';
export class DeleteOwnUserDto { export class DeleteOwnUserDto {
@IsOptional()
@IsString() @IsString()
accessToken: string; accessToken?: string;
} }

4
package-lock.json

@ -1,12 +1,12 @@
{ {
"name": "ghostfolio", "name": "ghostfolio",
"version": "3.81.0", "version": "3.82.0",
"lockfileVersion": 3, "lockfileVersion": 3,
"requires": true, "requires": true,
"packages": { "packages": {
"": { "": {
"name": "ghostfolio", "name": "ghostfolio",
"version": "3.81.0", "version": "3.82.0",
"hasInstallScript": true, "hasInstallScript": true,
"license": "AGPL-3.0", "license": "AGPL-3.0",
"dependencies": { "dependencies": {

2
package.json

@ -1,6 +1,6 @@
{ {
"name": "ghostfolio", "name": "ghostfolio",
"version": "3.81.0", "version": "3.82.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",

2
prisma/migrations/20261005010000_removed_index_for_data_source_from_market_data/migration.sql

@ -0,0 +1,2 @@
-- DropIndex
DROP INDEX "MarketData_dataSource_idx";

1
prisma/schema.prisma

@ -174,7 +174,6 @@ model MarketData {
symbol String symbol String
@@unique([dataSource, symbol, date]) @@unique([dataSource, symbol, date])
@@index([dataSource])
@@index([date]) @@index([date])
@@index([state]) @@index([state])
@@index([symbol]) @@index([symbol])

Loading…
Cancel
Save