Browse Source

Merge branch 'main' into task/refactor-rounding-logic-in-treemap-chart-component

pull/7200/head
Thomas Kaul 2 weeks ago
committed by GitHub
parent
commit
8d7d226f76
No known key found for this signature in database GPG Key ID: B5690EEEBB952194
  1. 9
      CHANGELOG.md
  2. 5
      apps/api/src/app/endpoints/public/public.controller.ts
  3. 12
      apps/api/src/services/benchmark/benchmark.service.spec.ts
  4. 8
      apps/api/src/services/benchmark/benchmark.service.ts
  5. 2
      apps/client/src/app/components/header/header.component.html
  6. 4
      apps/client/src/app/pages/about/overview/about-overview-page.html
  7. 138
      apps/client/src/app/pages/accounts/accounts-page.component.ts
  8. 4
      apps/client/src/app/pages/accounts/create-or-update-account-dialog/interfaces/interfaces.ts
  9. 4
      apps/client/src/app/pages/landing/landing-page.html
  10. 1
      apps/client/src/app/pages/open/open-page.html
  11. 3
      apps/client/src/app/pages/portfolio/activities/create-or-update-activity-dialog/create-or-update-activity-dialog.html
  12. 1
      apps/client/src/app/pages/resources/personal-finance-tools/personal-finance-tools-page.html
  13. 2
      libs/ui/src/lib/account-balances/account-balances.component.html
  14. 1
      libs/ui/src/lib/accounts-table/accounts-table.component.html
  15. 1
      libs/ui/src/lib/activities-table/activities-table.component.html
  16. 10
      libs/ui/src/lib/benchmark/benchmark.component.html
  17. 3
      libs/ui/src/lib/benchmark/benchmark.component.ts
  18. 17
      libs/ui/src/lib/fire-calculator/fire-calculator.component.html
  19. 6
      libs/ui/src/lib/fire-calculator/fire-calculator.component.ts
  20. 1
      libs/ui/src/lib/historical-market-data-editor/historical-market-data-editor-dialog/historical-market-data-editor-dialog.html
  21. 4
      libs/ui/src/lib/holdings-table/holdings-table.component.html
  22. 1
      libs/ui/src/lib/premium-indicator/premium-indicator.component.html
  23. 2
      libs/ui/src/lib/top-holdings/top-holdings.component.html
  24. 4
      libs/ui/src/lib/value/value.component.ts
  25. 12
      package-lock.json
  26. 4
      package.json

9
CHANGELOG.md

@ -9,9 +9,12 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
### Changed ### Changed
- Hardened the endpoint of the public access for portfolio sharing by restricting it to public accesses
- Improved the language localization by translating various tooltips across the application
- Refactored the rounding logic in the treemap chart component - Refactored the rounding logic in the treemap chart component
- Upgraded `yahoo-finance2` from version `3.14.3` to `3.15.4`
## 3.19.0 - 2026-07-02 ## 3.19.1 - 2026-07-03
### Added ### Added
@ -20,6 +23,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
### Changed ### Changed
- Harmonized the date picker styling across various components
- Updated the _Privacy Policy_ - Updated the _Privacy Policy_
- Updated the _Terms of Service_ - Updated the _Terms of Service_
- Improved the parsing of integer query parameters (`skip` and `take`) in the `GET api/v1/activities` endpoint - Improved the parsing of integer query parameters (`skip` and `take`) in the `GET api/v1/activities` endpoint
@ -30,6 +34,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
### Fixed ### Fixed
- Fixed an issue where values incorrectly rounded to negative zero in the value component
- Fixed the colorization of the change from all time high in the benchmark component when values round to zero
- Fixed the market condition of the benchmarks when values round to zero
- Fixed the validation of the data source field of an asset profile with market data - Fixed the validation of the data source field of an asset profile with market data
- Fixed a recurring issue where single-value fields were incorrectly validated as arrays in various endpoints - Fixed a recurring issue where single-value fields were incorrectly validated as arrays in various endpoints

5
apps/api/src/app/endpoints/public/public.controller.ts

@ -46,7 +46,10 @@ export class PublicController {
public async getPublicPortfolio( public async getPublicPortfolio(
@Param('accessId') accessId: string @Param('accessId') accessId: string
): Promise<PublicPortfolioResponse> { ): Promise<PublicPortfolioResponse> {
const access = await this.accessService.access({ id: accessId }); const access = await this.accessService.access({
granteeUserId: null,
id: accessId
});
if (!access) { if (!access) {
throw new HttpException( throw new HttpException(

12
apps/api/src/services/benchmark/benchmark.service.spec.ts

@ -12,4 +12,16 @@ describe('BenchmarkService', () => {
expect(benchmarkService.calculateChangeInPercentage(2, 2)).toEqual(0); expect(benchmarkService.calculateChangeInPercentage(2, 2)).toEqual(0);
expect(benchmarkService.calculateChangeInPercentage(2, 1)).toEqual(-0.5); expect(benchmarkService.calculateChangeInPercentage(2, 1)).toEqual(-0.5);
}); });
it('getMarketCondition', async () => {
expect(benchmarkService.getMarketCondition(0)).toEqual('ALL_TIME_HIGH');
expect(benchmarkService.getMarketCondition(-5.90736454893e-9)).toEqual(
'ALL_TIME_HIGH'
);
expect(benchmarkService.getMarketCondition(-0.1)).toEqual('NEUTRAL_MARKET');
expect(benchmarkService.getMarketCondition(-0.19996)).toEqual(
'BEAR_MARKET'
);
expect(benchmarkService.getMarketCondition(-0.2)).toEqual('BEAR_MARKET');
});
}); });

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

@ -24,7 +24,7 @@ import { Injectable, Logger } from '@nestjs/common';
import { SymbolProfile } from '@prisma/client'; import { SymbolProfile } from '@prisma/client';
import { Big } from 'big.js'; import { Big } from 'big.js';
import { addHours, isAfter, subDays } from 'date-fns'; import { addHours, isAfter, subDays } from 'date-fns';
import { uniqBy } from 'lodash'; import { round, uniqBy } from 'lodash';
import ms from 'ms'; import ms from 'ms';
import { BenchmarkValue } from './interfaces/benchmark-value.interface'; import { BenchmarkValue } from './interfaces/benchmark-value.interface';
@ -220,9 +220,11 @@ export class BenchmarkService {
public getMarketCondition( public getMarketCondition(
aPerformanceInPercent: number aPerformanceInPercent: number
): Benchmark['marketCondition'] { ): Benchmark['marketCondition'] {
if (aPerformanceInPercent >= 0) { const performanceInPercent = round(aPerformanceInPercent, 4);
if (performanceInPercent >= 0) {
return 'ALL_TIME_HIGH'; return 'ALL_TIME_HIGH';
} else if (aPerformanceInPercent <= -0.2) { } else if (performanceInPercent <= -0.2) {
return 'BEAR_MARKET'; return 'BEAR_MARKET';
} else { } else {
return 'NEUTRAL_MARKET'; return 'NEUTRAL_MARKET';

2
apps/client/src/app/components/header/header.component.html

@ -332,7 +332,7 @@
</li> </li>
</ul> </ul>
} }
@if (user() === null) { @if (!user()) {
<div class="d-flex h-100 logo-container" [class.filled]="hasTabs()"> <div class="d-flex h-100 logo-container" [class.filled]="hasTabs()">
<a <a
class="align-items-center h-100 justify-content-start px-2 px-sm-3 rounded-0" class="align-items-center h-100 justify-content-start px-2 px-sm-3 rounded-0"

4
apps/client/src/app/pages/about/overview/about-overview-page.html

@ -16,6 +16,7 @@
>The source code is fully available as >The source code is fully available as
<a <a
href="https://github.com/ghostfolio/ghostfolio" href="https://github.com/ghostfolio/ghostfolio"
i18n-title
title="Find Ghostfolio on GitHub" title="Find Ghostfolio on GitHub"
>open source software</a >open source software</a
> >
@ -49,6 +50,7 @@
>and is driven by the efforts of its >and is driven by the efforts of its
<a <a
href="https://github.com/ghostfolio/ghostfolio/graphs/contributors" href="https://github.com/ghostfolio/ghostfolio/graphs/contributors"
i18n-title
title="Contributors to Ghostfolio" title="Contributors to Ghostfolio"
>contributors</a >contributors</a
></ng-container ></ng-container
@ -72,12 +74,14 @@
Ghostfolio Ghostfolio
<a <a
href="https://join.slack.com/t/ghostfolio/shared_invite/zt-vsaan64h-F_I0fEo5M0P88lP9ibCxFg" href="https://join.slack.com/t/ghostfolio/shared_invite/zt-vsaan64h-F_I0fEo5M0P88lP9ibCxFg"
i18n-title
title="Join the Ghostfolio Slack community" title="Join the Ghostfolio Slack community"
>Slack</a >Slack</a
> >
community, post to community, post to
<a <a
href="https://x.com/ghostfolio_" href="https://x.com/ghostfolio_"
i18n-title
title="Post to Ghostfolio on X (formerly Twitter)" title="Post to Ghostfolio on X (formerly Twitter)"
>&#64;ghostfolio_</a >&#64;ghostfolio_</a
></ng-container ></ng-container

138
apps/client/src/app/pages/accounts/accounts-page.component.ts

@ -17,7 +17,9 @@ import { DataService } from '@ghostfolio/ui/services';
import { import {
ChangeDetectorRef, ChangeDetectorRef,
Component, Component,
computed,
DestroyRef, DestroyRef,
inject,
OnInit OnInit
} from '@angular/core'; } from '@angular/core';
import { takeUntilDestroyed } from '@angular/core/rxjs-interop'; import { takeUntilDestroyed } from '@angular/core/rxjs-interop';
@ -25,7 +27,7 @@ import { MatDialog } from '@angular/material/dialog';
import { ActivatedRoute, Router, RouterModule } from '@angular/router'; import { ActivatedRoute, Router, RouterModule } from '@angular/router';
import { Account as AccountModel } from '@prisma/client'; import { Account as AccountModel } from '@prisma/client';
import { DeviceDetectorService } from 'ngx-device-detector'; import { DeviceDetectorService } from 'ngx-device-detector';
import { EMPTY, Subscription } from 'rxjs'; import { EMPTY } from 'rxjs';
import { catchError } from 'rxjs/operators'; import { catchError } from 'rxjs/operators';
import { GfCreateOrUpdateAccountDialogComponent } from './create-or-update-account-dialog/create-or-update-account-dialog.component'; import { GfCreateOrUpdateAccountDialogComponent } from './create-or-update-account-dialog/create-or-update-account-dialog.component';
@ -41,29 +43,33 @@ import { GfTransferBalanceDialogComponent } from './transfer-balance/transfer-ba
templateUrl: './accounts-page.html' templateUrl: './accounts-page.html'
}) })
export class GfAccountsPageComponent implements OnInit { export class GfAccountsPageComponent implements OnInit {
public accounts: AccountModel[]; protected accounts: AccountModel[];
public activitiesCount = 0; protected activitiesCount = 0;
public deviceType: string; protected hasImpersonationId: boolean;
public hasImpersonationId: boolean; protected hasPermissionToCreateAccount: boolean;
public hasPermissionToCreateAccount: boolean; protected hasPermissionToUpdateAccount: boolean;
public hasPermissionToUpdateAccount: boolean; protected totalBalanceInBaseCurrency = 0;
public routeQueryParams: Subscription; protected totalValueInBaseCurrency = 0;
public totalBalanceInBaseCurrency = 0; protected user: User;
public totalValueInBaseCurrency = 0;
public user: User; private readonly deviceType = computed(
() => this.deviceDetectorService.deviceInfo().deviceType
public constructor( );
private changeDetectorRef: ChangeDetectorRef,
private dataService: DataService, private readonly changeDetectorRef = inject(ChangeDetectorRef);
private destroyRef: DestroyRef, private readonly dataService = inject(DataService);
private deviceDetectorService: DeviceDetectorService, private readonly destroyRef = inject(DestroyRef);
private dialog: MatDialog, private readonly deviceDetectorService = inject(DeviceDetectorService);
private impersonationStorageService: ImpersonationStorageService, private readonly dialog = inject(MatDialog);
private notificationService: NotificationService, private readonly impersonationStorageService = inject(
private route: ActivatedRoute, ImpersonationStorageService
private router: Router, );
private userService: UserService private readonly notificationService = inject(NotificationService);
) { private readonly route = inject(ActivatedRoute);
private readonly router = inject(Router);
private readonly userService = inject(UserService);
public constructor() {
this.route.queryParams this.route.queryParams
.pipe(takeUntilDestroyed(this.destroyRef)) .pipe(takeUntilDestroyed(this.destroyRef))
.subscribe((params) => { .subscribe((params) => {
@ -80,7 +86,9 @@ export class GfAccountsPageComponent implements OnInit {
return id === params['accountId']; return id === params['accountId'];
}); });
if (account) {
this.openUpdateAccountDialog(account); this.openUpdateAccountDialog(account);
}
} else { } else {
this.router.navigate(['.'], { relativeTo: this.route }); this.router.navigate(['.'], { relativeTo: this.route });
} }
@ -91,8 +99,6 @@ export class GfAccountsPageComponent implements OnInit {
} }
public ngOnInit() { public ngOnInit() {
this.deviceType = this.deviceDetectorService.getDeviceInfo().deviceType;
this.impersonationStorageService this.impersonationStorageService
.onChangeHasImpersonation() .onChangeHasImpersonation()
.pipe(takeUntilDestroyed(this.destroyRef)) .pipe(takeUntilDestroyed(this.destroyRef))
@ -122,32 +128,7 @@ export class GfAccountsPageComponent implements OnInit {
this.fetchAccounts(); this.fetchAccounts();
} }
public fetchAccounts() { protected onDeleteAccount(aId: string) {
this.dataService
.fetchAccounts()
.pipe(takeUntilDestroyed(this.destroyRef))
.subscribe(
({
accounts,
activitiesCount,
totalBalanceInBaseCurrency,
totalValueInBaseCurrency
}) => {
this.accounts = accounts;
this.activitiesCount = activitiesCount;
this.totalBalanceInBaseCurrency = totalBalanceInBaseCurrency;
this.totalValueInBaseCurrency = totalValueInBaseCurrency;
if (this.accounts?.length <= 0) {
this.router.navigate([], { queryParams: { createDialog: true } });
}
this.changeDetectorRef.markForCheck();
}
);
}
public onDeleteAccount(aId: string) {
this.reset(); this.reset();
this.dataService this.dataService
@ -163,19 +144,44 @@ export class GfAccountsPageComponent implements OnInit {
}); });
} }
public onTransferBalance() { protected onTransferBalance() {
this.router.navigate([], { this.router.navigate([], {
queryParams: { transferBalanceDialog: true } queryParams: { transferBalanceDialog: true }
}); });
} }
public onUpdateAccount(aAccount: AccountModel) { protected onUpdateAccount(aAccount: AccountModel) {
this.router.navigate([], { this.router.navigate([], {
queryParams: { accountId: aAccount.id, editDialog: true } queryParams: { accountId: aAccount.id, editDialog: true }
}); });
} }
public openUpdateAccountDialog({ private fetchAccounts() {
this.dataService
.fetchAccounts()
.pipe(takeUntilDestroyed(this.destroyRef))
.subscribe(
({
accounts,
activitiesCount,
totalBalanceInBaseCurrency,
totalValueInBaseCurrency
}) => {
this.accounts = accounts;
this.activitiesCount = activitiesCount;
this.totalBalanceInBaseCurrency = totalBalanceInBaseCurrency;
this.totalValueInBaseCurrency = totalValueInBaseCurrency;
if (this.accounts?.length <= 0) {
this.router.navigate([], { queryParams: { createDialog: true } });
}
this.changeDetectorRef.markForCheck();
}
);
}
private openUpdateAccountDialog({
balance, balance,
comment, comment,
currency, currency,
@ -199,8 +205,8 @@ export class GfAccountsPageComponent implements OnInit {
platformId platformId
} }
}, },
height: this.deviceType === 'mobile' ? '98vh' : '80vh', height: this.deviceType() === 'mobile' ? '98vh' : '80vh',
width: this.deviceType === 'mobile' ? '100vw' : '50rem' width: this.deviceType() === 'mobile' ? '100vw' : '50rem'
}); });
dialogRef dialogRef
@ -237,15 +243,15 @@ export class GfAccountsPageComponent implements OnInit {
autoFocus: false, autoFocus: false,
data: { data: {
accountId: aAccountId, accountId: aAccountId,
deviceType: this.deviceType, deviceType: this.deviceType(),
hasImpersonationId: this.hasImpersonationId, hasImpersonationId: this.hasImpersonationId,
hasPermissionToCreateActivity: hasPermissionToCreateActivity:
!this.hasImpersonationId && !this.hasImpersonationId &&
hasPermission(this.user?.permissions, permissions.createActivity) && hasPermission(this.user?.permissions, permissions.createActivity) &&
!this.user?.settings?.isRestrictedView !this.user?.settings?.isRestrictedView
}, },
height: this.deviceType === 'mobile' ? '98vh' : '80vh', height: this.deviceType() === 'mobile' ? '98vh' : '80vh',
width: this.deviceType === 'mobile' ? '100vw' : '50rem' width: this.deviceType() === 'mobile' ? '100vw' : '50rem'
}); });
dialogRef dialogRef
@ -267,15 +273,15 @@ export class GfAccountsPageComponent implements OnInit {
account: { account: {
balance: 0, balance: 0,
comment: null, comment: null,
currency: this.user?.settings?.baseCurrency, currency: this.user?.settings?.baseCurrency ?? null,
id: null, id: null,
isExcluded: false, isExcluded: false,
name: null, name: null,
platformId: null platformId: null
} }
}, } satisfies CreateOrUpdateAccountDialogParams,
height: this.deviceType === 'mobile' ? '98vh' : '80vh', height: this.deviceType() === 'mobile' ? '98vh' : '80vh',
width: this.deviceType === 'mobile' ? '100vw' : '50rem' width: this.deviceType() === 'mobile' ? '100vw' : '50rem'
}); });
dialogRef dialogRef
@ -312,7 +318,7 @@ export class GfAccountsPageComponent implements OnInit {
data: { data: {
accounts: this.accounts accounts: this.accounts
}, },
width: this.deviceType === 'mobile' ? '100vw' : '50rem' width: this.deviceType() === 'mobile' ? '100vw' : '50rem'
}); });
dialogRef dialogRef
@ -353,7 +359,7 @@ export class GfAccountsPageComponent implements OnInit {
} }
private reset() { private reset() {
this.accounts = undefined; this.accounts = [];
this.activitiesCount = 0; this.activitiesCount = 0;
this.totalBalanceInBaseCurrency = 0; this.totalBalanceInBaseCurrency = 0;
this.totalValueInBaseCurrency = 0; this.totalValueInBaseCurrency = 0;

4
apps/client/src/app/pages/accounts/create-or-update-account-dialog/interfaces/interfaces.ts

@ -1,5 +1,7 @@
import { Account } from '@prisma/client'; import { Account } from '@prisma/client';
export interface CreateOrUpdateAccountDialogParams { export interface CreateOrUpdateAccountDialogParams {
account: Omit<Account, 'createdAt' | 'updatedAt' | 'userId'>; account: Omit<Account, 'createdAt' | 'id' | 'updatedAt' | 'userId'> & {
id: string | null;
};
} }

4
apps/client/src/app/pages/landing/landing-page.html

@ -14,6 +14,7 @@
<div class="mb-4"> <div class="mb-4">
<a <a
href="https://www.youtube.com/watch?v=yY6ObSQVJZk" href="https://www.youtube.com/watch?v=yY6ObSQVJZk"
i18n-title
target="_blank" target="_blank"
title="Watch the Ghostfol.io Trailer on YouTube" title="Watch the Ghostfol.io Trailer on YouTube"
> >
@ -58,6 +59,7 @@
> >
<a <a
class="d-block" class="d-block"
i18n-title
title="Ghostfolio in Numbers: Monthly Active Users (MAU)" title="Ghostfolio in Numbers: Monthly Active Users (MAU)"
[routerLink]="routerLinkOpenStartup" [routerLink]="routerLinkOpenStartup"
> >
@ -76,6 +78,7 @@
> >
<a <a
class="d-block" class="d-block"
i18n-title
title="Ghostfolio in Numbers: Stars on GitHub" title="Ghostfolio in Numbers: Stars on GitHub"
[routerLink]="routerLinkOpenStartup" [routerLink]="routerLinkOpenStartup"
> >
@ -94,6 +97,7 @@
> >
<a <a
class="d-block" class="d-block"
i18n-title
title="Ghostfolio in Numbers: Pulls on Docker Hub" title="Ghostfolio in Numbers: Pulls on Docker Hub"
[routerLink]="routerLinkOpenStartup" [routerLink]="routerLinkOpenStartup"
> >

1
apps/client/src/app/pages/open/open-page.html

@ -8,6 +8,7 @@
the source code as the source code as
<a <a
href="https://github.com/ghostfolio/ghostfolio" href="https://github.com/ghostfolio/ghostfolio"
i18n-title
title="Find Ghostfolio on GitHub" title="Find Ghostfolio on GitHub"
>open source software</a >open source software</a
> >

3
apps/client/src/app/pages/portfolio/activities/create-or-update-activity-dialog/create-or-update-activity-dialog.html

@ -167,7 +167,7 @@
name="calendar-clear-outline" name="calendar-clear-outline"
/> />
</mat-datepicker-toggle> </mat-datepicker-toggle>
<mat-datepicker #date disabled="false" /> <mat-datepicker #date />
</mat-form-field> </mat-form-field>
</div> </div>
<div <div
@ -233,6 +233,7 @@
) { ) {
<button <button
class="ml-2 mt-1 no-min-width" class="ml-2 mt-1 no-min-width"
i18n-title
mat-button mat-button
title="Apply current market price" title="Apply current market price"
type="button" type="button"

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

@ -28,6 +28,7 @@
<div class="flex-nowrap no-gutters row"> <div class="flex-nowrap no-gutters row">
<a <a
class="d-flex overflow-hidden p-3 w-100" class="d-flex overflow-hidden p-3 w-100"
i18n-title
title="Compare Ghostfolio to {{ title="Compare Ghostfolio to {{
personalFinanceTool.name personalFinanceTool.name
}} - {{ personalFinanceTool.slogan }}" }} - {{ personalFinanceTool.slogan }}"

2
libs/ui/src/lib/account-balances/account-balances.component.html

@ -22,7 +22,7 @@
[matDatepicker]="date" [matDatepicker]="date"
[max]="maxDate" [max]="maxDate"
/> />
<mat-datepicker-toggle matSuffix [for]="date"> <mat-datepicker-toggle class="mr-2" matSuffix [for]="date">
<ion-icon <ion-icon
class="text-muted" class="text-muted"
matDatepickerToggleIcon matDatepickerToggleIcon

1
libs/ui/src/lib/accounts-table/accounts-table.component.html

@ -281,6 +281,7 @@
@if (element.comment) { @if (element.comment) {
<button <button
class="mx-1 no-min-width px-2" class="mx-1 no-min-width px-2"
i18n-title
mat-button mat-button
title="Note" title="Note"
(click)="onOpenComment(element.comment); $event.stopPropagation()" (click)="onOpenComment(element.comment); $event.stopPropagation()"

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

@ -366,6 +366,7 @@
@if (element.comment) { @if (element.comment) {
<button <button
class="mx-1 no-min-width px-2" class="mx-1 no-min-width px-2"
i18n-title
mat-button mat-button
title="Note" title="Note"
(click)="onOpenComment(element.comment); $event.stopPropagation()" (click)="onOpenComment(element.comment); $event.stopPropagation()"

10
libs/ui/src/lib/benchmark/benchmark.component.html

@ -135,9 +135,15 @@
class="d-inline-block justify-content-end" class="d-inline-block justify-content-end"
[class]="{ [class]="{
'text-danger': 'text-danger':
element?.performances?.allTimeHigh?.performancePercent < 0, round(
element?.performances?.allTimeHigh?.performancePercent,
4
) < 0,
'text-success': 'text-success':
element?.performances?.allTimeHigh?.performancePercent === 0 round(
element?.performances?.allTimeHigh?.performancePercent,
4
) === 0
}" }"
[isPercent]="true" [isPercent]="true"
[locale]="locale()" [locale]="locale()"

3
libs/ui/src/lib/benchmark/benchmark.component.ts

@ -33,7 +33,7 @@ import { ActivatedRoute, Router, RouterModule } from '@angular/router';
import { IonIcon } from '@ionic/angular/standalone'; import { IonIcon } from '@ionic/angular/standalone';
import { addIcons } from 'ionicons'; import { addIcons } from 'ionicons';
import { ellipsisHorizontal, trashOutline } from 'ionicons/icons'; import { ellipsisHorizontal, trashOutline } from 'ionicons/icons';
import { isNumber } from 'lodash'; import { isNumber, round } from 'lodash';
import { NgxSkeletonLoaderModule } from 'ngx-skeleton-loader'; import { NgxSkeletonLoaderModule } from 'ngx-skeleton-loader';
import { GfEntityLogoComponent } from '../entity-logo/entity-logo.component'; import { GfEntityLogoComponent } from '../entity-logo/entity-logo.component';
@ -92,6 +92,7 @@ export class GfBenchmarkComponent {
protected isLoading = true; protected isLoading = true;
protected readonly isNumber = isNumber; protected readonly isNumber = isNumber;
protected readonly resolveMarketCondition = resolveMarketCondition; protected readonly resolveMarketCondition = resolveMarketCondition;
protected readonly round = round;
protected readonly translate = translate; protected readonly translate = translate;
private readonly destroyRef = inject(DestroyRef); private readonly destroyRef = inject(DestroyRef);

17
libs/ui/src/lib/fire-calculator/fire-calculator.component.html

@ -39,19 +39,26 @@
class="d-none" class="d-none"
formControlName="retirementDate" formControlName="retirementDate"
matInput matInput
[matDatepicker]="datepicker" [matDatepicker]="date"
[min]="minDate" [min]="minDate"
/> />
<mat-datepicker-toggle <mat-datepicker-toggle
matIconSuffix class="mr-2"
matSuffix
[disabled]="hasPermissionToUpdateUserSettings !== true" [disabled]="hasPermissionToUpdateUserSettings !== true"
[for]="datepicker" [for]="date"
>
<ion-icon
class="text-muted"
matDatepickerToggleIcon
name="calendar-clear-outline"
/> />
</mat-datepicker-toggle>
<mat-datepicker <mat-datepicker
#datepicker #date
startView="multi-year" startView="multi-year"
[disabled]="hasPermissionToUpdateUserSettings !== true" [disabled]="hasPermissionToUpdateUserSettings !== true"
(monthSelected)="setMonthAndYear($event, datepicker)" (monthSelected)="setMonthAndYear($event, date)"
/> />
</mat-form-field> </mat-form-field>

6
libs/ui/src/lib/fire-calculator/fire-calculator.component.ts

@ -34,6 +34,7 @@ import {
} from '@angular/material/datepicker'; } from '@angular/material/datepicker';
import { MatFormFieldModule } from '@angular/material/form-field'; import { MatFormFieldModule } from '@angular/material/form-field';
import { MatInputModule } from '@angular/material/input'; import { MatInputModule } from '@angular/material/input';
import { IonIcon } from '@ionic/angular/standalone';
import { import {
BarController, BarController,
BarElement, BarElement,
@ -56,6 +57,8 @@ import {
startOfMonth, startOfMonth,
sub sub
} from 'date-fns'; } from 'date-fns';
import { addIcons } from 'ionicons';
import { calendarClearOutline } from 'ionicons/icons';
import { isNumber } from 'lodash'; import { isNumber } from 'lodash';
import { NgxSkeletonLoaderModule } from 'ngx-skeleton-loader'; import { NgxSkeletonLoaderModule } from 'ngx-skeleton-loader';
import { debounceTime } from 'rxjs'; import { debounceTime } from 'rxjs';
@ -67,6 +70,7 @@ import { FireCalculatorService } from './fire-calculator.service';
imports: [ imports: [
CommonModule, CommonModule,
FormsModule, FormsModule,
IonIcon,
MatButtonModule, MatButtonModule,
MatDatepickerModule, MatDatepickerModule,
MatFormFieldModule, MatFormFieldModule,
@ -136,6 +140,8 @@ export class GfFireCalculatorComponent implements OnChanges, OnDestroy {
Tooltip Tooltip
); );
addIcons({ calendarClearOutline });
this.calculatorForm.valueChanges this.calculatorForm.valueChanges
.pipe(takeUntilDestroyed(this.destroyRef)) .pipe(takeUntilDestroyed(this.destroyRef))
.subscribe(() => { .subscribe(() => {

1
libs/ui/src/lib/historical-market-data-editor/historical-market-data-editor-dialog/historical-market-data-editor-dialog.html

@ -35,6 +35,7 @@
</mat-form-field> </mat-form-field>
<button <button
class="ml-2 mt-1 no-min-width" class="ml-2 mt-1 no-min-width"
i18n-title
mat-button mat-button
title="Fetch market price" title="Fetch market price"
(click)="onFetchSymbolForDate()" (click)="onFetchSymbolForDate()"

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

@ -120,7 +120,7 @@
mat-sort-header mat-sort-header
> >
<span class="d-none d-sm-block" i18n>Allocation</span> <span class="d-none d-sm-block" i18n>Allocation</span>
<span class="d-block d-sm-none" title="Allocation">%</span> <span class="d-block d-sm-none" i18n-title title="Allocation">%</span>
</th> </th>
<td *matCellDef="let element" class="px-1" mat-cell> <td *matCellDef="let element" class="px-1" mat-cell>
<div class="d-flex justify-content-end"> <div class="d-flex justify-content-end">
@ -164,7 +164,7 @@
mat-sort-header="netPerformancePercentWithCurrencyEffect" mat-sort-header="netPerformancePercentWithCurrencyEffect"
> >
<span class="d-none d-sm-block" i18n>Performance</span> <span class="d-none d-sm-block" i18n>Performance</span>
<span class="d-block d-sm-none" title="Performance">±</span> <span class="d-block d-sm-none" i18n-title title="Performance">±</span>
</th> </th>
<td *matCellDef="let element" class="px-1" mat-cell> <td *matCellDef="let element" class="px-1" mat-cell>
<div class="d-flex justify-content-end"> <div class="d-flex justify-content-end">

1
libs/ui/src/lib/premium-indicator/premium-indicator.component.html

@ -1,5 +1,6 @@
<a <a
class="align-items-center d-flex" class="align-items-center d-flex"
i18n-title
title="Upgrade to Ghostfolio Premium" title="Upgrade to Ghostfolio Premium"
[routerLink]="routerLinkPricing" [routerLink]="routerLinkPricing"
[style.pointer-events]="enableLink ? 'initial' : 'none'" [style.pointer-events]="enableLink ? 'initial' : 'none'"

2
libs/ui/src/lib/top-holdings/top-holdings.component.html

@ -38,7 +38,7 @@
<ng-container matColumnDef="allocationInPercentage" stickyEnd> <ng-container matColumnDef="allocationInPercentage" stickyEnd>
<th *matHeaderCellDef class="px-2 text-right" mat-header-cell> <th *matHeaderCellDef class="px-2 text-right" mat-header-cell>
<span class="d-none d-sm-block" i18n>Allocation</span> <span class="d-none d-sm-block" i18n>Allocation</span>
<span class="d-block d-sm-none" title="Allocation">%</span> <span class="d-block d-sm-none" i18n-title title="Allocation">%</span>
</th> </th>
<td *matCellDef="let element" class="px-2" mat-cell> <td *matCellDef="let element" class="px-2" mat-cell>
<div class="d-flex justify-content-end"> <div class="d-flex justify-content-end">

4
libs/ui/src/lib/value/value.component.ts

@ -168,7 +168,9 @@ export class GfValueComponent implements AfterViewInit, OnChanges {
} }
} }
if (this.formattedValue === '0.00') { if (/^-?0([.,]0*)?$/.test(this.formattedValue)) {
// Remove algebraic sign of values rounding to zero
this.formattedValue = this.formattedValue.replace(/^-/, '');
this.useAbsoluteValue = true; this.useAbsoluteValue = true;
} }
} }

12
package-lock.json

@ -1,12 +1,12 @@
{ {
"name": "ghostfolio", "name": "ghostfolio",
"version": "3.19.0", "version": "3.19.1",
"lockfileVersion": 3, "lockfileVersion": 3,
"requires": true, "requires": true,
"packages": { "packages": {
"": { "": {
"name": "ghostfolio", "name": "ghostfolio",
"version": "3.19.0", "version": "3.19.1",
"hasInstallScript": true, "hasInstallScript": true,
"license": "AGPL-3.0", "license": "AGPL-3.0",
"dependencies": { "dependencies": {
@ -95,7 +95,7 @@
"tablemark": "4.1.0", "tablemark": "4.1.0",
"twitter-api-v2": "1.29.0", "twitter-api-v2": "1.29.0",
"undici": "8.5.0", "undici": "8.5.0",
"yahoo-finance2": "3.15.3", "yahoo-finance2": "3.15.4",
"zod": "4.4.3", "zod": "4.4.3",
"zone.js": "0.16.1" "zone.js": "0.16.1"
}, },
@ -35734,9 +35734,9 @@
} }
}, },
"node_modules/yahoo-finance2": { "node_modules/yahoo-finance2": {
"version": "3.15.3", "version": "3.15.4",
"resolved": "https://registry.npmjs.org/yahoo-finance2/-/yahoo-finance2-3.15.3.tgz", "resolved": "https://registry.npmjs.org/yahoo-finance2/-/yahoo-finance2-3.15.4.tgz",
"integrity": "sha512-AFmVZ4ACg3QFT2a/hyJv4Scp2J45gm/6xetVgvUvXzZypqsqbgreXJad4i+qm0onj6yeCf2fPhvow7Eh0CwwzA==", "integrity": "sha512-90eOw76iqS//ksQGL4d/VcchbysnpWzFXVuiBtG7uuImlBDdxBA0BtccxCuTvVZDDu1aXm1TqBGc+MPr6wNkyQ==",
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"@deno/shim-deno": "~0.18.0", "@deno/shim-deno": "~0.18.0",

4
package.json

@ -1,6 +1,6 @@
{ {
"name": "ghostfolio", "name": "ghostfolio",
"version": "3.19.0", "version": "3.19.1",
"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",
@ -139,7 +139,7 @@
"tablemark": "4.1.0", "tablemark": "4.1.0",
"twitter-api-v2": "1.29.0", "twitter-api-v2": "1.29.0",
"undici": "8.5.0", "undici": "8.5.0",
"yahoo-finance2": "3.15.3", "yahoo-finance2": "3.15.4",
"zod": "4.4.3", "zod": "4.4.3",
"zone.js": "0.16.1" "zone.js": "0.16.1"
}, },

Loading…
Cancel
Save