diff --git a/CHANGELOG.md b/CHANGELOG.md index 6d0224aa2..af2063666 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,9 +9,12 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### 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 +- 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 @@ -20,6 +23,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Changed +- Harmonized the date picker styling across various components - Updated the _Privacy Policy_ - Updated the _Terms of Service_ - 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 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 a recurring issue where single-value fields were incorrectly validated as arrays in various endpoints diff --git a/apps/api/src/app/endpoints/public/public.controller.ts b/apps/api/src/app/endpoints/public/public.controller.ts index d06e3b5fe..f43e51f7b 100644 --- a/apps/api/src/app/endpoints/public/public.controller.ts +++ b/apps/api/src/app/endpoints/public/public.controller.ts @@ -46,7 +46,10 @@ export class PublicController { public async getPublicPortfolio( @Param('accessId') accessId: string ): Promise { - const access = await this.accessService.access({ id: accessId }); + const access = await this.accessService.access({ + granteeUserId: null, + id: accessId + }); if (!access) { throw new HttpException( diff --git a/apps/api/src/services/benchmark/benchmark.service.spec.ts b/apps/api/src/services/benchmark/benchmark.service.spec.ts index 833dbcdfc..0e7119b04 100644 --- a/apps/api/src/services/benchmark/benchmark.service.spec.ts +++ b/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, 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'); + }); }); diff --git a/apps/api/src/services/benchmark/benchmark.service.ts b/apps/api/src/services/benchmark/benchmark.service.ts index 56a629163..99ceaf21e 100644 --- a/apps/api/src/services/benchmark/benchmark.service.ts +++ b/apps/api/src/services/benchmark/benchmark.service.ts @@ -24,7 +24,7 @@ import { Injectable, Logger } from '@nestjs/common'; import { SymbolProfile } from '@prisma/client'; import { Big } from 'big.js'; import { addHours, isAfter, subDays } from 'date-fns'; -import { uniqBy } from 'lodash'; +import { round, uniqBy } from 'lodash'; import ms from 'ms'; import { BenchmarkValue } from './interfaces/benchmark-value.interface'; @@ -220,9 +220,11 @@ export class BenchmarkService { public getMarketCondition( aPerformanceInPercent: number ): Benchmark['marketCondition'] { - if (aPerformanceInPercent >= 0) { + const performanceInPercent = round(aPerformanceInPercent, 4); + + if (performanceInPercent >= 0) { return 'ALL_TIME_HIGH'; - } else if (aPerformanceInPercent <= -0.2) { + } else if (performanceInPercent <= -0.2) { return 'BEAR_MARKET'; } else { return 'NEUTRAL_MARKET'; diff --git a/apps/client/src/app/components/header/header.component.html b/apps/client/src/app/components/header/header.component.html index ae3121861..35f072d72 100644 --- a/apps/client/src/app/components/header/header.component.html +++ b/apps/client/src/app/components/header/header.component.html @@ -332,7 +332,7 @@ } - @if (user() === null) { + @if (!user()) {
The source code is fully available as open source software @@ -49,6 +50,7 @@ >and is driven by the efforts of its contributorsSlack community, post to @ghostfolio_ this.deviceDetectorService.deviceInfo().deviceType + ); + + private readonly changeDetectorRef = inject(ChangeDetectorRef); + private readonly dataService = inject(DataService); + private readonly destroyRef = inject(DestroyRef); + private readonly deviceDetectorService = inject(DeviceDetectorService); + private readonly dialog = inject(MatDialog); + private readonly impersonationStorageService = inject( + ImpersonationStorageService + ); + 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 .pipe(takeUntilDestroyed(this.destroyRef)) .subscribe((params) => { @@ -80,7 +86,9 @@ export class GfAccountsPageComponent implements OnInit { return id === params['accountId']; }); - this.openUpdateAccountDialog(account); + if (account) { + this.openUpdateAccountDialog(account); + } } else { this.router.navigate(['.'], { relativeTo: this.route }); } @@ -91,8 +99,6 @@ export class GfAccountsPageComponent implements OnInit { } public ngOnInit() { - this.deviceType = this.deviceDetectorService.getDeviceInfo().deviceType; - this.impersonationStorageService .onChangeHasImpersonation() .pipe(takeUntilDestroyed(this.destroyRef)) @@ -122,32 +128,7 @@ export class GfAccountsPageComponent implements OnInit { this.fetchAccounts(); } - public 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(); - } - ); - } - - public onDeleteAccount(aId: string) { + protected onDeleteAccount(aId: string) { this.reset(); this.dataService @@ -163,19 +144,44 @@ export class GfAccountsPageComponent implements OnInit { }); } - public onTransferBalance() { + protected onTransferBalance() { this.router.navigate([], { queryParams: { transferBalanceDialog: true } }); } - public onUpdateAccount(aAccount: AccountModel) { + protected onUpdateAccount(aAccount: AccountModel) { this.router.navigate([], { 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, comment, currency, @@ -199,8 +205,8 @@ export class GfAccountsPageComponent implements OnInit { platformId } }, - height: this.deviceType === 'mobile' ? '98vh' : '80vh', - width: this.deviceType === 'mobile' ? '100vw' : '50rem' + height: this.deviceType() === 'mobile' ? '98vh' : '80vh', + width: this.deviceType() === 'mobile' ? '100vw' : '50rem' }); dialogRef @@ -237,15 +243,15 @@ export class GfAccountsPageComponent implements OnInit { autoFocus: false, data: { accountId: aAccountId, - deviceType: this.deviceType, + deviceType: this.deviceType(), hasImpersonationId: this.hasImpersonationId, hasPermissionToCreateActivity: !this.hasImpersonationId && hasPermission(this.user?.permissions, permissions.createActivity) && !this.user?.settings?.isRestrictedView }, - height: this.deviceType === 'mobile' ? '98vh' : '80vh', - width: this.deviceType === 'mobile' ? '100vw' : '50rem' + height: this.deviceType() === 'mobile' ? '98vh' : '80vh', + width: this.deviceType() === 'mobile' ? '100vw' : '50rem' }); dialogRef @@ -267,15 +273,15 @@ export class GfAccountsPageComponent implements OnInit { account: { balance: 0, comment: null, - currency: this.user?.settings?.baseCurrency, + currency: this.user?.settings?.baseCurrency ?? null, id: null, isExcluded: false, name: null, platformId: null } - }, - height: this.deviceType === 'mobile' ? '98vh' : '80vh', - width: this.deviceType === 'mobile' ? '100vw' : '50rem' + } satisfies CreateOrUpdateAccountDialogParams, + height: this.deviceType() === 'mobile' ? '98vh' : '80vh', + width: this.deviceType() === 'mobile' ? '100vw' : '50rem' }); dialogRef @@ -312,7 +318,7 @@ export class GfAccountsPageComponent implements OnInit { data: { accounts: this.accounts }, - width: this.deviceType === 'mobile' ? '100vw' : '50rem' + width: this.deviceType() === 'mobile' ? '100vw' : '50rem' }); dialogRef @@ -353,7 +359,7 @@ export class GfAccountsPageComponent implements OnInit { } private reset() { - this.accounts = undefined; + this.accounts = []; this.activitiesCount = 0; this.totalBalanceInBaseCurrency = 0; this.totalValueInBaseCurrency = 0; diff --git a/apps/client/src/app/pages/accounts/create-or-update-account-dialog/interfaces/interfaces.ts b/apps/client/src/app/pages/accounts/create-or-update-account-dialog/interfaces/interfaces.ts index a3e6272f8..469ebc844 100644 --- a/apps/client/src/app/pages/accounts/create-or-update-account-dialog/interfaces/interfaces.ts +++ b/apps/client/src/app/pages/accounts/create-or-update-account-dialog/interfaces/interfaces.ts @@ -1,5 +1,7 @@ import { Account } from '@prisma/client'; export interface CreateOrUpdateAccountDialogParams { - account: Omit; + account: Omit & { + id: string | null; + }; } diff --git a/apps/client/src/app/pages/landing/landing-page.html b/apps/client/src/app/pages/landing/landing-page.html index dc0df10d4..cb86b471d 100644 --- a/apps/client/src/app/pages/landing/landing-page.html +++ b/apps/client/src/app/pages/landing/landing-page.html @@ -14,6 +14,7 @@
@@ -58,6 +59,7 @@ > @@ -76,6 +78,7 @@ > @@ -94,6 +97,7 @@ > diff --git a/apps/client/src/app/pages/open/open-page.html b/apps/client/src/app/pages/open/open-page.html index e7ff2719c..7ba6c1f4c 100644 --- a/apps/client/src/app/pages/open/open-page.html +++ b/apps/client/src/app/pages/open/open-page.html @@ -8,6 +8,7 @@ the source code as open source software diff --git a/apps/client/src/app/pages/portfolio/activities/create-or-update-activity-dialog/create-or-update-activity-dialog.html b/apps/client/src/app/pages/portfolio/activities/create-or-update-activity-dialog/create-or-update-activity-dialog.html index 20c50d0fe..b5dbf4669 100644 --- a/apps/client/src/app/pages/portfolio/activities/create-or-update-activity-dialog/create-or-update-activity-dialog.html +++ b/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" /> - +
- + + [for]="date" + > + + diff --git a/libs/ui/src/lib/fire-calculator/fire-calculator.component.ts b/libs/ui/src/lib/fire-calculator/fire-calculator.component.ts index c8e281609..fa998778d 100644 --- a/libs/ui/src/lib/fire-calculator/fire-calculator.component.ts +++ b/libs/ui/src/lib/fire-calculator/fire-calculator.component.ts @@ -34,6 +34,7 @@ import { } from '@angular/material/datepicker'; import { MatFormFieldModule } from '@angular/material/form-field'; import { MatInputModule } from '@angular/material/input'; +import { IonIcon } from '@ionic/angular/standalone'; import { BarController, BarElement, @@ -56,6 +57,8 @@ import { startOfMonth, sub } from 'date-fns'; +import { addIcons } from 'ionicons'; +import { calendarClearOutline } from 'ionicons/icons'; import { isNumber } from 'lodash'; import { NgxSkeletonLoaderModule } from 'ngx-skeleton-loader'; import { debounceTime } from 'rxjs'; @@ -67,6 +70,7 @@ import { FireCalculatorService } from './fire-calculator.service'; imports: [ CommonModule, FormsModule, + IonIcon, MatButtonModule, MatDatepickerModule, MatFormFieldModule, @@ -136,6 +140,8 @@ export class GfFireCalculatorComponent implements OnChanges, OnDestroy { Tooltip ); + addIcons({ calendarClearOutline }); + this.calculatorForm.valueChanges .pipe(takeUntilDestroyed(this.destroyRef)) .subscribe(() => { diff --git a/libs/ui/src/lib/historical-market-data-editor/historical-market-data-editor-dialog/historical-market-data-editor-dialog.html b/libs/ui/src/lib/historical-market-data-editor/historical-market-data-editor-dialog/historical-market-data-editor-dialog.html index 7bb5827ef..7e8183664 100644 --- a/libs/ui/src/lib/historical-market-data-editor/historical-market-data-editor-dialog/historical-market-data-editor-dialog.html +++ b/libs/ui/src/lib/historical-market-data-editor/historical-market-data-editor-dialog/historical-market-data-editor-dialog.html @@ -35,6 +35,7 @@