Browse Source
Merge branch 'main' into translate-ukrainian
pull/7181/head
Thomas Kaul
2 weeks ago
committed by
GitHub
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
23 changed files with
79 additions and
23 deletions
-
CHANGELOG.md
-
apps/api/src/services/benchmark/benchmark.service.spec.ts
-
apps/api/src/services/benchmark/benchmark.service.ts
-
apps/client/src/app/components/header/header.component.html
-
apps/client/src/app/pages/about/overview/about-overview-page.html
-
apps/client/src/app/pages/landing/landing-page.html
-
apps/client/src/app/pages/open/open-page.html
-
apps/client/src/app/pages/portfolio/activities/create-or-update-activity-dialog/create-or-update-activity-dialog.html
-
apps/client/src/app/pages/resources/personal-finance-tools/personal-finance-tools-page.html
-
libs/ui/src/lib/account-balances/account-balances.component.html
-
libs/ui/src/lib/accounts-table/accounts-table.component.html
-
libs/ui/src/lib/activities-table/activities-table.component.html
-
libs/ui/src/lib/benchmark/benchmark.component.html
-
libs/ui/src/lib/benchmark/benchmark.component.ts
-
libs/ui/src/lib/fire-calculator/fire-calculator.component.html
-
libs/ui/src/lib/fire-calculator/fire-calculator.component.ts
-
libs/ui/src/lib/historical-market-data-editor/historical-market-data-editor-dialog/historical-market-data-editor-dialog.html
-
libs/ui/src/lib/holdings-table/holdings-table.component.html
-
libs/ui/src/lib/premium-indicator/premium-indicator.component.html
-
libs/ui/src/lib/top-holdings/top-holdings.component.html
-
libs/ui/src/lib/value/value.component.ts
-
package-lock.json
-
package.json
|
|
|
@ -9,9 +9,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 |
|
|
|
|
|
|
|
### Changed |
|
|
|
|
|
|
|
- Improved the language localization by translating various tooltips across the application |
|
|
|
- Improved the language localization for Ukrainian (`uk`) |
|
|
|
|
|
|
|
## 3.19.0 - 2026-07-02 |
|
|
|
## 3.19.1 - 2026-07-03 |
|
|
|
|
|
|
|
### Added |
|
|
|
|
|
|
|
@ -20,6 +21,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 +32,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 |
|
|
|
|
|
|
|
|
|
|
|
@ -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'); |
|
|
|
}); |
|
|
|
}); |
|
|
|
|
|
|
|
@ -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'; |
|
|
|
|
|
|
|
@ -332,7 +332,7 @@ |
|
|
|
</li> |
|
|
|
</ul> |
|
|
|
} |
|
|
|
@if (user() === null) { |
|
|
|
@if (!user()) { |
|
|
|
<div class="d-flex h-100 logo-container" [class.filled]="hasTabs()"> |
|
|
|
<a |
|
|
|
class="align-items-center h-100 justify-content-start px-2 px-sm-3 rounded-0" |
|
|
|
|
|
|
|
@ -16,6 +16,7 @@ |
|
|
|
>The source code is fully available as |
|
|
|
<a |
|
|
|
href="https://github.com/ghostfolio/ghostfolio" |
|
|
|
i18n-title |
|
|
|
title="Find Ghostfolio on GitHub" |
|
|
|
>open source software</a |
|
|
|
> |
|
|
|
@ -49,6 +50,7 @@ |
|
|
|
>and is driven by the efforts of its |
|
|
|
<a |
|
|
|
href="https://github.com/ghostfolio/ghostfolio/graphs/contributors" |
|
|
|
i18n-title |
|
|
|
title="Contributors to Ghostfolio" |
|
|
|
>contributors</a |
|
|
|
></ng-container |
|
|
|
@ -72,12 +74,14 @@ |
|
|
|
Ghostfolio |
|
|
|
<a |
|
|
|
href="https://join.slack.com/t/ghostfolio/shared_invite/zt-vsaan64h-F_I0fEo5M0P88lP9ibCxFg" |
|
|
|
i18n-title |
|
|
|
title="Join the Ghostfolio Slack community" |
|
|
|
>Slack</a |
|
|
|
> |
|
|
|
community, post to |
|
|
|
<a |
|
|
|
href="https://x.com/ghostfolio_" |
|
|
|
i18n-title |
|
|
|
title="Post to Ghostfolio on X (formerly Twitter)" |
|
|
|
>@ghostfolio_</a |
|
|
|
></ng-container |
|
|
|
|
|
|
|
@ -14,6 +14,7 @@ |
|
|
|
<div class="mb-4"> |
|
|
|
<a |
|
|
|
href="https://www.youtube.com/watch?v=yY6ObSQVJZk" |
|
|
|
i18n-title |
|
|
|
target="_blank" |
|
|
|
title="Watch the Ghostfol.io Trailer on YouTube" |
|
|
|
> |
|
|
|
@ -58,6 +59,7 @@ |
|
|
|
> |
|
|
|
<a |
|
|
|
class="d-block" |
|
|
|
i18n-title |
|
|
|
title="Ghostfolio in Numbers: Monthly Active Users (MAU)" |
|
|
|
[routerLink]="routerLinkOpenStartup" |
|
|
|
> |
|
|
|
@ -76,6 +78,7 @@ |
|
|
|
> |
|
|
|
<a |
|
|
|
class="d-block" |
|
|
|
i18n-title |
|
|
|
title="Ghostfolio in Numbers: Stars on GitHub" |
|
|
|
[routerLink]="routerLinkOpenStartup" |
|
|
|
> |
|
|
|
@ -94,6 +97,7 @@ |
|
|
|
> |
|
|
|
<a |
|
|
|
class="d-block" |
|
|
|
i18n-title |
|
|
|
title="Ghostfolio in Numbers: Pulls on Docker Hub" |
|
|
|
[routerLink]="routerLinkOpenStartup" |
|
|
|
> |
|
|
|
|
|
|
|
@ -8,6 +8,7 @@ |
|
|
|
the source code as |
|
|
|
<a |
|
|
|
href="https://github.com/ghostfolio/ghostfolio" |
|
|
|
i18n-title |
|
|
|
title="Find Ghostfolio on GitHub" |
|
|
|
>open source software</a |
|
|
|
> |
|
|
|
|
|
|
|
@ -167,7 +167,7 @@ |
|
|
|
name="calendar-clear-outline" |
|
|
|
/> |
|
|
|
</mat-datepicker-toggle> |
|
|
|
<mat-datepicker #date disabled="false" /> |
|
|
|
<mat-datepicker #date /> |
|
|
|
</mat-form-field> |
|
|
|
</div> |
|
|
|
<div |
|
|
|
@ -233,6 +233,7 @@ |
|
|
|
) { |
|
|
|
<button |
|
|
|
class="ml-2 mt-1 no-min-width" |
|
|
|
i18n-title |
|
|
|
mat-button |
|
|
|
title="Apply current market price" |
|
|
|
type="button" |
|
|
|
|
|
|
|
@ -28,6 +28,7 @@ |
|
|
|
<div class="flex-nowrap no-gutters row"> |
|
|
|
<a |
|
|
|
class="d-flex overflow-hidden p-3 w-100" |
|
|
|
i18n-title |
|
|
|
title="Compare Ghostfolio to {{ |
|
|
|
personalFinanceTool.name |
|
|
|
}} - {{ personalFinanceTool.slogan }}" |
|
|
|
|
|
|
|
@ -22,7 +22,7 @@ |
|
|
|
[matDatepicker]="date" |
|
|
|
[max]="maxDate" |
|
|
|
/> |
|
|
|
<mat-datepicker-toggle matSuffix [for]="date"> |
|
|
|
<mat-datepicker-toggle class="mr-2" matSuffix [for]="date"> |
|
|
|
<ion-icon |
|
|
|
class="text-muted" |
|
|
|
matDatepickerToggleIcon |
|
|
|
|
|
|
|
@ -281,6 +281,7 @@ |
|
|
|
@if (element.comment) { |
|
|
|
<button |
|
|
|
class="mx-1 no-min-width px-2" |
|
|
|
i18n-title |
|
|
|
mat-button |
|
|
|
title="Note" |
|
|
|
(click)="onOpenComment(element.comment); $event.stopPropagation()" |
|
|
|
|
|
|
|
@ -366,6 +366,7 @@ |
|
|
|
@if (element.comment) { |
|
|
|
<button |
|
|
|
class="mx-1 no-min-width px-2" |
|
|
|
i18n-title |
|
|
|
mat-button |
|
|
|
title="Note" |
|
|
|
(click)="onOpenComment(element.comment); $event.stopPropagation()" |
|
|
|
|
|
|
|
@ -135,9 +135,15 @@ |
|
|
|
class="d-inline-block justify-content-end" |
|
|
|
[class]="{ |
|
|
|
'text-danger': |
|
|
|
element?.performances?.allTimeHigh?.performancePercent < 0, |
|
|
|
round( |
|
|
|
element?.performances?.allTimeHigh?.performancePercent, |
|
|
|
4 |
|
|
|
) < 0, |
|
|
|
'text-success': |
|
|
|
element?.performances?.allTimeHigh?.performancePercent === 0 |
|
|
|
round( |
|
|
|
element?.performances?.allTimeHigh?.performancePercent, |
|
|
|
4 |
|
|
|
) === 0 |
|
|
|
}" |
|
|
|
[isPercent]="true" |
|
|
|
[locale]="locale()" |
|
|
|
|
|
|
|
@ -33,7 +33,7 @@ import { ActivatedRoute, Router, RouterModule } from '@angular/router'; |
|
|
|
import { IonIcon } from '@ionic/angular/standalone'; |
|
|
|
import { addIcons } from 'ionicons'; |
|
|
|
import { ellipsisHorizontal, trashOutline } from 'ionicons/icons'; |
|
|
|
import { isNumber } from 'lodash'; |
|
|
|
import { isNumber, round } from 'lodash'; |
|
|
|
import { NgxSkeletonLoaderModule } from 'ngx-skeleton-loader'; |
|
|
|
|
|
|
|
import { GfEntityLogoComponent } from '../entity-logo/entity-logo.component'; |
|
|
|
@ -92,6 +92,7 @@ export class GfBenchmarkComponent { |
|
|
|
protected isLoading = true; |
|
|
|
protected readonly isNumber = isNumber; |
|
|
|
protected readonly resolveMarketCondition = resolveMarketCondition; |
|
|
|
protected readonly round = round; |
|
|
|
protected readonly translate = translate; |
|
|
|
|
|
|
|
private readonly destroyRef = inject(DestroyRef); |
|
|
|
|
|
|
|
@ -39,19 +39,26 @@ |
|
|
|
class="d-none" |
|
|
|
formControlName="retirementDate" |
|
|
|
matInput |
|
|
|
[matDatepicker]="datepicker" |
|
|
|
[matDatepicker]="date" |
|
|
|
[min]="minDate" |
|
|
|
/> |
|
|
|
<mat-datepicker-toggle |
|
|
|
matIconSuffix |
|
|
|
class="mr-2" |
|
|
|
matSuffix |
|
|
|
[disabled]="hasPermissionToUpdateUserSettings !== true" |
|
|
|
[for]="datepicker" |
|
|
|
/> |
|
|
|
[for]="date" |
|
|
|
> |
|
|
|
<ion-icon |
|
|
|
class="text-muted" |
|
|
|
matDatepickerToggleIcon |
|
|
|
name="calendar-clear-outline" |
|
|
|
/> |
|
|
|
</mat-datepicker-toggle> |
|
|
|
<mat-datepicker |
|
|
|
#datepicker |
|
|
|
#date |
|
|
|
startView="multi-year" |
|
|
|
[disabled]="hasPermissionToUpdateUserSettings !== true" |
|
|
|
(monthSelected)="setMonthAndYear($event, datepicker)" |
|
|
|
(monthSelected)="setMonthAndYear($event, date)" |
|
|
|
/> |
|
|
|
</mat-form-field> |
|
|
|
|
|
|
|
|
|
|
|
@ -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(() => { |
|
|
|
|
|
|
|
@ -35,6 +35,7 @@ |
|
|
|
</mat-form-field> |
|
|
|
<button |
|
|
|
class="ml-2 mt-1 no-min-width" |
|
|
|
i18n-title |
|
|
|
mat-button |
|
|
|
title="Fetch market price" |
|
|
|
(click)="onFetchSymbolForDate()" |
|
|
|
|
|
|
|
@ -120,7 +120,7 @@ |
|
|
|
mat-sort-header |
|
|
|
> |
|
|
|
<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> |
|
|
|
<td *matCellDef="let element" class="px-1" mat-cell> |
|
|
|
<div class="d-flex justify-content-end"> |
|
|
|
@ -164,7 +164,7 @@ |
|
|
|
mat-sort-header="netPerformancePercentWithCurrencyEffect" |
|
|
|
> |
|
|
|
<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> |
|
|
|
<td *matCellDef="let element" class="px-1" mat-cell> |
|
|
|
<div class="d-flex justify-content-end"> |
|
|
|
|
|
|
|
@ -1,5 +1,6 @@ |
|
|
|
<a |
|
|
|
class="align-items-center d-flex" |
|
|
|
i18n-title |
|
|
|
title="Upgrade to Ghostfolio Premium" |
|
|
|
[routerLink]="routerLinkPricing" |
|
|
|
[style.pointer-events]="enableLink ? 'initial' : 'none'" |
|
|
|
|
|
|
|
@ -38,7 +38,7 @@ |
|
|
|
<ng-container matColumnDef="allocationInPercentage" stickyEnd> |
|
|
|
<th *matHeaderCellDef class="px-2 text-right" mat-header-cell> |
|
|
|
<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> |
|
|
|
<td *matCellDef="let element" class="px-2" mat-cell> |
|
|
|
<div class="d-flex justify-content-end"> |
|
|
|
|
|
|
|
@ -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; |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
@ -1,12 +1,12 @@ |
|
|
|
{ |
|
|
|
"name": "ghostfolio", |
|
|
|
"version": "3.19.0", |
|
|
|
"version": "3.19.1", |
|
|
|
"lockfileVersion": 3, |
|
|
|
"requires": true, |
|
|
|
"packages": { |
|
|
|
"": { |
|
|
|
"name": "ghostfolio", |
|
|
|
"version": "3.19.0", |
|
|
|
"version": "3.19.1", |
|
|
|
"hasInstallScript": true, |
|
|
|
"license": "AGPL-3.0", |
|
|
|
"dependencies": { |
|
|
|
|
|
|
|
@ -1,6 +1,6 @@ |
|
|
|
{ |
|
|
|
"name": "ghostfolio", |
|
|
|
"version": "3.19.0", |
|
|
|
"version": "3.19.1", |
|
|
|
"homepage": "https://ghostfol.io", |
|
|
|
"license": "AGPL-3.0", |
|
|
|
"repository": "https://github.com/ghostfolio/ghostfolio", |
|
|
|
|