Browse Source

Merge branch 'main' into feature/extend-account-detail-dialog-by-performance

pull/8259/head
Thomas Kaul 1 day ago
committed by GitHub
parent
commit
19f8928e6f
No known key found for this signature in database GPG Key ID: B5690EEEBB952194
  1. 2
      .github/workflows/build-code.yml
  2. 14
      CHANGELOG.md
  3. 2
      DEVELOPMENT.md
  4. 2
      apps/api/src/app/activities/activities.module.ts
  5. 8
      apps/api/src/app/app.module.ts
  6. 17
      apps/api/src/app/asset/asset.module.ts
  7. 3
      apps/api/src/app/endpoints/asset-profiles/asset-profiles.module.ts
  8. 3
      apps/api/src/app/endpoints/asset-profiles/asset.controller.ts
  9. 0
      apps/api/src/app/endpoints/cache/cache.controller.ts
  10. 0
      apps/api/src/app/endpoints/cache/cache.module.ts
  11. 0
      apps/api/src/app/endpoints/info/info.controller.ts
  12. 0
      apps/api/src/app/endpoints/info/info.module.ts
  13. 0
      apps/api/src/app/endpoints/info/info.service.ts
  14. 0
      apps/api/src/app/endpoints/logo/get-logo.dto.ts
  15. 0
      apps/api/src/app/endpoints/logo/logo.controller.ts
  16. 0
      apps/api/src/app/endpoints/logo/logo.module.ts
  17. 0
      apps/api/src/app/endpoints/logo/logo.service.ts
  18. 2
      apps/api/src/app/import/import.module.ts
  19. 12
      apps/client/src/app/components/home-overview/home-overview.component.ts
  20. 14
      apps/client/src/app/components/home-overview/home-overview.html
  21. 13
      apps/client/src/app/components/home-overview/home-overview.scss
  22. 18
      apps/client/src/app/components/subscription-interstitial-dialog/subscription-interstitial-dialog.component.ts
  23. 42
      apps/client/src/app/components/subscription-interstitial-dialog/subscription-interstitial-dialog.html
  24. 10
      apps/client/src/app/components/user-account-membership/user-account-membership.component.ts
  25. 5
      apps/client/src/app/components/user-account-membership/user-account-membership.html
  26. 14
      libs/common/src/lib/config.ts
  27. 7
      libs/ui/src/lib/dialog-footer/dialog-footer.component.html
  28. 4
      libs/ui/src/lib/dialog-footer/dialog-footer.component.scss
  29. 4
      libs/ui/src/lib/dialog-footer/dialog-footer.component.ts
  30. 13
      libs/ui/src/lib/dialog-header/dialog-header.component.html
  31. 11
      libs/ui/src/lib/dialog-header/dialog-header.component.scss
  32. 4
      libs/ui/src/lib/dialog-header/dialog-header.component.ts
  33. 22
      package-lock.json
  34. 8
      package.json

2
.github/workflows/build-code.yml

@ -13,7 +13,7 @@ jobs:
strategy: strategy:
matrix: matrix:
node_version: node_version:
- 22.23.2 - 22.23.4
steps: steps:
- name: Checkout code - name: Checkout code
uses: actions/checkout@v4 uses: actions/checkout@v4

14
CHANGELOG.md

@ -7,14 +7,28 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
## Unreleased ## Unreleased
### Added
- Added a link to the holdings tab in the holdings table on the overview tab of the home page (experimental)
### Changed ### Changed
- Extended the account detail dialog to include the change and the performance - Extended the account detail dialog to include the change and the performance
- Improved the style of the close button in the dialog footer component
- Improved the style of the close button in the dialog header component
## 3.90.0 - 2026-10-21
### Changed
- Extended the variations of the interstitials for the subscription by a trial
- Deprecated the `api/v1/auth-device` endpoint in favor of the `api/v1/auth-devices` endpoint - Deprecated the `api/v1/auth-device` endpoint in favor of the `api/v1/auth-devices` endpoint
- Deprecated the `api/v1/exchange-rate` endpoint in favor of the `api/v1/exchange-rates` endpoint - Deprecated the `api/v1/exchange-rate` endpoint in favor of the `api/v1/exchange-rates` endpoint
- Upgraded the `Node.js` engine from version `>=22.22.3` to `>=22.23.4` (`package.json`)
- Upgraded `@modelcontextprotocol/core` and `@modelcontextprotocol/server` from version `2.0.0` to `2.3.1` - Upgraded `@modelcontextprotocol/core` and `@modelcontextprotocol/server` from version `2.0.0` to `2.3.1`
- Upgraded `@modelcontextprotocol/node` from version `2.0.0` to `2.1.1` - Upgraded `@modelcontextprotocol/node` from version `2.0.0` to `2.1.1`
- Upgraded `@rekog/mcp-nest` from version `2.0.7` to `2.0.8` - Upgraded `@rekog/mcp-nest` from version `2.0.7` to `2.0.8`
- Upgraded `undici` from version `8.11.2` to `8.12.2`
## 3.89.0 - 2026-10-20 ## 3.89.0 - 2026-10-20

2
DEVELOPMENT.md

@ -5,7 +5,7 @@
### Prerequisites ### Prerequisites
- [Docker](https://www.docker.com/products/docker-desktop) - [Docker](https://www.docker.com/products/docker-desktop)
- [Node.js](https://nodejs.org/en/download) (version `>=22.18.0`) - [Node.js](https://nodejs.org/en/download) (version `>=22.23.4`)
- Create a local copy of this Git repository (clone) - Create a local copy of this Git repository (clone)
- Copy the file `.env.dev` to `.env` and populate it with your data (`cp .env.dev .env`) - Copy the file `.env.dev` to `.env` and populate it with your data (`cp .env.dev .env`)

2
apps/api/src/app/activities/activities.module.ts

@ -1,6 +1,5 @@
import { AccountBalanceService } from '@ghostfolio/api/app/account-balance/account-balance.service'; import { AccountBalanceService } from '@ghostfolio/api/app/account-balance/account-balance.service';
import { AccountService } from '@ghostfolio/api/app/account/account.service'; import { AccountService } from '@ghostfolio/api/app/account/account.service';
import { CacheModule } from '@ghostfolio/api/app/cache/cache.module';
import { RedactValuesInResponseModule } from '@ghostfolio/api/interceptors/redact-values-in-response/redact-values-in-response.module'; import { RedactValuesInResponseModule } from '@ghostfolio/api/interceptors/redact-values-in-response/redact-values-in-response.module';
import { TransformDataSourceInRequestModule } from '@ghostfolio/api/interceptors/transform-data-source-in-request/transform-data-source-in-request.module'; import { TransformDataSourceInRequestModule } from '@ghostfolio/api/interceptors/transform-data-source-in-request/transform-data-source-in-request.module';
import { TransformDataSourceInResponseModule } from '@ghostfolio/api/interceptors/transform-data-source-in-response/transform-data-source-in-response.module'; import { TransformDataSourceInResponseModule } from '@ghostfolio/api/interceptors/transform-data-source-in-response/transform-data-source-in-response.module';
@ -29,7 +28,6 @@ import { ActivitiesService } from './activities.service';
ApiModule, ApiModule,
AssetProfileSplitModule, AssetProfileSplitModule,
BenchmarkModule, BenchmarkModule,
CacheModule,
DataGatheringQueueModule, DataGatheringQueueModule,
DataProviderModule, DataProviderModule,
ExchangeRateDataModule, ExchangeRateDataModule,

8
apps/api/src/app/app.module.ts

@ -40,17 +40,18 @@ import { AccountModule } from './account/account.module';
import { ActivitiesModule } from './activities/activities.module'; import { ActivitiesModule } from './activities/activities.module';
import { AdminModule } from './admin/admin.module'; import { AdminModule } from './admin/admin.module';
import { AppController } from './app.controller'; import { AppController } from './app.controller';
import { AssetModule } from './asset/asset.module';
import { AuthModule } from './auth/auth.module'; import { AuthModule } from './auth/auth.module';
import { CacheModule } from './cache/cache.module';
import { AiModule } from './endpoints/ai/ai.module'; import { AiModule } from './endpoints/ai/ai.module';
import { ApiKeysModule } from './endpoints/api-keys/api-keys.module'; import { ApiKeysModule } from './endpoints/api-keys/api-keys.module';
import { AssetProfilesModule } from './endpoints/asset-profiles/asset-profiles.module'; import { AssetProfilesModule } from './endpoints/asset-profiles/asset-profiles.module';
import { AssetsModule } from './endpoints/assets/assets.module'; import { AssetsModule } from './endpoints/assets/assets.module';
import { AuthDevicesModule } from './endpoints/auth-devices/auth-devices.module'; import { AuthDevicesModule } from './endpoints/auth-devices/auth-devices.module';
import { BenchmarksModule } from './endpoints/benchmarks/benchmarks.module'; import { BenchmarksModule } from './endpoints/benchmarks/benchmarks.module';
import { CacheModule } from './endpoints/cache/cache.module';
import { GhostfolioModule } from './endpoints/data-providers/ghostfolio/ghostfolio.module'; import { GhostfolioModule } from './endpoints/data-providers/ghostfolio/ghostfolio.module';
import { ExchangeRatesModule } from './endpoints/exchange-rates/exchange-rates.module'; import { ExchangeRatesModule } from './endpoints/exchange-rates/exchange-rates.module';
import { InfoModule } from './endpoints/info/info.module';
import { LogoModule } from './endpoints/logo/logo.module';
import { MarketDataModule } from './endpoints/market-data/market-data.module'; import { MarketDataModule } from './endpoints/market-data/market-data.module';
import { McpModule } from './endpoints/mcp/mcp.module'; import { McpModule } from './endpoints/mcp/mcp.module';
import { PlatformsModule } from './endpoints/platforms/platforms.module'; import { PlatformsModule } from './endpoints/platforms/platforms.module';
@ -61,8 +62,6 @@ import { WatchlistModule } from './endpoints/watchlist/watchlist.module';
import { ExportModule } from './export/export.module'; import { ExportModule } from './export/export.module';
import { HealthModule } from './health/health.module'; import { HealthModule } from './health/health.module';
import { ImportModule } from './import/import.module'; import { ImportModule } from './import/import.module';
import { InfoModule } from './info/info.module';
import { LogoModule } from './logo/logo.module';
import { PortfolioModule } from './portfolio/portfolio.module'; import { PortfolioModule } from './portfolio/portfolio.module';
import { SubscriptionModule } from './subscription/subscription.module'; import { SubscriptionModule } from './subscription/subscription.module';
import { SymbolModule } from './symbol/symbol.module'; import { SymbolModule } from './symbol/symbol.module';
@ -78,7 +77,6 @@ import { UserModule } from './user/user.module';
AiModule, AiModule,
ApiKeysModule, ApiKeysModule,
AssetProfilesModule, AssetProfilesModule,
AssetModule,
AssetsModule, AssetsModule,
AuthDevicesModule, AuthDevicesModule,
AuthModule, AuthModule,

17
apps/api/src/app/asset/asset.module.ts

@ -1,17 +0,0 @@
import { AssetProfilesModule } from '@ghostfolio/api/app/endpoints/asset-profiles/asset-profiles.module';
import { TransformDataSourceInRequestModule } from '@ghostfolio/api/interceptors/transform-data-source-in-request/transform-data-source-in-request.module';
import { TransformDataSourceInResponseModule } from '@ghostfolio/api/interceptors/transform-data-source-in-response/transform-data-source-in-response.module';
import { Module } from '@nestjs/common';
import { AssetController } from './asset.controller';
@Module({
controllers: [AssetController],
imports: [
AssetProfilesModule,
TransformDataSourceInRequestModule,
TransformDataSourceInResponseModule
]
})
export class AssetModule {}

3
apps/api/src/app/endpoints/asset-profiles/asset-profiles.module.ts

@ -15,9 +15,10 @@ import { Module } from '@nestjs/common';
import { AssetProfilesController } from './asset-profiles.controller'; import { AssetProfilesController } from './asset-profiles.controller';
import { AssetProfilesService } from './asset-profiles.service'; import { AssetProfilesService } from './asset-profiles.service';
import { AssetController } from './asset.controller';
@Module({ @Module({
controllers: [AssetProfilesController], controllers: [AssetController, AssetProfilesController],
exports: [AssetProfilesService], exports: [AssetProfilesService],
imports: [ imports: [
ActivitiesModule, ActivitiesModule,

3
apps/api/src/app/asset/asset.controller.ts → apps/api/src/app/endpoints/asset-profiles/asset.controller.ts

@ -1,4 +1,3 @@
import { AssetProfilesService } from '@ghostfolio/api/app/endpoints/asset-profiles/asset-profiles.service';
import { TransformDataSourceInRequestInterceptor } from '@ghostfolio/api/interceptors/transform-data-source-in-request/transform-data-source-in-request.interceptor'; import { TransformDataSourceInRequestInterceptor } from '@ghostfolio/api/interceptors/transform-data-source-in-request/transform-data-source-in-request.interceptor';
import { TransformDataSourceInResponseInterceptor } from '@ghostfolio/api/interceptors/transform-data-source-in-response/transform-data-source-in-response.interceptor'; import { TransformDataSourceInResponseInterceptor } from '@ghostfolio/api/interceptors/transform-data-source-in-response/transform-data-source-in-response.interceptor';
import type { AssetResponse } from '@ghostfolio/common/interfaces'; import type { AssetResponse } from '@ghostfolio/common/interfaces';
@ -7,6 +6,8 @@ import { Controller, Get, Param, UseInterceptors } from '@nestjs/common';
import { DataSource } from '@prisma/client'; import { DataSource } from '@prisma/client';
import { pick } from 'lodash-es'; import { pick } from 'lodash-es';
import { AssetProfilesService } from './asset-profiles.service';
@Controller('asset') @Controller('asset')
export class AssetController { export class AssetController {
public constructor( public constructor(

0
apps/api/src/app/cache/cache.controller.ts → apps/api/src/app/endpoints/cache/cache.controller.ts

0
apps/api/src/app/cache/cache.module.ts → apps/api/src/app/endpoints/cache/cache.module.ts

0
apps/api/src/app/info/info.controller.ts → apps/api/src/app/endpoints/info/info.controller.ts

0
apps/api/src/app/info/info.module.ts → apps/api/src/app/endpoints/info/info.module.ts

0
apps/api/src/app/info/info.service.ts → apps/api/src/app/endpoints/info/info.service.ts

0
apps/api/src/app/logo/get-logo.dto.ts → apps/api/src/app/endpoints/logo/get-logo.dto.ts

0
apps/api/src/app/logo/logo.controller.ts → apps/api/src/app/endpoints/logo/logo.controller.ts

0
apps/api/src/app/logo/logo.module.ts → apps/api/src/app/endpoints/logo/logo.module.ts

0
apps/api/src/app/logo/logo.service.ts → apps/api/src/app/endpoints/logo/logo.service.ts

2
apps/api/src/app/import/import.module.ts

@ -1,6 +1,5 @@
import { AccountModule } from '@ghostfolio/api/app/account/account.module'; import { AccountModule } from '@ghostfolio/api/app/account/account.module';
import { ActivitiesModule } from '@ghostfolio/api/app/activities/activities.module'; import { ActivitiesModule } from '@ghostfolio/api/app/activities/activities.module';
import { CacheModule } from '@ghostfolio/api/app/cache/cache.module';
import { PortfolioModule } from '@ghostfolio/api/app/portfolio/portfolio.module'; import { PortfolioModule } from '@ghostfolio/api/app/portfolio/portfolio.module';
import { TransformDataSourceInRequestModule } from '@ghostfolio/api/interceptors/transform-data-source-in-request/transform-data-source-in-request.module'; import { TransformDataSourceInRequestModule } from '@ghostfolio/api/interceptors/transform-data-source-in-request/transform-data-source-in-request.module';
import { TransformDataSourceInResponseModule } from '@ghostfolio/api/interceptors/transform-data-source-in-response/transform-data-source-in-response.module'; import { TransformDataSourceInResponseModule } from '@ghostfolio/api/interceptors/transform-data-source-in-response/transform-data-source-in-response.module';
@ -28,7 +27,6 @@ import { ImportService } from './import.service';
AccountModule, AccountModule,
ActivitiesModule, ActivitiesModule,
ApiModule, ApiModule,
CacheModule,
ConfigurationModule, ConfigurationModule,
DataGatheringQueueModule, DataGatheringQueueModule,
DataProviderModule, DataProviderModule,

12
apps/client/src/app/components/home-overview/home-overview.component.ts

@ -36,6 +36,9 @@ import { takeUntilDestroyed } from '@angular/core/rxjs-interop';
import { MatButtonModule } from '@angular/material/button'; import { MatButtonModule } from '@angular/material/button';
import { MatCardModule } from '@angular/material/card'; import { MatCardModule } from '@angular/material/card';
import { Router, RouterModule } from '@angular/router'; import { Router, RouterModule } from '@angular/router';
import { IonIcon } from '@ionic/angular/standalone';
import { addIcons } from 'ionicons';
import { arrowForwardOutline } from 'ionicons/icons';
import { DeviceDetectorService } from 'ngx-device-detector'; import { DeviceDetectorService } from 'ngx-device-detector';
@Component({ @Component({
@ -44,6 +47,7 @@ import { DeviceDetectorService } from 'ngx-device-detector';
GfHoldingsTableComponent, GfHoldingsTableComponent,
GfLineChartComponent, GfLineChartComponent,
GfPortfolioPerformanceComponent, GfPortfolioPerformanceComponent,
IonIcon,
MatButtonModule, MatButtonModule,
MatCardModule, MatCardModule,
RouterModule RouterModule
@ -71,9 +75,15 @@ export class GfHomeOverviewComponent implements OnInit {
protected readonly user = signal<User | null>(null); protected readonly user = signal<User | null>(null);
protected readonly routerLinkAccounts = internalRoutes.accounts.routerLink; protected readonly routerLinkAccounts = internalRoutes.accounts.routerLink;
protected readonly routerLinkHomeHoldings =
internalRoutes.home.subRoutes.holdings.routerLink;
protected readonly routerLinkPortfolio = internalRoutes.portfolio.routerLink; protected readonly routerLinkPortfolio = internalRoutes.portfolio.routerLink;
protected readonly routerLinkPortfolioActivities = protected readonly routerLinkPortfolioActivities =
internalRoutes.portfolio.subRoutes.activities.routerLink; internalRoutes.portfolio.subRoutes.activities.routerLink;
protected readonly routerLinkPortfolioActivitiesCreate = protected readonly routerLinkPortfolioActivitiesCreate =
internalRoutes.portfolio.subRoutes.activities.subRoutes.create.routerLink; internalRoutes.portfolio.subRoutes.activities.subRoutes.create.routerLink;
@ -134,6 +144,8 @@ export class GfHomeOverviewComponent implements OnInit {
this.update(); this.update();
} }
}); });
addIcons({ arrowForwardOutline });
} }
public ngOnInit() { public ngOnInit() {

14
apps/client/src/app/components/home-overview/home-overview.html

@ -101,8 +101,20 @@
appearance="outlined" appearance="outlined"
class="h-100 holdings position-absolute w-100" class="h-100 holdings position-absolute w-100"
> >
<mat-card-header> <mat-card-header class="align-items-center">
<mat-card-title i18n>Holdings</mat-card-title> <mat-card-title i18n>Holdings</mat-card-title>
<a
class="ms-auto"
i18n-title
mat-icon-button
title="View Details"
[routerLink]="routerLinkHomeHoldings"
>
<ion-icon
class="d-block text-body-secondary"
name="arrow-forward-outline"
/>
</a>
</mat-card-header> </mat-card-header>
<mat-card-content class="overflow-auto"> <mat-card-content class="overflow-auto">
<gf-holdings-table <gf-holdings-table

13
apps/client/src/app/components/home-overview/home-overview.scss

@ -34,6 +34,19 @@
&.holdings { &.holdings {
--mat-card-title-text-size: var(--mat-sys-title-medium-size); --mat-card-title-text-size: var(--mat-sys-title-medium-size);
mat-card-header {
a[mat-icon-button] {
--mat-icon-button-icon-size: 1rem;
margin-right: -0.125rem;
margin-top: -0.5rem;
}
mat-card-title {
padding-left: 0.1875rem;
}
}
mat-card-content { mat-card-content {
scrollbar-width: none; scrollbar-width: none;

18
apps/client/src/app/components/subscription-interstitial-dialog/subscription-interstitial-dialog.component.ts

@ -1,3 +1,5 @@
import { TRY_SUBSCRIPTION_MAIL_HREF } from '@ghostfolio/common/config';
import { hasPermission, permissions } from '@ghostfolio/common/permissions';
import { publicRoutes } from '@ghostfolio/common/routes/routes'; import { publicRoutes } from '@ghostfolio/common/routes/routes';
import { GfMembershipCardComponent } from '@ghostfolio/ui/membership-card'; import { GfMembershipCardComponent } from '@ghostfolio/ui/membership-card';
import { GfPremiumIndicatorComponent } from '@ghostfolio/ui/premium-indicator'; import { GfPremiumIndicatorComponent } from '@ghostfolio/ui/premium-indicator';
@ -51,6 +53,7 @@ export class GfSubscriptionInterstitialDialogComponent implements OnInit {
public remainingSkipButtonDelay = public remainingSkipButtonDelay =
GfSubscriptionInterstitialDialogComponent.SKIP_BUTTON_DELAY_IN_SECONDS; GfSubscriptionInterstitialDialogComponent.SKIP_BUTTON_DELAY_IN_SECONDS;
public routerLinkPricing = publicRoutes.pricing.routerLink; public routerLinkPricing = publicRoutes.pricing.routerLink;
public trySubscriptionMailHref = TRY_SUBSCRIPTION_MAIL_HREF;
public variantIndex: number; public variantIndex: number;
public constructor( public constructor(
@ -59,9 +62,18 @@ export class GfSubscriptionInterstitialDialogComponent implements OnInit {
private destroyRef: DestroyRef, private destroyRef: DestroyRef,
public dialogRef: MatDialogRef<GfSubscriptionInterstitialDialogComponent> public dialogRef: MatDialogRef<GfSubscriptionInterstitialDialogComponent>
) { ) {
this.variantIndex = Math.floor( const isEligibleForTrial =
Math.random() * GfSubscriptionInterstitialDialogComponent.VARIANTS_COUNT !this.data.user?.subscription?.subscribedAt &&
); hasPermission(
this.data.user?.permissions,
permissions.updateUserSettings
);
const variantsCount = isEligibleForTrial
? GfSubscriptionInterstitialDialogComponent.VARIANTS_COUNT + 1
: GfSubscriptionInterstitialDialogComponent.VARIANTS_COUNT;
this.variantIndex = Math.floor(Math.random() * variantsCount);
addIcons({ arrowForwardOutline, checkmarkCircleOutline }); addIcons({ arrowForwardOutline, checkmarkCircleOutline });
} }

42
apps/client/src/app/components/subscription-interstitial-dialog/subscription-interstitial-dialog.html

@ -195,4 +195,46 @@
</a> </a>
</div> </div>
} }
@case (4) {
<div class="flex-grow-1 pt-0" mat-dialog-content>
<div class="intro-container">
<div class="intro-inner-container mx-auto">
<div class="h-100 intro w-100"></div>
</div>
</div>
<div>
<h5 class="align-items-center d-flex justify-content-center mb-3">
<span>Ghostfolio Premium</span>
<gf-premium-indicator class="ms-1" [enableLink]="false" />
</h5>
<div class="fw-normal h5 mb-3 text-center" i18n>
Request a free trial
</div>
<p class="m-0 text-center" i18n>
No credit card required. The trial ends automatically.
</p>
</div>
</div>
<div class="justify-content-end" mat-dialog-actions>
<button
mat-button
[disabled]="remainingSkipButtonDelay > 0"
(click)="closeDialog()"
>
<ng-container i18n>Skip</ng-container>
@if (remainingSkipButtonDelay > 0) {
({{ remainingSkipButtonDelay }})
}
</button>
<a
color="primary"
mat-flat-button
[href]="trySubscriptionMailHref"
(click)="closeDialog()"
>
<span i18n>Try Premium</span>
<ion-icon class="ms-1" name="arrow-forward-outline" />
</a>
</div>
}
} }

10
apps/client/src/app/components/user-account-membership/user-account-membership.component.ts

@ -1,5 +1,5 @@
import { UserService } from '@ghostfolio/client/services/user/user.service'; import { UserService } from '@ghostfolio/client/services/user/user.service';
import { E_MAIL_LINE_BREAK } from '@ghostfolio/common/config'; import { TRY_SUBSCRIPTION_MAIL_HREF } from '@ghostfolio/common/config';
import { ConfirmationDialogType } from '@ghostfolio/common/enums'; import { ConfirmationDialogType } from '@ghostfolio/common/enums';
import { getDateFormatString } from '@ghostfolio/common/helper'; import { getDateFormatString } from '@ghostfolio/common/helper';
import { User } from '@ghostfolio/common/interfaces'; import { User } from '@ghostfolio/common/interfaces';
@ -49,13 +49,7 @@ export class GfUserAccountMembershipComponent {
protected hasPermissionToCreateApiKey: boolean; protected hasPermissionToCreateApiKey: boolean;
protected hasPermissionToUpdateUserSettings: boolean; protected hasPermissionToUpdateUserSettings: boolean;
protected price: number; protected price: number;
protected readonly trySubscriptionMailHref = `mailto:hi@ghostfol.io?subject=Ghostfolio Premium Trial&body=${[ protected readonly trySubscriptionMailHref = TRY_SUBSCRIPTION_MAIL_HREF;
'Hello',
'',
'I am interested in Ghostfolio Premium. Can you please send me a coupon code to try it for some time?',
'',
'Kind regards'
].join(E_MAIL_LINE_BREAK)}`;
protected user: User; protected user: User;
private couponId: string | undefined; private couponId: string | undefined;

5
apps/client/src/app/components/user-account-membership/user-account-membership.html

@ -50,7 +50,10 @@
} }
} }
<div class="align-items-center d-flex justify-content-center mt-4"> <div class="align-items-center d-flex justify-content-center mt-4">
@if (!user?.subscription?.subscribedAt) { @if (
!user?.subscription?.subscribedAt &&
hasPermissionToUpdateUserSettings
) {
<a <a
class="mx-1" class="mx-1"
mat-stroked-button mat-stroked-button

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

@ -422,4 +422,18 @@ export const THROTTLE_DEFAULT_TTL = ms('1 minute');
export const THROTTLE_SIGNUP_LIMIT = 5; export const THROTTLE_SIGNUP_LIMIT = 5;
export const THROTTLE_SIGNUP_TTL = ms('1 hour'); export const THROTTLE_SIGNUP_TTL = ms('1 hour');
export const TRY_SUBSCRIPTION_MAIL_HREF = `mailto:hi@ghostfol.io?subject=${encodeURIComponent(
'Ghostfolio Premium Trial'
)}&body=${[
'Hello',
'',
'I am interested in Ghostfolio Premium. Can you please send me a coupon code to try it for some time?',
'',
'Kind regards'
]
.map((line) => {
return encodeURIComponent(line);
})
.join(E_MAIL_LINE_BREAK)}`;
export const UNKNOWN_KEY = 'UNKNOWN'; export const UNKNOWN_KEY = 'UNKNOWN';

7
libs/ui/src/lib/dialog-footer/dialog-footer.component.html

@ -1,7 +1,10 @@
@if (deviceType === 'mobile') { @if (deviceType === 'mobile') {
<div class="d-flex justify-content-center" mat-dialog-actions> <div class="d-flex justify-content-center" mat-dialog-actions>
<button mat-button (click)="onClickCloseButton()"> <button mat-icon-button (click)="onClickCloseButton()">
<ion-icon name="close" size="large" /> <ion-icon
class="d-block mat-icon text-body-secondary"
name="close-outline"
/>
</button> </button>
</div> </div>
} }

4
libs/ui/src/lib/dialog-footer/dialog-footer.component.scss

@ -1,3 +1,7 @@
:host { :host {
display: block; display: block;
button[mat-icon-button] {
--mat-icon-button-touch-target-display: block;
}
} }

4
libs/ui/src/lib/dialog-footer/dialog-footer.component.ts

@ -9,7 +9,7 @@ import { MatButtonModule } from '@angular/material/button';
import { MatDialogModule } from '@angular/material/dialog'; import { MatDialogModule } from '@angular/material/dialog';
import { IonIcon } from '@ionic/angular/standalone'; import { IonIcon } from '@ionic/angular/standalone';
import { addIcons } from 'ionicons'; import { addIcons } from 'ionicons';
import { close } from 'ionicons/icons'; import { closeOutline } from 'ionicons/icons';
@Component({ @Component({
changeDetection: ChangeDetectionStrategy.OnPush, changeDetection: ChangeDetectionStrategy.OnPush,
@ -25,7 +25,7 @@ export class GfDialogFooterComponent {
@Output() closeButtonClicked = new EventEmitter<void>(); @Output() closeButtonClicked = new EventEmitter<void>();
public constructor() { public constructor() {
addIcons({ close }); addIcons({ closeOutline });
} }
public onClickCloseButton() { public onClickCloseButton() {

13
libs/ui/src/lib/dialog-header/dialog-header.component.html

@ -1,12 +1,19 @@
<div class="d-flex" mat-dialog-title> <div
class="d-flex"
mat-dialog-title
[class.with-close-button]="deviceType() !== 'mobile'"
>
<span <span
class="flex-grow-1 text-truncate" class="flex-grow-1 text-truncate"
[class.text-center]="position() === 'center'" [class.text-center]="position() === 'center'"
>{{ title() }}</span >{{ title() }}</span
> >
@if (deviceType() !== 'mobile') { @if (deviceType() !== 'mobile') {
<button class="no-min-width px-0" mat-button (click)="onClickCloseButton()"> <button mat-icon-button (click)="onClickCloseButton()">
<ion-icon name="close" size="large" /> <ion-icon
class="d-block mat-icon text-body-secondary"
name="close-outline"
/>
</button> </button>
} }
</div> </div>

11
libs/ui/src/lib/dialog-header/dialog-header.component.scss

@ -1,3 +1,14 @@
:host { :host {
display: block; display: block;
button[mat-icon-button] {
margin-right: -0.8125rem;
margin-top: 0.125rem;
}
.with-close-button {
.text-center {
padding-left: 0.9375rem;
}
}
} }

4
libs/ui/src/lib/dialog-header/dialog-header.component.ts

@ -8,7 +8,7 @@ import { MatButtonModule } from '@angular/material/button';
import { MatDialogModule } from '@angular/material/dialog'; import { MatDialogModule } from '@angular/material/dialog';
import { IonIcon } from '@ionic/angular/standalone'; import { IonIcon } from '@ionic/angular/standalone';
import { addIcons } from 'ionicons'; import { addIcons } from 'ionicons';
import { close } from 'ionicons/icons'; import { closeOutline } from 'ionicons/icons';
@Component({ @Component({
changeDetection: ChangeDetectionStrategy.OnPush, changeDetection: ChangeDetectionStrategy.OnPush,
@ -26,7 +26,7 @@ export class GfDialogHeaderComponent {
public readonly closeButtonClicked = output<void>(); public readonly closeButtonClicked = output<void>();
public constructor() { public constructor() {
addIcons({ close }); addIcons({ closeOutline });
} }
protected onClickCloseButton() { protected onClickCloseButton() {

22
package-lock.json

@ -1,12 +1,12 @@
{ {
"name": "ghostfolio", "name": "ghostfolio",
"version": "3.89.0", "version": "3.90.0",
"lockfileVersion": 3, "lockfileVersion": 3,
"requires": true, "requires": true,
"packages": { "packages": {
"": { "": {
"name": "ghostfolio", "name": "ghostfolio",
"version": "3.89.0", "version": "3.90.0",
"hasInstallScript": true, "hasInstallScript": true,
"license": "AGPL-3.0", "license": "AGPL-3.0",
"dependencies": { "dependencies": {
@ -104,7 +104,7 @@
"svgmap": "2.21.0", "svgmap": "2.21.0",
"tablemark": "4.1.0", "tablemark": "4.1.0",
"twitter-api-v2": "1.29.1", "twitter-api-v2": "1.29.1",
"undici": "8.11.2", "undici": "8.12.2",
"uuid": "14.0.2", "uuid": "14.0.2",
"yahoo-finance2": "4.0.2", "yahoo-finance2": "4.0.2",
"zod": "4.6.5", "zod": "4.6.5",
@ -149,7 +149,7 @@
"@types/jest": "30.0.0", "@types/jest": "30.0.0",
"@types/jsonpath": "0.2.4", "@types/jsonpath": "0.2.4",
"@types/lodash-es": "4.17.12", "@types/lodash-es": "4.17.12",
"@types/node": "22.20.1", "@types/node": "22.20.5",
"@types/papaparse": "5.5.2", "@types/papaparse": "5.5.2",
"@types/passport-google-oauth20": "2.0.17", "@types/passport-google-oauth20": "2.0.17",
"@types/passport-openidconnect": "0.1.3", "@types/passport-openidconnect": "0.1.3",
@ -180,7 +180,7 @@
"webpack-cli": "7.1.0" "webpack-cli": "7.1.0"
}, },
"engines": { "engines": {
"node": ">=22.22.3" "node": ">=22.23.4"
} }
}, },
"node_modules/@adobe/css-tools": { "node_modules/@adobe/css-tools": {
@ -14306,9 +14306,9 @@
"license": "MIT" "license": "MIT"
}, },
"node_modules/@types/node": { "node_modules/@types/node": {
"version": "22.20.1", "version": "22.20.5",
"resolved": "https://registry.npmjs.org/@types/node/-/node-22.20.1.tgz", "resolved": "https://registry.npmjs.org/@types/node/-/node-22.20.5.tgz",
"integrity": "sha512-EANqOCF9QFyra+4pfxUcX9STKJpCLjMbObVzljIJomAWSnuSIEAvyzEU53GaajbXJEgdh0iEcPL+DGvpUd4k1Q==", "integrity": "sha512-U2+DNr+wSjpsTS/wZGYHq7GcwfuSmKiKvoPvK22zwTlRhU91yOniN4qRR5KhIjvif7ysw/dz/hKmfDH0Ris4aA==",
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"undici-types": "~6.21.0" "undici-types": "~6.21.0"
@ -33913,9 +33913,9 @@
"license": "MIT" "license": "MIT"
}, },
"node_modules/undici": { "node_modules/undici": {
"version": "8.11.2", "version": "8.12.2",
"resolved": "https://registry.npmjs.org/undici/-/undici-8.11.2.tgz", "resolved": "https://registry.npmjs.org/undici/-/undici-8.12.2.tgz",
"integrity": "sha512-u4UB2/IrKdU6lFxumHmmo1a3fCQO5tzQllRorfoRS63txhrB7xTpSn1PftwC4qEHkOaqP95fCWW4lJzwErwzhQ==", "integrity": "sha512-XtuDWVdqTLr3NAxm2ly9/hU6kUsJOgGh1RASzMBbsw6aWCOaQzHdW+QQDfdERjfJ2V5vL5pRPa4RetO9UfivDg==",
"license": "MIT", "license": "MIT",
"engines": { "engines": {
"node": ">=22.19.0" "node": ">=22.19.0"

8
package.json

@ -1,6 +1,6 @@
{ {
"name": "ghostfolio", "name": "ghostfolio",
"version": "3.89.0", "version": "3.90.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",
@ -148,7 +148,7 @@
"svgmap": "2.21.0", "svgmap": "2.21.0",
"tablemark": "4.1.0", "tablemark": "4.1.0",
"twitter-api-v2": "1.29.1", "twitter-api-v2": "1.29.1",
"undici": "8.11.2", "undici": "8.12.2",
"uuid": "14.0.2", "uuid": "14.0.2",
"yahoo-finance2": "4.0.2", "yahoo-finance2": "4.0.2",
"zod": "4.6.5", "zod": "4.6.5",
@ -193,7 +193,7 @@
"@types/jest": "30.0.0", "@types/jest": "30.0.0",
"@types/jsonpath": "0.2.4", "@types/jsonpath": "0.2.4",
"@types/lodash-es": "4.17.12", "@types/lodash-es": "4.17.12",
"@types/node": "22.20.1", "@types/node": "22.20.5",
"@types/papaparse": "5.5.2", "@types/papaparse": "5.5.2",
"@types/passport-google-oauth20": "2.0.17", "@types/passport-google-oauth20": "2.0.17",
"@types/passport-openidconnect": "0.1.3", "@types/passport-openidconnect": "0.1.3",
@ -224,6 +224,6 @@
"webpack-cli": "7.1.0" "webpack-cli": "7.1.0"
}, },
"engines": { "engines": {
"node": ">=22.22.3" "node": ">=22.23.4"
} }
} }

Loading…
Cancel
Save