Browse Source

Merge remote-tracking branch 'origin/main' into task/create-update-activity-dialog-type-safety

pull/7294/head
KenTandrian 4 days ago
parent
commit
cf1dd9ad91
  1. 16
      CHANGELOG.md
  2. 6
      apps/api/src/app/endpoints/market-data/market-data.controller.ts
  3. 8
      apps/api/src/app/endpoints/market-data/market-data.module.ts
  4. 13
      apps/api/src/app/info/info.service.ts
  5. 3
      apps/api/src/services/configuration/configuration.service.ts
  6. 6
      apps/api/src/services/data-provider/data-provider.service.ts
  7. 32
      apps/api/src/services/data-provider/rapid-api/rapid-api.service.ts
  8. 1
      apps/api/src/services/interfaces/environment.interface.ts
  9. 8
      apps/api/src/services/twitter-bot/twitter-bot.module.ts
  10. 10
      apps/api/src/services/twitter-bot/twitter-bot.service.ts
  11. 32
      apps/client/src/app/components/portfolio-summary/portfolio-summary.component.ts
  12. 46
      apps/client/src/app/components/user-detail-dialog/user-detail-dialog.component.ts
  13. 12
      apps/client/src/app/pages/about/about-page.component.ts
  14. 11
      apps/client/src/app/pages/admin/admin-page.component.ts
  15. 3
      apps/client/src/app/pages/faq/faq-page.component.ts
  16. 6
      apps/client/src/app/pages/home/home-page.component.ts
  17. 3
      apps/client/src/app/pages/resources/resources-page.component.ts
  18. 12
      apps/client/src/app/pages/user-account/user-account-page.component.ts
  19. 12
      apps/client/src/app/pages/zen/zen-page.component.ts
  20. 2
      apps/client/src/locales/messages.de.xlf
  21. 1
      libs/common/src/lib/config.ts
  22. 4
      package-lock.json
  23. 2
      package.json

16
CHANGELOG.md

@ -5,14 +5,28 @@ All notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
## Unreleased ### Unreleased
### Added
- Exposed the `DATA_SOURCE_FEAR_AND_GREED_INDEX_STOCKS` environment variable to set the data source of the _Fear & Greed Index_ (market mood)
## 3.23.0 - 2026-07-10
### Changed ### Changed
- Migrated the deprecated `@nx/webpack:webpack` executor to `@nx/webpack/plugin` - Migrated the deprecated `@nx/webpack:webpack` executor to `@nx/webpack/plugin`
- Set the change detection strategy to `OnPush` in the about page
- Set the change detection strategy to `OnPush` in the admin control panel
- Set the change detection strategy to `OnPush` in the blog page components - Set the change detection strategy to `OnPush` in the blog page components
- Set the change detection strategy to `OnPush` in the Frequently Asked Questions (FAQ) page
- Set the change detection strategy to `OnPush` in the home page
- Set the change detection strategy to `OnPush` in the markets overview - Set the change detection strategy to `OnPush` in the markets overview
- Set the change detection strategy to `OnPush` in the resources page
- Set the change detection strategy to `OnPush` in the user account page
- Set the change detection strategy to `OnPush` in the _Zen Mode_
- Improved the language localization for Chinese (`zh`) - Improved the language localization for Chinese (`zh`)
- Improved the language localization for German (`de`)
## 3.22.0 - 2026-07-08 ## 3.22.0 - 2026-07-08

6
apps/api/src/app/endpoints/market-data/market-data.controller.ts

@ -1,11 +1,11 @@
import { SymbolService } from '@ghostfolio/api/app/symbol/symbol.service'; import { SymbolService } from '@ghostfolio/api/app/symbol/symbol.service';
import { HasPermission } from '@ghostfolio/api/decorators/has-permission.decorator'; import { HasPermission } from '@ghostfolio/api/decorators/has-permission.decorator';
import { HasPermissionGuard } from '@ghostfolio/api/guards/has-permission.guard'; import { HasPermissionGuard } from '@ghostfolio/api/guards/has-permission.guard';
import { DataProviderService } from '@ghostfolio/api/services/data-provider/data-provider.service';
import { MarketDataService } from '@ghostfolio/api/services/market-data/market-data.service'; import { MarketDataService } from '@ghostfolio/api/services/market-data/market-data.service';
import { SymbolProfileService } from '@ghostfolio/api/services/symbol-profile/symbol-profile.service'; import { SymbolProfileService } from '@ghostfolio/api/services/symbol-profile/symbol-profile.service';
import { import {
ghostfolioFearAndGreedIndexDataSourceCryptocurrencies, ghostfolioFearAndGreedIndexDataSourceCryptocurrencies,
ghostfolioFearAndGreedIndexDataSourceStocks,
ghostfolioFearAndGreedIndexSymbolCryptocurrencies, ghostfolioFearAndGreedIndexSymbolCryptocurrencies,
ghostfolioFearAndGreedIndexSymbolStocks ghostfolioFearAndGreedIndexSymbolStocks
} from '@ghostfolio/common/config'; } from '@ghostfolio/common/config';
@ -36,6 +36,7 @@ import { getReasonPhrase, StatusCodes } from 'http-status-codes';
@Controller('market-data') @Controller('market-data')
export class MarketDataController { export class MarketDataController {
public constructor( public constructor(
private readonly dataProviderService: DataProviderService,
private readonly marketDataService: MarketDataService, private readonly marketDataService: MarketDataService,
@Inject(REQUEST) private readonly request: RequestWithUser, @Inject(REQUEST) private readonly request: RequestWithUser,
private readonly symbolProfileService: SymbolProfileService, private readonly symbolProfileService: SymbolProfileService,
@ -64,7 +65,8 @@ export class MarketDataController {
this.symbolService.get({ this.symbolService.get({
includeHistoricalData, includeHistoricalData,
dataGatheringItem: { dataGatheringItem: {
dataSource: ghostfolioFearAndGreedIndexDataSourceStocks, dataSource:
this.dataProviderService.getDataSourceForFearAndGreedIndexStocks(),
symbol: ghostfolioFearAndGreedIndexSymbolStocks symbol: ghostfolioFearAndGreedIndexSymbolStocks
}, },
useIntradayData: true useIntradayData: true

8
apps/api/src/app/endpoints/market-data/market-data.module.ts

@ -1,4 +1,5 @@
import { SymbolModule } from '@ghostfolio/api/app/symbol/symbol.module'; import { SymbolModule } from '@ghostfolio/api/app/symbol/symbol.module';
import { DataProviderModule } from '@ghostfolio/api/services/data-provider/data-provider.module';
import { MarketDataModule as MarketDataServiceModule } from '@ghostfolio/api/services/market-data/market-data.module'; import { MarketDataModule as MarketDataServiceModule } from '@ghostfolio/api/services/market-data/market-data.module';
import { SymbolProfileModule } from '@ghostfolio/api/services/symbol-profile/symbol-profile.module'; import { SymbolProfileModule } from '@ghostfolio/api/services/symbol-profile/symbol-profile.module';
@ -8,6 +9,11 @@ import { MarketDataController } from './market-data.controller';
@Module({ @Module({
controllers: [MarketDataController], controllers: [MarketDataController],
imports: [MarketDataServiceModule, SymbolModule, SymbolProfileModule] imports: [
DataProviderModule,
MarketDataServiceModule,
SymbolModule,
SymbolProfileModule
]
}) })
export class MarketDataModule {} export class MarketDataModule {}

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

@ -4,6 +4,7 @@ import { UserService } from '@ghostfolio/api/app/user/user.service';
import { encodeDataSource } from '@ghostfolio/api/helper/data-source.helper'; import { encodeDataSource } from '@ghostfolio/api/helper/data-source.helper';
import { BenchmarkService } from '@ghostfolio/api/services/benchmark/benchmark.service'; import { BenchmarkService } from '@ghostfolio/api/services/benchmark/benchmark.service';
import { ConfigurationService } from '@ghostfolio/api/services/configuration/configuration.service'; import { ConfigurationService } from '@ghostfolio/api/services/configuration/configuration.service';
import { DataProviderService } from '@ghostfolio/api/services/data-provider/data-provider.service';
import { ExchangeRateDataService } from '@ghostfolio/api/services/exchange-rate-data/exchange-rate-data.service'; import { ExchangeRateDataService } from '@ghostfolio/api/services/exchange-rate-data/exchange-rate-data.service';
import { PropertyService } from '@ghostfolio/api/services/property/property.service'; import { PropertyService } from '@ghostfolio/api/services/property/property.service';
import { import {
@ -15,8 +16,7 @@ import {
PROPERTY_GITHUB_STARGAZERS, PROPERTY_GITHUB_STARGAZERS,
PROPERTY_IS_READ_ONLY_MODE, PROPERTY_IS_READ_ONLY_MODE,
PROPERTY_SLACK_COMMUNITY_USERS, PROPERTY_SLACK_COMMUNITY_USERS,
PROPERTY_UPTIME, PROPERTY_UPTIME
ghostfolioFearAndGreedIndexDataSourceStocks
} from '@ghostfolio/common/config'; } from '@ghostfolio/common/config';
import { InfoItem, Statistics } from '@ghostfolio/common/interfaces'; import { InfoItem, Statistics } from '@ghostfolio/common/interfaces';
import { permissions } from '@ghostfolio/common/permissions'; import { permissions } from '@ghostfolio/common/permissions';
@ -33,6 +33,7 @@ export class InfoService {
public constructor( public constructor(
private readonly benchmarkService: BenchmarkService, private readonly benchmarkService: BenchmarkService,
private readonly configurationService: ConfigurationService, private readonly configurationService: ConfigurationService,
private readonly dataProviderService: DataProviderService,
private readonly exchangeRateDataService: ExchangeRateDataService, private readonly exchangeRateDataService: ExchangeRateDataService,
private readonly jwtService: JwtService, private readonly jwtService: JwtService,
private readonly propertyService: PropertyService, private readonly propertyService: PropertyService,
@ -60,13 +61,15 @@ export class InfoService {
} }
if (this.configurationService.get('ENABLE_FEATURE_FEAR_AND_GREED_INDEX')) { if (this.configurationService.get('ENABLE_FEATURE_FEAR_AND_GREED_INDEX')) {
const fearAndGreedIndexDataSource =
this.dataProviderService.getDataSourceForFearAndGreedIndexStocks();
if (this.configurationService.get('ENABLE_FEATURE_SUBSCRIPTION')) { if (this.configurationService.get('ENABLE_FEATURE_SUBSCRIPTION')) {
info.fearAndGreedDataSource = encodeDataSource( info.fearAndGreedDataSource = encodeDataSource(
ghostfolioFearAndGreedIndexDataSourceStocks fearAndGreedIndexDataSource
); );
} else { } else {
info.fearAndGreedDataSource = info.fearAndGreedDataSource = fearAndGreedIndexDataSource;
ghostfolioFearAndGreedIndexDataSourceStocks;
} }
globalPermissions.push(permissions.enableFearAndGreedIndex); globalPermissions.push(permissions.enableFearAndGreedIndex);

3
apps/api/src/services/configuration/configuration.service.ts

@ -34,6 +34,9 @@ export class ConfigurationService {
CACHE_QUOTES_TTL: num({ default: ms('1 minute') }), CACHE_QUOTES_TTL: num({ default: ms('1 minute') }),
CACHE_TTL: num({ default: CACHE_TTL_NO_CACHE }), CACHE_TTL: num({ default: CACHE_TTL_NO_CACHE }),
DATA_SOURCE_EXCHANGE_RATES: str({ default: DataSource.YAHOO }), DATA_SOURCE_EXCHANGE_RATES: str({ default: DataSource.YAHOO }),
DATA_SOURCE_FEAR_AND_GREED_INDEX_STOCKS: str({
default: DataSource.RAPID_API
}),
DATA_SOURCE_IMPORT: str({ default: DataSource.YAHOO }), DATA_SOURCE_IMPORT: str({ default: DataSource.YAHOO }),
DATA_SOURCES: json({ DATA_SOURCES: json({
default: [DataSource.COINGECKO, DataSource.MANUAL, DataSource.YAHOO] default: [DataSource.COINGECKO, DataSource.MANUAL, DataSource.YAHOO]

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

@ -177,6 +177,12 @@ export class DataProviderService implements OnModuleInit {
]; ];
} }
public getDataSourceForFearAndGreedIndexStocks(): DataSource {
return DataSource[
this.configurationService.get('DATA_SOURCE_FEAR_AND_GREED_INDEX_STOCKS')
];
}
public getDataSourceForImport(): DataSource { public getDataSourceForImport(): DataSource {
return DataSource[this.configurationService.get('DATA_SOURCE_IMPORT')]; return DataSource[this.configurationService.get('DATA_SOURCE_IMPORT')];
} }

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

@ -64,13 +64,15 @@ export class RapidApiService implements DataProviderInterface {
if (symbol === ghostfolioFearAndGreedIndexSymbolStocks) { if (symbol === ghostfolioFearAndGreedIndexSymbolStocks) {
const fgi = await this.getFearAndGreedIndex(); const fgi = await this.getFearAndGreedIndex();
return { if (fgi) {
[symbol]: { return {
[format(getYesterday(), DATE_FORMAT)]: { [symbol]: {
marketPrice: fgi.previousClose.value [format(getYesterday(), DATE_FORMAT)]: {
marketPrice: fgi.previousClose.value
}
} }
} };
}; }
} }
} catch (error) { } catch (error) {
throw new Error( throw new Error(
@ -101,14 +103,16 @@ export class RapidApiService implements DataProviderInterface {
if (symbol === ghostfolioFearAndGreedIndexSymbolStocks) { if (symbol === ghostfolioFearAndGreedIndexSymbolStocks) {
const fgi = await this.getFearAndGreedIndex(); const fgi = await this.getFearAndGreedIndex();
return { if (fgi) {
[symbol]: { return {
currency: undefined, [symbol]: {
dataSource: this.getName(), currency: undefined,
marketPrice: fgi.now.value, dataSource: this.getName(),
marketState: 'open' marketPrice: fgi.now.value,
} marketState: 'open'
}; }
};
}
} }
} catch (error) { } catch (error) {
this.logger.error(error); this.logger.error(error);

1
apps/api/src/services/interfaces/environment.interface.ts

@ -13,6 +13,7 @@ export interface Environment extends CleanedEnvAccessors {
CACHE_QUOTES_TTL: number; CACHE_QUOTES_TTL: number;
CACHE_TTL: number; CACHE_TTL: number;
DATA_SOURCE_EXCHANGE_RATES: string; DATA_SOURCE_EXCHANGE_RATES: string;
DATA_SOURCE_FEAR_AND_GREED_INDEX_STOCKS: string;
DATA_SOURCE_IMPORT: string; DATA_SOURCE_IMPORT: string;
DATA_SOURCES: string[]; DATA_SOURCES: string[];
DATA_SOURCES_GHOSTFOLIO_DATA_PROVIDER: string[]; DATA_SOURCES_GHOSTFOLIO_DATA_PROVIDER: string[];

8
apps/api/src/services/twitter-bot/twitter-bot.module.ts

@ -1,13 +1,19 @@
import { SymbolModule } from '@ghostfolio/api/app/symbol/symbol.module'; import { SymbolModule } from '@ghostfolio/api/app/symbol/symbol.module';
import { BenchmarkModule } from '@ghostfolio/api/services/benchmark/benchmark.module'; import { BenchmarkModule } from '@ghostfolio/api/services/benchmark/benchmark.module';
import { ConfigurationModule } from '@ghostfolio/api/services/configuration/configuration.module'; import { ConfigurationModule } from '@ghostfolio/api/services/configuration/configuration.module';
import { DataProviderModule } from '@ghostfolio/api/services/data-provider/data-provider.module';
import { TwitterBotService } from '@ghostfolio/api/services/twitter-bot/twitter-bot.service'; import { TwitterBotService } from '@ghostfolio/api/services/twitter-bot/twitter-bot.service';
import { Module } from '@nestjs/common'; import { Module } from '@nestjs/common';
@Module({ @Module({
exports: [TwitterBotService], exports: [TwitterBotService],
imports: [BenchmarkModule, ConfigurationModule, SymbolModule], imports: [
BenchmarkModule,
ConfigurationModule,
DataProviderModule,
SymbolModule
],
providers: [TwitterBotService] providers: [TwitterBotService]
}) })
export class TwitterBotModule {} export class TwitterBotModule {}

10
apps/api/src/services/twitter-bot/twitter-bot.service.ts

@ -1,10 +1,8 @@
import { SymbolService } from '@ghostfolio/api/app/symbol/symbol.service'; import { SymbolService } from '@ghostfolio/api/app/symbol/symbol.service';
import { BenchmarkService } from '@ghostfolio/api/services/benchmark/benchmark.service'; import { BenchmarkService } from '@ghostfolio/api/services/benchmark/benchmark.service';
import { ConfigurationService } from '@ghostfolio/api/services/configuration/configuration.service'; import { ConfigurationService } from '@ghostfolio/api/services/configuration/configuration.service';
import { import { DataProviderService } from '@ghostfolio/api/services/data-provider/data-provider.service';
ghostfolioFearAndGreedIndexDataSourceStocks, import { ghostfolioFearAndGreedIndexSymbolStocks } from '@ghostfolio/common/config';
ghostfolioFearAndGreedIndexSymbolStocks
} from '@ghostfolio/common/config';
import { import {
resolveFearAndGreedIndex, resolveFearAndGreedIndex,
resolveMarketCondition resolveMarketCondition
@ -24,6 +22,7 @@ export class TwitterBotService implements OnModuleInit {
public constructor( public constructor(
private readonly benchmarkService: BenchmarkService, private readonly benchmarkService: BenchmarkService,
private readonly configurationService: ConfigurationService, private readonly configurationService: ConfigurationService,
private readonly dataProviderService: DataProviderService,
private readonly symbolService: SymbolService private readonly symbolService: SymbolService
) {} ) {}
@ -49,7 +48,8 @@ export class TwitterBotService implements OnModuleInit {
try { try {
const symbolItem = await this.symbolService.get({ const symbolItem = await this.symbolService.get({
dataGatheringItem: { dataGatheringItem: {
dataSource: ghostfolioFearAndGreedIndexDataSourceStocks, dataSource:
this.dataProviderService.getDataSourceForFearAndGreedIndexStocks(),
symbol: ghostfolioFearAndGreedIndexSymbolStocks symbol: ghostfolioFearAndGreedIndexSymbolStocks
} }
}); });

32
apps/client/src/app/components/portfolio-summary/portfolio-summary.component.ts

@ -8,10 +8,10 @@ import { GfValueComponent } from '@ghostfolio/ui/value';
import { import {
ChangeDetectionStrategy, ChangeDetectionStrategy,
Component, Component,
EventEmitter, inject,
Input, Input,
OnChanges, OnChanges,
Output output
} from '@angular/core'; } from '@angular/core';
import { MatTooltipModule } from '@angular/material/tooltip'; import { MatTooltipModule } from '@angular/material/tooltip';
import { IonIcon } from '@ionic/angular/standalone'; import { IonIcon } from '@ionic/angular/standalone';
@ -40,44 +40,46 @@ export class GfPortfolioSummaryComponent implements OnChanges {
@Input() summary: PortfolioSummary; @Input() summary: PortfolioSummary;
@Input() user: User; @Input() user: User;
@Output() emergencyFundChanged = new EventEmitter<number>(); public emergencyFundChanged = output<number>();
public buyAndSellActivitiesTooltip = translate( protected readonly buyAndSellActivitiesTooltip = translate(
'BUY_AND_SELL_ACTIVITIES_TOOLTIP' 'BUY_AND_SELL_ACTIVITIES_TOOLTIP'
); );
public precision = 2; protected precision = 2;
public timeInMarket: string; protected timeInMarket: string | undefined;
public get buyingPowerPercentage() { private readonly notificationService = inject(NotificationService);
public constructor() {
addIcons({ ellipsisHorizontalCircleOutline, informationCircleOutline });
}
protected get buyingPowerPercentage() {
return this.summary?.totalValueInBaseCurrency return this.summary?.totalValueInBaseCurrency
? this.summary.cash / this.summary.totalValueInBaseCurrency ? this.summary.cash / this.summary.totalValueInBaseCurrency
: 0; : 0;
} }
public get emergencyFundPercentage() { protected get emergencyFundPercentage() {
return this.summary?.totalValueInBaseCurrency return this.summary?.totalValueInBaseCurrency
? (this.summary.emergencyFund?.total || 0) / ? (this.summary.emergencyFund?.total || 0) /
this.summary.totalValueInBaseCurrency this.summary.totalValueInBaseCurrency
: 0; : 0;
} }
public get excludedFromAnalysisPercentage() { protected get excludedFromAnalysisPercentage() {
return this.summary?.totalValueInBaseCurrency return this.summary?.totalValueInBaseCurrency
? this.summary.excludedAccountsAndActivities / ? this.summary.excludedAccountsAndActivities /
this.summary.totalValueInBaseCurrency this.summary.totalValueInBaseCurrency
: 0; : 0;
} }
public constructor(private notificationService: NotificationService) {
addIcons({ ellipsisHorizontalCircleOutline, informationCircleOutline });
}
public ngOnChanges() { public ngOnChanges() {
if (this.summary) { if (this.summary) {
if ( if (
this.deviceType === 'mobile' && this.deviceType === 'mobile' &&
this.summary.totalValueInBaseCurrency >= (this.summary.totalValueInBaseCurrency ?? 0) >=
NUMERICAL_PRECISION_THRESHOLD_6_FIGURES NUMERICAL_PRECISION_THRESHOLD_6_FIGURES
) { ) {
this.precision = 0; this.precision = 0;
@ -98,7 +100,7 @@ export class GfPortfolioSummaryComponent implements OnChanges {
} }
} }
public onEditEmergencyFund() { protected onEditEmergencyFund() {
this.notificationService.prompt({ this.notificationService.prompt({
confirmFn: (value) => { confirmFn: (value) => {
const emergencyFund = parseFloat(value.trim()) || 0; const emergencyFund = parseFloat(value.trim()) || 0;

46
apps/client/src/app/components/user-detail-dialog/user-detail-dialog.component.ts

@ -9,7 +9,7 @@ import {
Component, Component,
CUSTOM_ELEMENTS_SCHEMA, CUSTOM_ELEMENTS_SCHEMA,
DestroyRef, DestroyRef,
Inject, inject,
OnInit OnInit
} from '@angular/core'; } from '@angular/core';
import { takeUntilDestroyed } from '@angular/core/rxjs-interop'; import { takeUntilDestroyed } from '@angular/core/rxjs-interop';
@ -49,28 +49,30 @@ import {
templateUrl: './user-detail-dialog.html' templateUrl: './user-detail-dialog.html'
}) })
export class GfUserDetailDialogComponent implements OnInit { export class GfUserDetailDialogComponent implements OnInit {
public baseCurrency: string; protected baseCurrency: string;
public readonly getCountryName = getCountryName; protected readonly getCountryName = getCountryName;
public subscriptionsDataSource = new MatTableDataSource<Subscription>(); protected readonly subscriptionsDataSource =
public subscriptionsDisplayedColumns = [ new MatTableDataSource<Subscription>();
protected readonly subscriptionsDisplayedColumns = [
'createdAt', 'createdAt',
'type', 'type',
'price', 'price',
'expiresAt' 'expiresAt'
]; ];
public user: AdminUserResponse; protected user: AdminUserResponse;
public constructor( protected readonly data = inject<UserDetailDialogParams>(MAT_DIALOG_DATA);
private adminService: AdminService,
private changeDetectorRef: ChangeDetectorRef, private readonly adminService = inject(AdminService);
@Inject(MAT_DIALOG_DATA) public data: UserDetailDialogParams, private readonly changeDetectorRef = inject(ChangeDetectorRef);
private dataService: DataService, private readonly dataService = inject(DataService);
private destroyRef: DestroyRef, private readonly destroyRef = inject(DestroyRef);
public dialogRef: MatDialogRef< private readonly dialogRef =
GfUserDetailDialogComponent, inject<MatDialogRef<GfUserDetailDialogComponent, UserDetailDialogResult>>(
UserDetailDialogResult MatDialogRef
> );
) {
public constructor() {
this.baseCurrency = this.dataService.fetchInfo().baseCurrency; this.baseCurrency = this.dataService.fetchInfo().baseCurrency;
addIcons({ addIcons({
@ -98,14 +100,14 @@ export class GfUserDetailDialogComponent implements OnInit {
}); });
} }
public deleteUser() { protected deleteUser() {
this.dialogRef.close({ this.dialogRef.close({
action: 'delete', action: 'delete',
userId: this.data.userId userId: this.data.userId
}); });
} }
public getSum() { protected getSum() {
return getSum( return getSum(
this.subscriptionsDataSource.data this.subscriptionsDataSource.data
.filter(({ price }) => { .filter(({ price }) => {
@ -117,7 +119,7 @@ export class GfUserDetailDialogComponent implements OnInit {
).toNumber(); ).toNumber();
} }
public getType({ createdAt, expiresAt, price }: Subscription) { protected getType({ createdAt, expiresAt, price }: Subscription) {
if (price) { if (price) {
return $localize`Paid`; return $localize`Paid`;
} }
@ -127,7 +129,7 @@ export class GfUserDetailDialogComponent implements OnInit {
: $localize`Coupon`; : $localize`Coupon`;
} }
public onClose() { protected onClose() {
this.dialogRef.close(); this.dialogRef.close();
} }
} }

12
apps/client/src/app/pages/about/about-page.component.ts

@ -8,7 +8,12 @@ import {
} from '@ghostfolio/ui/page-tabs'; } from '@ghostfolio/ui/page-tabs';
import { DataService } from '@ghostfolio/ui/services'; import { DataService } from '@ghostfolio/ui/services';
import { ChangeDetectorRef, Component, DestroyRef } from '@angular/core'; import {
ChangeDetectionStrategy,
ChangeDetectorRef,
Component,
DestroyRef
} from '@angular/core';
import { takeUntilDestroyed } from '@angular/core/rxjs-interop'; import { takeUntilDestroyed } from '@angular/core/rxjs-interop';
import { addIcons } from 'ionicons'; import { addIcons } from 'ionicons';
import { import {
@ -21,6 +26,7 @@ import {
} from 'ionicons/icons'; } from 'ionicons/icons';
@Component({ @Component({
changeDetection: ChangeDetectionStrategy.OnPush,
host: { class: 'page' }, host: { class: 'page' },
imports: [GfPageTabsComponent], imports: [GfPageTabsComponent],
selector: 'gf-about-page', selector: 'gf-about-page',
@ -83,8 +89,6 @@ export class AboutPageComponent {
}); });
this.user = state.user; this.user = state.user;
this.changeDetectorRef.markForCheck();
} }
this.tabs.push({ this.tabs.push({
@ -92,6 +96,8 @@ export class AboutPageComponent {
label: publicRoutes.about.subRoutes.ossFriends.title, label: publicRoutes.about.subRoutes.ossFriends.title,
routerLink: publicRoutes.about.subRoutes.ossFriends.routerLink routerLink: publicRoutes.about.subRoutes.ossFriends.routerLink
}); });
this.changeDetectorRef.markForCheck();
}); });
addIcons({ addIcons({

11
apps/client/src/app/pages/admin/admin-page.component.ts

@ -12,7 +12,12 @@ import {
TabConfiguration TabConfiguration
} from '@ghostfolio/ui/page-tabs'; } from '@ghostfolio/ui/page-tabs';
import { Component, inject } from '@angular/core'; import {
ChangeDetectionStrategy,
ChangeDetectorRef,
Component,
inject
} from '@angular/core';
import { takeUntilDestroyed } from '@angular/core/rxjs-interop'; import { takeUntilDestroyed } from '@angular/core/rxjs-interop';
import { addIcons } from 'ionicons'; import { addIcons } from 'ionicons';
import { import {
@ -24,6 +29,7 @@ import {
} from 'ionicons/icons'; } from 'ionicons/icons';
@Component({ @Component({
changeDetection: ChangeDetectionStrategy.OnPush,
host: { class: 'page' }, host: { class: 'page' },
imports: [GfPageTabsComponent], imports: [GfPageTabsComponent],
selector: 'gf-admin-page', selector: 'gf-admin-page',
@ -35,6 +41,7 @@ export class AdminPageComponent {
private user: User; private user: User;
private readonly changeDetectorRef = inject(ChangeDetectorRef);
private readonly tokenStorageService = inject(TokenStorageService); private readonly tokenStorageService = inject(TokenStorageService);
private readonly userService = inject(UserService); private readonly userService = inject(UserService);
@ -45,6 +52,8 @@ export class AdminPageComponent {
this.user = state?.user; this.user = state?.user;
this.initializeTabs(); this.initializeTabs();
this.changeDetectorRef.markForCheck();
}); });
addIcons({ addIcons({

3
apps/client/src/app/pages/faq/faq-page.component.ts

@ -6,11 +6,12 @@ import {
} from '@ghostfolio/ui/page-tabs'; } from '@ghostfolio/ui/page-tabs';
import { DataService } from '@ghostfolio/ui/services'; import { DataService } from '@ghostfolio/ui/services';
import { Component } from '@angular/core'; import { ChangeDetectionStrategy, Component } from '@angular/core';
import { addIcons } from 'ionicons'; import { addIcons } from 'ionicons';
import { cloudyOutline, readerOutline, serverOutline } from 'ionicons/icons'; import { cloudyOutline, readerOutline, serverOutline } from 'ionicons/icons';
@Component({ @Component({
changeDetection: ChangeDetectionStrategy.OnPush,
host: { class: 'page' }, host: { class: 'page' },
imports: [GfPageTabsComponent], imports: [GfPageTabsComponent],
selector: 'gf-faq-page', selector: 'gf-faq-page',

6
apps/client/src/app/pages/home/home-page.component.ts

@ -9,6 +9,7 @@ import {
} from '@ghostfolio/ui/page-tabs'; } from '@ghostfolio/ui/page-tabs';
import { import {
ChangeDetectionStrategy,
ChangeDetectorRef, ChangeDetectorRef,
Component, Component,
DestroyRef, DestroyRef,
@ -25,6 +26,7 @@ import {
} from 'ionicons/icons'; } from 'ionicons/icons';
@Component({ @Component({
changeDetection: ChangeDetectionStrategy.OnPush,
host: { class: 'page' }, host: { class: 'page' },
imports: [GfPageTabsComponent], imports: [GfPageTabsComponent],
selector: 'gf-home-page', selector: 'gf-home-page',
@ -85,9 +87,9 @@ export class GfHomePageComponent implements OnInit {
: internalRoutes.home.subRoutes.markets.routerLink : internalRoutes.home.subRoutes.markets.routerLink
} }
]; ];
this.changeDetectorRef.markForCheck();
} }
this.changeDetectorRef.markForCheck();
}); });
addIcons({ addIcons({

3
apps/client/src/app/pages/resources/resources-page.component.ts

@ -4,7 +4,7 @@ import {
TabConfiguration TabConfiguration
} from '@ghostfolio/ui/page-tabs'; } from '@ghostfolio/ui/page-tabs';
import { Component } from '@angular/core'; import { ChangeDetectionStrategy, Component } from '@angular/core';
import { addIcons } from 'ionicons'; import { addIcons } from 'ionicons';
import { import {
bookOutline, bookOutline,
@ -14,6 +14,7 @@ import {
} from 'ionicons/icons'; } from 'ionicons/icons';
@Component({ @Component({
changeDetection: ChangeDetectionStrategy.OnPush,
host: { class: 'page' }, host: { class: 'page' },
imports: [GfPageTabsComponent], imports: [GfPageTabsComponent],
selector: 'gf-resources-page', selector: 'gf-resources-page',

12
apps/client/src/app/pages/user-account/user-account-page.component.ts

@ -6,12 +6,18 @@ import {
TabConfiguration TabConfiguration
} from '@ghostfolio/ui/page-tabs'; } from '@ghostfolio/ui/page-tabs';
import { ChangeDetectorRef, Component, DestroyRef } from '@angular/core'; import {
ChangeDetectionStrategy,
ChangeDetectorRef,
Component,
DestroyRef
} from '@angular/core';
import { takeUntilDestroyed } from '@angular/core/rxjs-interop'; import { takeUntilDestroyed } from '@angular/core/rxjs-interop';
import { addIcons } from 'ionicons'; import { addIcons } from 'ionicons';
import { diamondOutline, keyOutline, settingsOutline } from 'ionicons/icons'; import { diamondOutline, keyOutline, settingsOutline } from 'ionicons/icons';
@Component({ @Component({
changeDetection: ChangeDetectionStrategy.OnPush,
host: { class: 'page' }, host: { class: 'page' },
imports: [GfPageTabsComponent], imports: [GfPageTabsComponent],
selector: 'gf-user-account-page', selector: 'gf-user-account-page',
@ -52,9 +58,9 @@ export class GfUserAccountPageComponent {
routerLink: internalRoutes.account.subRoutes.access.routerLink routerLink: internalRoutes.account.subRoutes.access.routerLink
} }
]; ];
this.changeDetectorRef.markForCheck();
} }
this.changeDetectorRef.markForCheck();
}); });
addIcons({ diamondOutline, keyOutline, settingsOutline }); addIcons({ diamondOutline, keyOutline, settingsOutline });

12
apps/client/src/app/pages/zen/zen-page.component.ts

@ -6,12 +6,18 @@ import {
TabConfiguration TabConfiguration
} from '@ghostfolio/ui/page-tabs'; } from '@ghostfolio/ui/page-tabs';
import { ChangeDetectorRef, Component, DestroyRef } from '@angular/core'; import {
ChangeDetectionStrategy,
ChangeDetectorRef,
Component,
DestroyRef
} from '@angular/core';
import { takeUntilDestroyed } from '@angular/core/rxjs-interop'; import { takeUntilDestroyed } from '@angular/core/rxjs-interop';
import { addIcons } from 'ionicons'; import { addIcons } from 'ionicons';
import { albumsOutline, analyticsOutline } from 'ionicons/icons'; import { albumsOutline, analyticsOutline } from 'ionicons/icons';
@Component({ @Component({
changeDetection: ChangeDetectionStrategy.OnPush,
host: { class: 'page' }, host: { class: 'page' },
imports: [GfPageTabsComponent], imports: [GfPageTabsComponent],
selector: 'gf-zen-page', selector: 'gf-zen-page',
@ -44,9 +50,9 @@ export class GfZenPageComponent {
} }
]; ];
this.user = state.user; this.user = state.user;
this.changeDetectorRef.markForCheck();
} }
this.changeDetectorRef.markForCheck();
}); });
addIcons({ albumsOutline, analyticsOutline }); addIcons({ albumsOutline, analyticsOutline });

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

@ -1739,7 +1739,7 @@
</trans-unit> </trans-unit>
<trans-unit id="7407646470109951507" datatype="html"> <trans-unit id="7407646470109951507" datatype="html">
<source>popular</source> <source>popular</source>
<target state="new">popular</target> <target state="translated">beliebt</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">79</context> <context context-type="linenumber">79</context>

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

@ -11,7 +11,6 @@ export const ghostfolioScraperApiSymbolPrefix = `_${ghostfolioPrefix}_`;
export const ghostfolioFearAndGreedIndexDataSourceCryptocurrencies = export const ghostfolioFearAndGreedIndexDataSourceCryptocurrencies =
DataSource.MANUAL; DataSource.MANUAL;
export const ghostfolioFearAndGreedIndexDataSourceStocks = DataSource.RAPID_API;
export const ghostfolioFearAndGreedIndexSymbolCryptocurrencies = `${ghostfolioPrefix}_FEAR_AND_GREED_INDEX_CRYPTOCURRENCIES`; export const ghostfolioFearAndGreedIndexSymbolCryptocurrencies = `${ghostfolioPrefix}_FEAR_AND_GREED_INDEX_CRYPTOCURRENCIES`;
export const ghostfolioFearAndGreedIndexSymbolStocks = `${ghostfolioPrefix}_FEAR_AND_GREED_INDEX_STOCKS`; export const ghostfolioFearAndGreedIndexSymbolStocks = `${ghostfolioPrefix}_FEAR_AND_GREED_INDEX_STOCKS`;

4
package-lock.json

@ -1,12 +1,12 @@
{ {
"name": "ghostfolio", "name": "ghostfolio",
"version": "3.22.0", "version": "3.23.0",
"lockfileVersion": 3, "lockfileVersion": 3,
"requires": true, "requires": true,
"packages": { "packages": {
"": { "": {
"name": "ghostfolio", "name": "ghostfolio",
"version": "3.22.0", "version": "3.23.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.22.0", "version": "3.23.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",

Loading…
Cancel
Save