Browse Source

Merge branch 'main' into task/improve-style-of-tabs-in-various-dialogs-on-mobile

pull/7481/head
Thomas Kaul 1 month ago
committed by GitHub
parent
commit
1896b7e952
No known key found for this signature in database GPG Key ID: B5690EEEBB952194
  1. 18
      CHANGELOG.md
  2. 7
      apps/api/src/app/admin/admin.service.ts
  3. 4
      apps/api/src/app/health/health.service.ts
  4. 208
      apps/api/src/app/portfolio/portfolio.service.ts
  5. 4
      apps/api/src/app/subscription/subscription.controller.ts
  6. 6
      apps/api/src/services/benchmark/benchmark.service.ts
  7. 70
      apps/api/src/services/property/property.service.ts
  8. 4
      apps/client/src/locales/messages.de.xlf
  9. 4
      libs/ui/src/lib/activities-table/activities-table.component.html
  10. 9
      libs/ui/src/lib/activities-table/activities-table.component.scss
  11. 25
      libs/ui/src/lib/activities-table/activities-table.component.stories.ts
  12. 7
      libs/ui/src/lib/fear-and-greed-index/fear-and-greed-index.component.html

18
CHANGELOG.md

@ -12,6 +12,24 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- Improved the style of the tabs in the account detail dialog on mobile - Improved the style of the tabs in the account detail dialog on mobile
- Improved the style of the tabs in the holding detail dialog on mobile - Improved the style of the tabs in the holding detail dialog on mobile
- Improved the style of the tabs in the asset profile dialog of the admin control panel on mobile - Improved the style of the tabs in the asset profile dialog of the admin control panel on mobile
- Improved the style of the empty state in the _Fear & Greed Index_ component
- Improved the style of the type filter in the activities table component (experimental)
- Improved the performance of the property service by caching the properties in memory
- Improved the language localization for German (`de`)
### Fixed
- Fixed the static portfolio analysis rule for a portfolio with no holdings: _Asset Class Cluster Risks_ (Equity)
- Fixed the static portfolio analysis rule for a portfolio with no holdings: _Asset Class Cluster Risks_ (Fixed Income)
- Fixed the static portfolio analysis rule for a portfolio with no holdings: _Currency Cluster Risks_ (Investment)
- Fixed the static portfolio analysis rule for a portfolio with no holdings: _Currency Cluster Risks_ (Investment: Base Currency)
- Fixed the static portfolio analysis rule for a portfolio with no holdings: _Economic Market Cluster Risks_ (Developed Markets)
- Fixed the static portfolio analysis rule for a portfolio with no holdings: _Economic Market Cluster Risks_ (Emerging Markets)
- Fixed the static portfolio analysis rule for a portfolio with no holdings: _Regional Market Cluster Risks_ (Asia-Pacific)
- Fixed the static portfolio analysis rule for a portfolio with no holdings: _Regional Market Cluster Risks_ (Emerging Markets)
- Fixed the static portfolio analysis rule for a portfolio with no holdings: _Regional Market Cluster Risks_ (Europe)
- Fixed the static portfolio analysis rule for a portfolio with no holdings: _Regional Market Cluster Risks_ (Japan)
- Fixed the static portfolio analysis rule for a portfolio with no holdings: _Regional Market Cluster Risks_ (North America)
## 3.37.0 - 2026-07-30 ## 3.37.0 - 2026-07-30

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

@ -107,8 +107,11 @@ export class AdminService {
await this.marketDataService.deleteMany({ dataSource, symbol }); await this.marketDataService.deleteMany({ dataSource, symbol });
const currency = getCurrencyFromSymbol(symbol); const currency = getCurrencyFromSymbol(symbol);
const customCurrencies =
await this.propertyService.getByKey<string[]>(PROPERTY_CURRENCIES); const customCurrencies = await this.propertyService.getByKey<string[]>(
PROPERTY_CURRENCIES,
{ skipCache: true }
);
if (customCurrencies.includes(currency)) { if (customCurrencies.includes(currency)) {
const updatedCustomCurrencies = customCurrencies.filter( const updatedCustomCurrencies = customCurrencies.filter(

4
apps/api/src/app/health/health.service.ts

@ -26,7 +26,9 @@ export class HealthService {
public async isDatabaseHealthy() { public async isDatabaseHealthy() {
try { try {
await this.propertyService.getByKey(PROPERTY_CURRENCIES); await this.propertyService.getByKey(PROPERTY_CURRENCIES, {
skipCache: true
});
return true; return true;
} catch { } catch {

208
apps/api/src/app/portfolio/portfolio.service.ts

@ -1126,6 +1126,8 @@ export class PortfolioService {
withSummary: true withSummary: true
}); });
const hasOpenHoldings = Object.keys(holdings).length > 0;
const marketsAdvancedTotalInBaseCurrency = getSum( const marketsAdvancedTotalInBaseCurrency = getSum(
Object.values(marketsAdvanced).map(({ valueInBaseCurrency }) => { Object.values(marketsAdvanced).map(({ valueInBaseCurrency }) => {
return new Big(valueInBaseCurrency); return new Big(valueInBaseCurrency);
@ -1185,26 +1187,25 @@ export class PortfolioService {
id: 'rule.currencyClusterRisk.category', id: 'rule.currencyClusterRisk.category',
languageCode: userSettings.language languageCode: userSettings.language
}), }),
rules: rules: hasOpenHoldings
summary.activityCount > 0 ? await this.rulesService.evaluate(
? await this.rulesService.evaluate( [
[ new CurrencyClusterRiskBaseCurrencyCurrentInvestment(
new CurrencyClusterRiskBaseCurrencyCurrentInvestment( this.exchangeRateDataService,
this.exchangeRateDataService, this.i18nService,
this.i18nService, Object.values(holdings),
Object.values(holdings), userSettings.language
userSettings.language ),
), new CurrencyClusterRiskCurrentInvestment(
new CurrencyClusterRiskCurrentInvestment( this.exchangeRateDataService,
this.exchangeRateDataService, this.i18nService,
this.i18nService, Object.values(holdings),
Object.values(holdings), userSettings.language
userSettings.language )
) ],
], userSettings
userSettings )
) : undefined
: undefined
}, },
{ {
key: 'assetClassClusterRisk', key: 'assetClassClusterRisk',
@ -1212,26 +1213,25 @@ export class PortfolioService {
id: 'rule.assetClassClusterRisk.category', id: 'rule.assetClassClusterRisk.category',
languageCode: userSettings.language languageCode: userSettings.language
}), }),
rules: rules: hasOpenHoldings
summary.activityCount > 0 ? await this.rulesService.evaluate(
? await this.rulesService.evaluate( [
[ new AssetClassClusterRiskEquity(
new AssetClassClusterRiskEquity( this.exchangeRateDataService,
this.exchangeRateDataService, this.i18nService,
this.i18nService, userSettings.language,
userSettings.language, Object.values(holdings)
Object.values(holdings) ),
), new AssetClassClusterRiskFixedIncome(
new AssetClassClusterRiskFixedIncome( this.exchangeRateDataService,
this.exchangeRateDataService, this.i18nService,
this.i18nService, userSettings.language,
userSettings.language, Object.values(holdings)
Object.values(holdings) )
) ],
], userSettings
userSettings )
) : undefined
: undefined
}, },
{ {
key: 'accountClusterRisk', key: 'accountClusterRisk',
@ -1266,28 +1266,27 @@ export class PortfolioService {
id: 'rule.economicMarketClusterRisk.category', id: 'rule.economicMarketClusterRisk.category',
languageCode: userSettings.language languageCode: userSettings.language
}), }),
rules: rules: hasOpenHoldings
summary.activityCount > 0 ? await this.rulesService.evaluate(
? await this.rulesService.evaluate( [
[ new EconomicMarketClusterRiskDevelopedMarkets(
new EconomicMarketClusterRiskDevelopedMarkets( this.exchangeRateDataService,
this.exchangeRateDataService, this.i18nService,
this.i18nService, marketsTotalInBaseCurrency,
marketsTotalInBaseCurrency, markets.developedMarkets.valueInBaseCurrency,
markets.developedMarkets.valueInBaseCurrency, userSettings.language
userSettings.language ),
), new EconomicMarketClusterRiskEmergingMarkets(
new EconomicMarketClusterRiskEmergingMarkets( this.exchangeRateDataService,
this.exchangeRateDataService, this.i18nService,
this.i18nService, marketsTotalInBaseCurrency,
marketsTotalInBaseCurrency, markets.emergingMarkets.valueInBaseCurrency,
markets.emergingMarkets.valueInBaseCurrency, userSettings.language
userSettings.language )
) ],
], userSettings
userSettings )
) : undefined
: undefined
}, },
{ {
key: 'regionalMarketClusterRisk', key: 'regionalMarketClusterRisk',
@ -1295,49 +1294,48 @@ export class PortfolioService {
id: 'rule.regionalMarketClusterRisk.category', id: 'rule.regionalMarketClusterRisk.category',
languageCode: userSettings.language languageCode: userSettings.language
}), }),
rules: rules: hasOpenHoldings
summary.activityCount > 0 ? await this.rulesService.evaluate(
? await this.rulesService.evaluate( [
[ new RegionalMarketClusterRiskAsiaPacific(
new RegionalMarketClusterRiskAsiaPacific( this.exchangeRateDataService,
this.exchangeRateDataService, this.i18nService,
this.i18nService, userSettings.language,
userSettings.language, marketsAdvancedTotalInBaseCurrency,
marketsAdvancedTotalInBaseCurrency, marketsAdvanced.asiaPacific.valueInBaseCurrency
marketsAdvanced.asiaPacific.valueInBaseCurrency ),
), new RegionalMarketClusterRiskEmergingMarkets(
new RegionalMarketClusterRiskEmergingMarkets( this.exchangeRateDataService,
this.exchangeRateDataService, this.i18nService,
this.i18nService, userSettings.language,
userSettings.language, marketsAdvancedTotalInBaseCurrency,
marketsAdvancedTotalInBaseCurrency, marketsAdvanced.emergingMarkets.valueInBaseCurrency
marketsAdvanced.emergingMarkets.valueInBaseCurrency ),
), new RegionalMarketClusterRiskEurope(
new RegionalMarketClusterRiskEurope( this.exchangeRateDataService,
this.exchangeRateDataService, this.i18nService,
this.i18nService, userSettings.language,
userSettings.language, marketsAdvancedTotalInBaseCurrency,
marketsAdvancedTotalInBaseCurrency, marketsAdvanced.europe.valueInBaseCurrency
marketsAdvanced.europe.valueInBaseCurrency ),
), new RegionalMarketClusterRiskJapan(
new RegionalMarketClusterRiskJapan( this.exchangeRateDataService,
this.exchangeRateDataService, this.i18nService,
this.i18nService, userSettings.language,
userSettings.language, marketsAdvancedTotalInBaseCurrency,
marketsAdvancedTotalInBaseCurrency, marketsAdvanced.japan.valueInBaseCurrency
marketsAdvanced.japan.valueInBaseCurrency ),
), new RegionalMarketClusterRiskNorthAmerica(
new RegionalMarketClusterRiskNorthAmerica( this.exchangeRateDataService,
this.exchangeRateDataService, this.i18nService,
this.i18nService, userSettings.language,
userSettings.language, marketsAdvancedTotalInBaseCurrency,
marketsAdvancedTotalInBaseCurrency, marketsAdvanced.northAmerica.valueInBaseCurrency
marketsAdvanced.northAmerica.valueInBaseCurrency )
) ],
], userSettings
userSettings )
) : undefined
: undefined
}, },
{ {
key: 'fees', key: 'fees',

4
apps/api/src/app/subscription/subscription.controller.ts

@ -54,7 +54,9 @@ export class SubscriptionController {
} }
let coupons = let coupons =
(await this.propertyService.getByKey<Coupon[]>(PROPERTY_COUPONS)) ?? []; (await this.propertyService.getByKey<Coupon[]>(PROPERTY_COUPONS, {
skipCache: true
})) ?? [];
const coupon = coupons.find((currentCoupon) => { const coupon = coupons.find((currentCoupon) => {
return currentCoupon.code === couponCode; return currentCoupon.code === couponCode;

6
apps/api/src/services/benchmark/benchmark.service.ts

@ -159,7 +159,8 @@ export class BenchmarkService {
let benchmarks = let benchmarks =
(await this.propertyService.getByKey<BenchmarkProperty[]>( (await this.propertyService.getByKey<BenchmarkProperty[]>(
PROPERTY_BENCHMARKS PROPERTY_BENCHMARKS,
{ skipCache: true }
)) ?? []; )) ?? [];
benchmarks.push({ symbolProfileId: assetProfile.id }); benchmarks.push({ symbolProfileId: assetProfile.id });
@ -196,7 +197,8 @@ export class BenchmarkService {
let benchmarks = let benchmarks =
(await this.propertyService.getByKey<BenchmarkProperty[]>( (await this.propertyService.getByKey<BenchmarkProperty[]>(
PROPERTY_BENCHMARKS PROPERTY_BENCHMARKS,
{ skipCache: true }
)) ?? []; )) ?? [];
benchmarks = benchmarks.filter(({ symbolProfileId }) => { benchmarks = benchmarks.filter(({ symbolProfileId }) => {

70
apps/api/src/services/property/property.service.ts

@ -6,27 +6,39 @@ import {
import { PropertyKey } from '@ghostfolio/common/types'; import { PropertyKey } from '@ghostfolio/common/types';
import { Injectable } from '@nestjs/common'; import { Injectable } from '@nestjs/common';
import { Property } from '@prisma/client';
import { addMilliseconds, isBefore } from 'date-fns';
import ms from 'ms';
import { PropertyValue } from './interfaces/interfaces'; import { PropertyValue } from './interfaces/interfaces';
@Injectable() @Injectable()
export class PropertyService { export class PropertyService {
private static readonly CACHE_TTL = ms('1 minute');
private cachedProperties: Promise<Property[]>;
private cachedPropertiesExpiresAt: Date;
public constructor(private readonly prismaService: PrismaService) {} public constructor(private readonly prismaService: PrismaService) {}
public async delete({ key }: { key: PropertyKey }) { public async delete({ key }: { key: PropertyKey }) {
return this.prismaService.property.delete({ const property = await this.prismaService.property.delete({
where: { key } where: { key }
}); });
this.invalidateCache();
return property;
} }
public async get() { public async get({ skipCache = false } = {}) {
const response: { const response: {
[key: string]: PropertyValue; [key: string]: PropertyValue;
} = { } = {
[PROPERTY_CURRENCIES]: [] [PROPERTY_CURRENCIES]: []
}; };
const properties = await this.prismaService.property.findMany(); const properties = await this.getProperties({ skipCache });
for (const property of properties) { for (const property of properties) {
let value = property.value; let value = property.value;
@ -41,8 +53,11 @@ export class PropertyService {
return response; return response;
} }
public async getByKey<TValue extends PropertyValue>(aKey: PropertyKey) { public async getByKey<TValue extends PropertyValue>(
const properties = await this.get(); aKey: PropertyKey,
{ skipCache = false } = {}
) {
const properties = await this.get({ skipCache });
return properties[aKey] as TValue; return properties[aKey] as TValue;
} }
@ -53,10 +68,53 @@ export class PropertyService {
} }
public async put({ key, value }: { key: PropertyKey; value: string }) { public async put({ key, value }: { key: PropertyKey; value: string }) {
return this.prismaService.property.upsert({ const property = await this.prismaService.property.upsert({
create: { key, value }, create: { key, value },
update: { value }, update: { value },
where: { key } where: { key }
}); });
this.invalidateCache();
return property;
}
/**
* Returns the properties from the in-memory cache, falling back to the
* database. Callers which write back a modified property must set
* skipCache to avoid basing the write on a stale read.
*/
private async getProperties({ skipCache = false } = {}) {
if (skipCache) {
return this.prismaService.property.findMany();
}
if (
this.cachedProperties &&
isBefore(new Date(), this.cachedPropertiesExpiresAt)
) {
return this.cachedProperties;
}
const properties = this.prismaService.property.findMany().catch((error) => {
if (this.cachedProperties === properties) {
this.invalidateCache();
}
throw error;
});
this.cachedProperties = properties;
this.cachedPropertiesExpiresAt = addMilliseconds(
new Date(),
PropertyService.CACHE_TTL
);
return this.cachedProperties;
}
private invalidateCache() {
this.cachedProperties = undefined;
this.cachedPropertiesExpiresAt = undefined;
} }
} }

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

@ -351,7 +351,7 @@
</trans-unit> </trans-unit>
<trans-unit id="4467323362722952678" datatype="html"> <trans-unit id="4467323362722952678" datatype="html">
<source>Unknown</source> <source>Unknown</source>
<target state="new">Unknown</target> <target state="translated">Unbekannt</target>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">libs/ui/src/lib/i18n.ts</context> <context context-type="sourcefile">libs/ui/src/lib/i18n.ts</context>
<context context-type="linenumber">88</context> <context context-type="linenumber">88</context>
@ -1767,7 +1767,7 @@
</trans-unit> </trans-unit>
<trans-unit id="659291745893337969" datatype="html"> <trans-unit id="659291745893337969" datatype="html">
<source>Invalid API key</source> <source>Invalid API key</source>
<target state="new">Invalid API key</target> <target state="translated">Ungültiger API-Schlüssel</target>
<context-group purpose="location"> <context-group purpose="location">
<context context-type="sourcefile">apps/client/src/app/components/admin-settings/admin-settings.component.html</context> <context context-type="sourcefile">apps/client/src/app/components/admin-settings/admin-settings.component.html</context>
<context context-type="linenumber">112</context> <context context-type="linenumber">112</context>

4
libs/ui/src/lib/activities-table/activities-table.component.html

@ -1,4 +1,6 @@
<div class="d-flex justify-content-end justify-content-lg-between"> <div
class="align-items-center d-flex justify-content-end justify-content-lg-between"
>
<div class="d-none d-lg-flex"> <div class="d-none d-lg-flex">
@if (hasPermissionToFilterByType) { @if (hasPermissionToFilterByType) {
<mat-form-field appearance="outline" class="without-hint"> <mat-form-field appearance="outline" class="without-hint">

9
libs/ui/src/lib/activities-table/activities-table.component.scss

@ -1,3 +1,12 @@
@use '@angular/material' as mat;
:host { :host {
display: block; display: block;
@include mat.form-field-overrides(
(
container-height: 2rem,
container-vertical-padding: 0.33rem
)
);
} }

25
libs/ui/src/lib/activities-table/activities-table.component.stories.ts

@ -494,3 +494,28 @@ export const Actions: Story = {
totalItems: activities.length totalItems: activities.length
} }
}; };
export const Toolbar: Story = {
args: {
dataSource,
baseCurrency: 'USD',
deviceType: 'desktop',
hasActivities: true,
hasPermissionToCreateActivity: true,
hasPermissionToDeleteActivity: true,
hasPermissionToExportActivities: true,
hasPermissionToFilterByType: true,
hasPermissionToOpenDetails: false,
locale: 'en-US',
pageIndex: 0,
pageSize: 10,
showAccountColumn: true,
showActions: false,
showCheckbox: false,
showNameColumn: true,
sortColumn: 'date',
sortDirection: 'desc',
sortDisabled: false,
totalItems: activities.length
}
};

7
libs/ui/src/lib/fear-and-greed-index/fear-and-greed-index.component.html

@ -12,10 +12,11 @@
<div> <div>
<div class="h4 mb-0"> <div class="h4 mb-0">
<span class="mr-2">{{ fearAndGreedIndexText() }}</span> <span class="mr-2">{{ fearAndGreedIndexText() }}</span>
@let value = fearAndGreedIndex() | number: '1.0-0';
<small class="text-muted" <small class="text-muted"
><strong>{{ ><span [class.font-weight-bold]="value">{{
(fearAndGreedIndex() | number: '1.0-0') ?? placeholder value ?? placeholder
}}</strong }}</span
>/100</small >/100</small
> >
</div> </div>

Loading…
Cancel
Save