From 93967c1c31838b70869462ba9dc51b39473442da Mon Sep 17 00:00:00 2001 From: Thomas Kaul <4159106+dtslvr@users.noreply.github.com> Date: Sat, 10 Oct 2026 12:15:35 +0200 Subject: [PATCH 01/14] Task/improve label in portfolio evolution chart (#8104) Improve label --- .../src/app/pages/portfolio/analysis/analysis-page.component.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/client/src/app/pages/portfolio/analysis/analysis-page.component.ts b/apps/client/src/app/pages/portfolio/analysis/analysis-page.component.ts index 62debb78a5..460ac9de8e 100644 --- a/apps/client/src/app/pages/portfolio/analysis/analysis-page.component.ts +++ b/apps/client/src/app/pages/portfolio/analysis/analysis-page.component.ts @@ -103,7 +103,7 @@ export class GfAnalysisPageComponent implements OnInit { protected readonly PerformanceCalculationType = PerformanceCalculationType; protected performanceDataItems: LineChartItem[]; protected performanceDataItemsInPercentage: LineChartItem[]; - protected readonly portfolioEvolutionDataLabel = $localize`Investment`; + protected readonly portfolioEvolutionDataLabel = $localize`Invested Capital`; protected precision = 2; protected savingsRatePerMonth: number | undefined; protected streaks: PortfolioInvestmentsResponse['streaks']; From 59e990c7436555e3defabff290f529c4565cc1f2 Mon Sep 17 00:00:00 2001 From: Kenrick Tandrian <60643640+KenTandrian@users.noreply.github.com> Date: Sat, 10 Oct 2026 17:28:07 +0700 Subject: [PATCH 02/14] Task/resolve strict template errors in home components (#8101) Resolve strict template errors --- .../home-holdings/home-holdings.component.ts | 56 +++++++++++++------ .../home-holdings/home-holdings.html | 10 ++-- .../home-overview/home-overview.component.ts | 8 ++- .../home-overview/home-overview.html | 13 +++-- .../portfolio-performance.component.ts | 14 +++-- .../lib/line-chart/line-chart.component.ts | 2 +- 6 files changed, 68 insertions(+), 35 deletions(-) diff --git a/apps/client/src/app/components/home-holdings/home-holdings.component.ts b/apps/client/src/app/components/home-holdings/home-holdings.component.ts index f079e1c9c6..333a24d5d6 100644 --- a/apps/client/src/app/components/home-holdings/home-holdings.component.ts +++ b/apps/client/src/app/components/home-holdings/home-holdings.component.ts @@ -1,5 +1,9 @@ import { UserService } from '@ghostfolio/client/services/user/user.service'; -import { DEFAULT_LOCALE } from '@ghostfolio/common/config'; +import { + DEFAULT_CURRENCY, + DEFAULT_DATE_RANGE, + DEFAULT_LOCALE +} from '@ghostfolio/common/config'; import { AssetProfileIdentifier, PortfolioPosition, @@ -22,10 +26,12 @@ import { ChangeDetectionStrategy, ChangeDetectorRef, Component, + computed, CUSTOM_ELEMENTS_SCHEMA, DestroyRef, inject, - OnInit + OnInit, + signal } from '@angular/core'; import { takeUntilDestroyed } from '@angular/core/rxjs-interop'; import { MatButtonModule } from '@angular/material/button'; @@ -51,9 +57,22 @@ import { DeviceDetectorService } from 'ngx-device-detector'; export class GfHomeHoldingsComponent implements OnInit { public static DEFAULT_HOLDINGS_VIEW_MODE: HoldingsViewMode = 'TABLE'; - protected readonly DEFAULT_LOCALE = DEFAULT_LOCALE; + protected readonly baseCurrency = computed(() => { + return this.user()?.settings?.baseCurrency ?? DEFAULT_CURRENCY; + }); + + protected readonly colorScheme = computed(() => { + return this.user()?.settings?.colorScheme; + }); + + protected readonly dateRange = computed(() => { + return this.user()?.settings?.dateRange ?? DEFAULT_DATE_RANGE; + }); + + protected readonly deviceType = computed(() => { + return this.deviceDetectorService.deviceInfo().deviceType; + }); - protected deviceType: string; protected hasPermissionToAccessHoldingsChart: boolean; protected hasPermissionToCreateActivity: boolean; protected holdings: PortfolioPosition[] | undefined; @@ -78,9 +97,14 @@ export class GfHomeHoldingsComponent implements OnInit { { label: $localize`Closed`, value: 'CLOSED' } ]; protected isHoldingsViewModeToggleDisabled = true; + + protected readonly locale = computed(() => { + return this.user()?.settings?.locale ?? DEFAULT_LOCALE; + }); + protected readonly routerLinkPortfolioActivities = internalRoutes.portfolio.subRoutes.activities.routerLink; - protected user: User; + protected readonly user = signal(null); private readonly changeDetectorRef = inject(ChangeDetectorRef); private readonly dataService = inject(DataService); @@ -94,22 +118,22 @@ export class GfHomeHoldingsComponent implements OnInit { } public ngOnInit() { - this.deviceType = this.deviceDetectorService.getDeviceInfo().deviceType; - this.userService.stateChanged .pipe(takeUntilDestroyed(this.destroyRef)) .subscribe((state) => { if (state?.user) { - this.user = state.user; + const user = state.user; + + this.user.set(user); this.hasPermissionToAccessHoldingsChart = hasPermission( - this.user.permissions, + user.permissions, permissions.accessHoldingsChart ); this.hasPermissionToCreateActivity = - hasPermission(this.user.permissions, permissions.createActivity) && - hasScope(this.user.scopes, scopes.activityCreate); + hasPermission(user.permissions, permissions.createActivity) && + hasScope(user.scopes, scopes.activityCreate); this.initialize(); } @@ -129,7 +153,7 @@ export class GfHomeHoldingsComponent implements OnInit { .get(true) .pipe(takeUntilDestroyed(this.destroyRef)) .subscribe((user) => { - this.user = user; + this.user.set(user); this.changeDetectorRef.markForCheck(); }); @@ -144,7 +168,7 @@ export class GfHomeHoldingsComponent implements OnInit { protected onHoldingClicked({ dataSource, symbol }: AssetProfileIdentifier) { if (dataSource && symbol) { - this.router.navigate([], { + void this.router.navigate([], { queryParams: { dataSource, symbol, holdingDetailDialog: true } }); } @@ -157,7 +181,7 @@ export class GfHomeHoldingsComponent implements OnInit { return this.dataService.fetchPortfolioHoldings({ filters, - range: this.user?.settings?.dateRange + range: this.user()?.settings?.dateRange }); } @@ -171,9 +195,9 @@ export class GfHomeHoldingsComponent implements OnInit { this.isHoldingsViewModeToggleDisabled = false; this.holdingsViewMode = - this.deviceType === 'mobile' + this.deviceType() === 'mobile' ? GfHomeHoldingsComponent.DEFAULT_HOLDINGS_VIEW_MODE - : (this.user?.settings?.holdingsViewMode ?? + : (this.user()?.settings?.holdingsViewMode ?? GfHomeHoldingsComponent.DEFAULT_HOLDINGS_VIEW_MODE); } else { this.holdingsViewMode = diff --git a/apps/client/src/app/components/home-holdings/home-holdings.html b/apps/client/src/app/components/home-holdings/home-holdings.html index b55b1c54ef..7c533b2835 100644 --- a/apps/client/src/app/components/home-holdings/home-holdings.html +++ b/apps/client/src/app/components/home-holdings/home-holdings.html @@ -27,18 +27,18 @@ }
@if (hasPermissionToCreateActivity && (holdings?.length ?? 0) > 0) { diff --git a/apps/client/src/app/components/home-overview/home-overview.component.ts b/apps/client/src/app/components/home-overview/home-overview.component.ts index b677f88a39..3cecc5e5de 100644 --- a/apps/client/src/app/components/home-overview/home-overview.component.ts +++ b/apps/client/src/app/components/home-overview/home-overview.component.ts @@ -53,7 +53,6 @@ import { DeviceDetectorService } from 'ngx-device-detector'; templateUrl: './home-overview.html' }) export class GfHomeOverviewComponent implements OnInit { - protected readonly DEFAULT_LOCALE = DEFAULT_LOCALE; protected readonly errors = signal([]); protected readonly hasImpersonationId = signal(false); protected readonly historicalDataItems = signal(null); @@ -61,6 +60,11 @@ export class GfHomeOverviewComponent implements OnInit { undefined ); protected readonly isLoadingPerformance = signal(true); + + protected readonly locale = computed(() => { + return this.user()?.settings?.locale ?? DEFAULT_LOCALE; + }); + protected readonly performance = signal(null); protected readonly performanceLabel = $localize`Performance`; protected readonly precision = signal(2); @@ -147,7 +151,7 @@ export class GfHomeOverviewComponent implements OnInit { protected onHoldingClicked({ dataSource, symbol }: AssetProfileIdentifier) { if (dataSource && symbol) { - this.router.navigate([], { + void this.router.navigate([], { queryParams: { dataSource, symbol, holdingDetailDialog: true } }); } diff --git a/apps/client/src/app/components/home-overview/home-overview.html b/apps/client/src/app/components/home-overview/home-overview.html index 7648777463..6a166f93fd 100644 --- a/apps/client/src/app/components/home-overview/home-overview.html +++ b/apps/client/src/app/components/home-overview/home-overview.html @@ -11,7 +11,10 @@

Welcome to Ghostfolio

Ready to take control of your personal finances?

    -
  1. +
  2. Setup your accounts
    Setup accounts
    - } @else if (user()?.accounts?.length > 1) { + } @else if ((user()?.accounts?.length ?? 0) > 1) { diff --git a/apps/client/src/app/components/portfolio-performance/portfolio-performance.component.ts b/apps/client/src/app/components/portfolio-performance/portfolio-performance.component.ts index 4f7f35dd1e..aade357b78 100644 --- a/apps/client/src/app/components/portfolio-performance/portfolio-performance.component.ts +++ b/apps/client/src/app/components/portfolio-performance/portfolio-performance.component.ts @@ -35,9 +35,9 @@ import { NgxSkeletonLoaderModule } from 'ngx-skeleton-loader'; }) export class GfPortfolioPerformanceComponent { public readonly errors = input(); - public readonly isLoading = input(); + public readonly isLoading = input(false); public readonly locale = input(getLocale()); - public readonly performance = input.required(); + public readonly performance = input.required(); public readonly precision = input.required({ transform: (value) => { return value >= 0 ? value : 2; @@ -60,17 +60,19 @@ export class GfPortfolioPerformanceComponent { this.value().nativeElement.innerHTML = ''; } } else { - if (isNumber(this.performance().currentValueInBaseCurrency)) { - new CountUp('value', this.performance().currentValueInBaseCurrency, { + const performance = this.performance(); + + if (isNumber(performance?.currentValueInBaseCurrency)) { + new CountUp('value', performance.currentValueInBaseCurrency, { decimal: getNumberFormatDecimal(this.locale()), decimalPlaces: this.precision(), duration: 1, separator: getNumberFormatGroup(this.locale()) }).start(); - } else if (this.showDetails() === false) { + } else if (this.showDetails() === false && performance) { new CountUp( 'value', - this.performance().netPerformancePercentageWithCurrencyEffect * 100, + performance.netPerformancePercentageWithCurrencyEffect * 100, { decimal: getNumberFormatDecimal(this.locale()), decimalPlaces: 2, diff --git a/libs/ui/src/lib/line-chart/line-chart.component.ts b/libs/ui/src/lib/line-chart/line-chart.component.ts index e6f1b9b2e2..d910033bc0 100644 --- a/libs/ui/src/lib/line-chart/line-chart.component.ts +++ b/libs/ui/src/lib/line-chart/line-chart.component.ts @@ -60,7 +60,7 @@ export class GfLineChartComponent @Input() benchmarkLabel = ''; @Input() colorScheme?: ColorScheme; @Input() currency?: string; - @Input() historicalDataItems: LineChartItem[]; + @Input() historicalDataItems: LineChartItem[] | null; @Input() isAnimated = false; @Input() label: string; @Input() locale = getLocale(); From ce042c1cd7b57ff9f1d7f9bd2e411519843ec357 Mon Sep 17 00:00:00 2001 From: Kenrick Tandrian <60643640+KenTandrian@users.noreply.github.com> Date: Sat, 10 Oct 2026 17:29:26 +0700 Subject: [PATCH 03/14] Task/resolve strict template errors in shell and dialogs (#8102) Resolve strict template errors --- apps/client/src/app/app.component.html | 2 +- .../create-or-update-platform-dialog.html | 8 +- .../components/footer/footer.component.html | 18 ++--- .../app/components/footer/footer.component.ts | 80 ++++++++++--------- .../components/header/header.component.html | 10 +-- .../user-account-membership.html | 4 +- .../create-or-update-account-dialog.html | 8 +- apps/client/src/app/util/date.util.ts | 7 +- 8 files changed, 69 insertions(+), 68 deletions(-) diff --git a/apps/client/src/app/app.component.html b/apps/client/src/app/app.component.html index 61d5023e2d..5e2b658a28 100644 --- a/apps/client/src/app/app.component.html +++ b/apps/client/src/app/app.component.html @@ -48,6 +48,6 @@ @if (showFooter) {
    - +
    } diff --git a/apps/client/src/app/components/admin-platform/create-or-update-platform-dialog/create-or-update-platform-dialog.html b/apps/client/src/app/components/admin-platform/create-or-update-platform-dialog/create-or-update-platform-dialog.html index f923b95a6e..4109fd3cb4 100644 --- a/apps/client/src/app/components/admin-platform/create-or-update-platform-dialog/create-or-update-platform-dialog.html +++ b/apps/client/src/app/components/admin-platform/create-or-update-platform-dialog/create-or-update-platform-dialog.html @@ -28,12 +28,8 @@ matInput (keydown.enter)="$event.stopPropagation()" /> - @if (platformForm.get('url')?.value) { - + @if (platformForm.get('url')?.value; as url) { + }
diff --git a/apps/client/src/app/components/footer/footer.component.html b/apps/client/src/app/components/footer/footer.component.html index 5f0016317e..6dc44f35ee 100644 --- a/apps/client/src/app/components/footer/footer.component.html +++ b/apps/client/src/app/components/footer/footer.component.html @@ -6,7 +6,7 @@
Personal Finance
    - @if (hasPermissionToAccessFearAndGreedIndex) { + @if (hasPermissionToAccessFearAndGreedIndex()) {
  • Markets
  • @@ -18,7 +18,7 @@
    Ghostfolio
    • About
    • - @if (hasPermissionForSubscription) { + @if (hasPermissionForSubscription()) {
    • Blog
    • @@ -27,43 +27,43 @@ Changelog
    • Features
    • - @if (hasPermissionForSubscription) { + @if (hasPermissionForSubscription()) {
    • Frequently Asked Questions (FAQ)
    • } - @if (!hasPermissionForSubscription) { + @if (!hasPermissionForSubscription()) {
    • License
    • } - @if (hasPermissionForStatistics) { + @if (hasPermissionForStatistics()) {
    • Open Startup
    • } - @if (hasPermissionForSubscription) { + @if (hasPermissionForSubscription()) {
    • Pricing
    • } - @if (hasPermissionForSubscription) { + @if (hasPermissionForSubscription()) {
    • Privacy Policy
    • } - @if (hasPermissionForSubscription) { + @if (hasPermissionForSubscription()) {
    • Terms of Service
    • } - @if (hasPermissionForSubscription) { + @if (hasPermissionForSubscription()) {
    • (); - public currentYear = new Date().getFullYear(); - public hasPermissionForStatistics: boolean; - public hasPermissionForSubscription: boolean; - public hasPermissionToAccessFearAndGreedIndex: boolean; - public routerLinkAbout = publicRoutes.about.routerLink; - public routerLinkAboutChangelog = - publicRoutes.about.subRoutes.changelog.routerLink; - public routerLinkAboutLicense = - publicRoutes.about.subRoutes.license.routerLink; - public routerLinkAboutPrivacyPolicy = - publicRoutes.about.subRoutes.privacyPolicy.routerLink; - public routerLinkAboutTermsOfService = - publicRoutes.about.subRoutes.termsOfService.routerLink; - public routerLinkBlog = publicRoutes.blog.routerLink; - public routerLinkFaq = publicRoutes.faq.routerLink; - public routerLinkFeatures = publicRoutes.features.routerLink; - public routerLinkMarkets = publicRoutes.markets.routerLink; - public routerLinkOpenStartup = publicRoutes.openStartup.routerLink; - public routerLinkPricing = publicRoutes.pricing.routerLink; - public routerLinkResources = publicRoutes.resources.routerLink; - - public constructor() { - addIcons({ - openOutline - }); - } + protected readonly currentYear = new Date().getFullYear(); - public ngOnChanges() { - this.hasPermissionForStatistics = hasPermission( - this.info?.globalPermissions, + protected readonly hasPermissionForStatistics = computed(() => { + return hasPermission( + this.info()?.globalPermissions, permissions.enableStatistics ); + }); - this.hasPermissionForSubscription = hasPermission( - this.info?.globalPermissions, + protected readonly hasPermissionForSubscription = computed(() => { + return hasPermission( + this.info()?.globalPermissions, permissions.enableSubscription ); + }); - this.hasPermissionToAccessFearAndGreedIndex = hasPermission( - this.info?.globalPermissions, + protected readonly hasPermissionToAccessFearAndGreedIndex = computed(() => { + return hasPermission( + this.info()?.globalPermissions, permissions.enableFearAndGreedIndex ); + }); + + protected readonly routerLinkAbout = publicRoutes.about.routerLink; + protected readonly routerLinkAboutChangelog = + publicRoutes.about.subRoutes.changelog.routerLink; + protected readonly routerLinkAboutLicense = + publicRoutes.about.subRoutes.license.routerLink; + protected readonly routerLinkAboutPrivacyPolicy = + publicRoutes.about.subRoutes.privacyPolicy.routerLink; + protected readonly routerLinkAboutTermsOfService = + publicRoutes.about.subRoutes.termsOfService.routerLink; + protected readonly routerLinkBlog = publicRoutes.blog.routerLink; + protected readonly routerLinkFaq = publicRoutes.faq.routerLink; + protected readonly routerLinkFeatures = publicRoutes.features.routerLink; + protected readonly routerLinkMarkets = publicRoutes.markets.routerLink; + protected readonly routerLinkOpenStartup = + publicRoutes.openStartup.routerLink; + protected readonly routerLinkPricing = publicRoutes.pricing.routerLink; + protected readonly routerLinkResources = publicRoutes.resources.routerLink; + + public constructor() { + addIcons({ + openOutline + }); } } diff --git a/apps/client/src/app/components/header/header.component.html b/apps/client/src/app/components/header/header.component.html index eaccbb3326..48ed63ab34 100644 --- a/apps/client/src/app/components/header/header.component.html +++ b/apps/client/src/app/components/header/header.component.html @@ -124,7 +124,7 @@ >About
    • - @if (hasPermissionToAccessAssistant) { + @if (hasPermissionToAccessAssistant && user(); as user) {

Fully managed Ghostfolio cloud offering.

-

- - @if (coupon) { - {{ baseCurrency }} {{ price }} {{ baseCurrency }} {{ - price - coupon - }} - } @else { - {{ baseCurrency }} {{ price }} - } -   - per year -

+ @if (price) { +

+ + @if (coupon) { + {{ baseCurrency }} {{ price }} {{ baseCurrency }} {{ + price - coupon + }} + } @else { + {{ baseCurrency }} {{ price }} + } +   + per year +

+ } @if ( hasPermissionToUpdateUserSettings && user?.subscription?.type === 'Basic' diff --git a/apps/client/tsconfig.json b/apps/client/tsconfig.json index a6815ec125..b009454a0d 100644 --- a/apps/client/tsconfig.json +++ b/apps/client/tsconfig.json @@ -14,20 +14,16 @@ } ], "angularCompilerOptions": { + "extendedDiagnostics": { + "checks": { + // TODO: Remove once the fields are typed as optional + "nullishCoalescingNotNullable": "suppress", + "optionalChainNotNullable": "suppress" + } + }, "strictInjectionParameters": true, - // TODO: Remove these subsets once strictTemplates is enabled - "strictAttributeTypes": true, - "strictContextGenerics": true, - "strictDomEventTypes": true, - "strictDomLocalRefTypes": true, "strictInputAccessModifiers": true, - "strictInputTypes": true, - "strictLiteralTypes": true, - "strictNullInputTypes": true, - "strictOutputEventTypes": true, - "strictSafeNavigationTypes": true, - // TODO: Enable stricter rules for this project - "strictTemplates": false + "strictTemplates": true }, "compilerOptions": { "module": "preserve",