Browse Source

Merge remote-tracking branch 'origin/main' into feature/extend-holdings-endpoint-for-cash

pull/5650/head
KenTandrian 4 weeks ago
parent
commit
e409c2d56f
  1. 5
      CHANGELOG.md
  2. 1
      apps/client/src/app/components/rule/rule-settings-dialog/interfaces/interfaces.ts
  3. 2
      apps/client/src/app/components/rule/rule-settings-dialog/rule-settings-dialog.component.ts
  4. 133
      apps/client/src/app/components/rule/rule-settings-dialog/rule-settings-dialog.html
  5. 2
      apps/client/src/app/pages/portfolio/activities/import-activities-dialog/import-activities-dialog.component.ts
  6. 63
      libs/ui/src/lib/assistant/assistant.component.ts
  7. 2
      libs/ui/src/lib/holdings-table/holdings-table.component.ts
  8. 4
      package-lock.json
  9. 2
      package.json

5
CHANGELOG.md

@ -5,7 +5,7 @@ All notable changes to this project will be documented in this file.
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
## Unreleased ## 2.206.0 - 2025-10-04
### Added ### Added
@ -13,6 +13,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
### Changed ### Changed
- Localized the number formatting in the settings dialog to customize the rule thresholds of the _X-ray_ page
- Improved the usability of the assistant by preselecting the first search result
- Improved the usability of the _Cancel_ / _Close_ buttons in the create watchlist item dialog - Improved the usability of the _Cancel_ / _Close_ buttons in the create watchlist item dialog
- Refactored the `fireWealth` from `number` type to a structured object in the summary of the portfolio details endpoint - Refactored the `fireWealth` from `number` type to a structured object in the summary of the portfolio details endpoint
- Refactored the _Open Startup_ (`/open`) page to standalone - Refactored the _Open Startup_ (`/open`) page to standalone
@ -22,6 +24,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
### Fixed ### Fixed
- Handled an exception in the get asset profile functionality of the _Financial Modeling Prep_ service - Handled an exception in the get asset profile functionality of the _Financial Modeling Prep_ service
- Added the missing `CommonModule` import in the import activities dialog
## 2.205.0 - 2025-10-01 ## 2.205.0 - 2025-10-01

1
apps/client/src/app/components/rule/rule-settings-dialog/interfaces/interfaces.ts

@ -5,6 +5,7 @@ import {
export interface IRuleSettingsDialogParams { export interface IRuleSettingsDialogParams {
categoryName: string; categoryName: string;
locale: string;
rule: PortfolioReportRule; rule: PortfolioReportRule;
settings: XRayRulesSettings['AccountClusterRiskCurrentInvestment']; settings: XRayRulesSettings['AccountClusterRiskCurrentInvestment'];
} }

2
apps/client/src/app/components/rule/rule-settings-dialog/rule-settings-dialog.component.ts

@ -1,4 +1,5 @@
import { XRayRulesSettings } from '@ghostfolio/common/interfaces'; import { XRayRulesSettings } from '@ghostfolio/common/interfaces';
import { GfValueComponent } from '@ghostfolio/ui/value';
import { CommonModule } from '@angular/common'; import { CommonModule } from '@angular/common';
import { Component, Inject } from '@angular/core'; import { Component, Inject } from '@angular/core';
@ -17,6 +18,7 @@ import { IRuleSettingsDialogParams } from './interfaces/interfaces';
imports: [ imports: [
CommonModule, CommonModule,
FormsModule, FormsModule,
GfValueComponent,
MatButtonModule, MatButtonModule,
MatDialogModule, MatDialogModule,
MatSliderModule MatSliderModule

133
apps/client/src/app/components/rule/rule-settings-dialog/rule-settings-dialog.html

@ -5,28 +5,30 @@
data.rule.configuration.thresholdMin && data.rule.configuration.thresholdMax data.rule.configuration.thresholdMin && data.rule.configuration.thresholdMax
) { ) {
<div class="w-100"> <div class="w-100">
<h6 class="mb-0"> <h6 class="d-flex mb-0">
<ng-container i18n>Threshold range</ng-container>: <ng-container i18n>Threshold range</ng-container>:
@if (data.rule.configuration.threshold.unit === '%') { <gf-value
{{ data.settings.thresholdMin | percent: '1.2-2' }} class="ml-1"
} @else { [isPercent]="data.rule.configuration.threshold.unit === '%'"
{{ data.settings.thresholdMin }} [locale]="data.locale"
} [precision]="2"
- [value]="data.settings.thresholdMin"
@if (data.rule.configuration.threshold.unit === '%') { />
{{ data.settings.thresholdMax | percent: '1.2-2' }} <span class="mx-1">-</span>
} @else { <gf-value
{{ data.settings.thresholdMax }} [isPercent]="data.rule.configuration.threshold.unit === '%'"
} [locale]="data.locale"
[precision]="2"
[value]="data.settings.thresholdMax"
/>
</h6> </h6>
<div class="align-items-center d-flex w-100"> <div class="align-items-center d-flex w-100">
@if (data.rule.configuration.threshold.unit === '%') { <gf-value
<label>{{ [isPercent]="data.rule.configuration.threshold.unit === '%'"
data.rule.configuration.threshold.min | percent: '1.2-2' [locale]="data.locale"
}}</label> [precision]="2"
} @else { [value]="data.rule.configuration.threshold.min"
<label>{{ data.rule.configuration.threshold.min }}</label> />
}
<mat-slider <mat-slider
class="flex-grow-1" class="flex-grow-1"
[max]="data.rule.configuration.threshold.max" [max]="data.rule.configuration.threshold.max"
@ -36,13 +38,12 @@
<input matSliderStartThumb [(ngModel)]="data.settings.thresholdMin" /> <input matSliderStartThumb [(ngModel)]="data.settings.thresholdMin" />
<input matSliderEndThumb [(ngModel)]="data.settings.thresholdMax" /> <input matSliderEndThumb [(ngModel)]="data.settings.thresholdMax" />
</mat-slider> </mat-slider>
@if (data.rule.configuration.threshold.unit === '%') { <gf-value
<label>{{ [isPercent]="data.rule.configuration.threshold.unit === '%'"
data.rule.configuration.threshold.max | percent: '1.2-2' [locale]="data.locale"
}}</label> [precision]="2"
} @else { [value]="data.rule.configuration.threshold.max"
<label>{{ data.rule.configuration.threshold.max }}</label> />
}
</div> </div>
</div> </div>
} @else { } @else {
@ -50,22 +51,23 @@
class="w-100" class="w-100"
[ngClass]="{ 'd-none': !data.rule.configuration.thresholdMin }" [ngClass]="{ 'd-none': !data.rule.configuration.thresholdMin }"
> >
<h6 class="mb-0"> <h6 class="d-flex mb-0">
<ng-container i18n>Threshold Min</ng-container>: <ng-container i18n>Threshold Min</ng-container>:
@if (data.rule.configuration.threshold.unit === '%') { <gf-value
{{ data.settings.thresholdMin | percent: '1.2-2' }} class="ml-1"
} @else { [isPercent]="data.rule.configuration.threshold.unit === '%'"
{{ data.settings.thresholdMin }} [locale]="data.locale"
} [precision]="2"
[value]="data.settings.thresholdMin"
/>
</h6> </h6>
<div class="align-items-center d-flex w-100"> <div class="align-items-center d-flex w-100">
@if (data.rule.configuration.threshold.unit === '%') { <gf-value
<label>{{ [isPercent]="data.rule.configuration.threshold.unit === '%'"
data.rule.configuration.threshold.min | percent: '1.2-2' [locale]="data.locale"
}}</label> [precision]="2"
} @else { [value]="data.rule.configuration.threshold.min"
<label>{{ data.rule.configuration.threshold.min }}</label> />
}
<mat-slider <mat-slider
class="flex-grow-1" class="flex-grow-1"
name="thresholdMin" name="thresholdMin"
@ -75,35 +77,35 @@
> >
<input matSliderThumb [(ngModel)]="data.settings.thresholdMin" /> <input matSliderThumb [(ngModel)]="data.settings.thresholdMin" />
</mat-slider> </mat-slider>
@if (data.rule.configuration.threshold.unit === '%') { <gf-value
<label>{{ [isPercent]="data.rule.configuration.threshold.unit === '%'"
data.rule.configuration.threshold.max | percent: '1.2-2' [locale]="data.locale"
}}</label> [precision]="2"
} @else { [value]="data.rule.configuration.threshold.max"
<label>{{ data.rule.configuration.threshold.max }}</label> />
}
</div> </div>
</div> </div>
<div <div
class="w-100" class="w-100"
[ngClass]="{ 'd-none': !data.rule.configuration.thresholdMax }" [ngClass]="{ 'd-none': !data.rule.configuration.thresholdMax }"
> >
<h6 class="mb-0"> <h6 class="d-flex mb-0">
<ng-container i18n>Threshold Max</ng-container>: <ng-container i18n>Threshold Max</ng-container>:
@if (data.rule.configuration.threshold.unit === '%') { <gf-value
{{ data.settings.thresholdMax | percent: '1.2-2' }} class="ml-1"
} @else { [isPercent]="data.rule.configuration.threshold.unit === '%'"
{{ data.settings.thresholdMax }} [locale]="data.locale"
} [precision]="2"
[value]="data.settings.thresholdMax"
/>
</h6> </h6>
<div class="align-items-center d-flex w-100"> <div class="align-items-center d-flex w-100">
@if (data.rule.configuration.threshold.unit === '%') { <gf-value
<label>{{ [isPercent]="data.rule.configuration.threshold.unit === '%'"
data.rule.configuration.threshold.min | percent: '1.2-2' [locale]="data.locale"
}}</label> [precision]="2"
} @else { [value]="data.rule.configuration.threshold.min"
<label>{{ data.rule.configuration.threshold.min }}</label> />
}
<mat-slider <mat-slider
class="flex-grow-1" class="flex-grow-1"
name="thresholdMax" name="thresholdMax"
@ -113,13 +115,12 @@
> >
<input matSliderThumb [(ngModel)]="data.settings.thresholdMax" /> <input matSliderThumb [(ngModel)]="data.settings.thresholdMax" />
</mat-slider> </mat-slider>
@if (data.rule.configuration.threshold.unit === '%') { <gf-value
<label>{{ [isPercent]="data.rule.configuration.threshold.unit === '%'"
data.rule.configuration.threshold.max | percent: '1.2-2' [locale]="data.locale"
}}</label> [precision]="2"
} @else { [value]="data.rule.configuration.threshold.max"
<label>{{ data.rule.configuration.threshold.max }}</label> />
}
</div> </div>
</div> </div>
} }

2
apps/client/src/app/pages/portfolio/activities/import-activities-dialog/import-activities-dialog.component.ts

@ -15,6 +15,7 @@ import {
StepperOrientation, StepperOrientation,
StepperSelectionEvent StepperSelectionEvent
} from '@angular/cdk/stepper'; } from '@angular/cdk/stepper';
import { CommonModule } from '@angular/common';
import { import {
ChangeDetectionStrategy, ChangeDetectionStrategy,
ChangeDetectorRef, ChangeDetectorRef,
@ -59,6 +60,7 @@ import { ImportActivitiesDialogParams } from './interfaces/interfaces';
changeDetection: ChangeDetectionStrategy.OnPush, changeDetection: ChangeDetectionStrategy.OnPush,
host: { class: 'd-flex flex-column h-100' }, host: { class: 'd-flex flex-column h-100' },
imports: [ imports: [
CommonModule,
GfActivitiesTableComponent, GfActivitiesTableComponent,
GfDialogFooterComponent, GfDialogFooterComponent,
GfDialogHeaderComponent, GfDialogHeaderComponent,

63
libs/ui/src/lib/assistant/assistant.component.ts

@ -169,6 +169,8 @@ export class GfAssistantComponent implements OnChanges, OnDestroy, OnInit {
}; };
public tags: Filter[] = []; public tags: Filter[] = [];
private readonly PRESELECTION_DELAY = 100;
private filterTypes: Filter['type'][] = [ private filterTypes: Filter['type'][] = [
'ACCOUNT', 'ACCOUNT',
'ASSET_CLASS', 'ASSET_CLASS',
@ -176,7 +178,9 @@ export class GfAssistantComponent implements OnChanges, OnDestroy, OnInit {
'SYMBOL', 'SYMBOL',
'TAG' 'TAG'
]; ];
private keyManager: FocusKeyManager<GfAssistantListItemComponent>; private keyManager: FocusKeyManager<GfAssistantListItemComponent>;
private preselectionTimeout: ReturnType<typeof setTimeout>;
private unsubscribeSubject = new Subject<void>(); private unsubscribeSubject = new Subject<void>();
public constructor( public constructor(
@ -344,6 +348,9 @@ export class GfAssistantComponent implements OnChanges, OnDestroy, OnInit {
.subscribe({ .subscribe({
next: (searchResults) => { next: (searchResults) => {
this.searchResults = searchResults; this.searchResults = searchResults;
this.preselectFirstItem();
this.changeDetectorRef.markForCheck(); this.changeDetectorRef.markForCheck();
}, },
error: (error) => { error: (error) => {
@ -585,6 +592,10 @@ export class GfAssistantComponent implements OnChanges, OnDestroy, OnInit {
} }
public ngOnDestroy() { public ngOnDestroy() {
if (this.preselectionTimeout) {
clearTimeout(this.preselectionTimeout);
}
this.unsubscribeSubject.next(); this.unsubscribeSubject.next();
this.unsubscribeSubject.complete(); this.unsubscribeSubject.complete();
} }
@ -595,6 +606,58 @@ export class GfAssistantComponent implements OnChanges, OnDestroy, OnInit {
}); });
} }
private getFirstSearchResultItem() {
if (this.searchResults.quickLinks?.length > 0) {
return this.searchResults.quickLinks[0];
}
if (this.searchResults.accounts?.length > 0) {
return this.searchResults.accounts[0];
}
if (this.searchResults.holdings?.length > 0) {
return this.searchResults.holdings[0];
}
if (this.searchResults.assetProfiles?.length > 0) {
return this.searchResults.assetProfiles[0];
}
return null;
}
private preselectFirstItem() {
if (this.preselectionTimeout) {
clearTimeout(this.preselectionTimeout);
}
this.preselectionTimeout = setTimeout(() => {
if (!this.isOpen || !this.searchFormControl.value) {
return;
}
const firstItem = this.getFirstSearchResultItem();
if (!firstItem) {
return;
}
for (const item of this.assistantListItems) {
item.removeFocus();
}
this.keyManager.setFirstItemActive();
const currentFocusedItem = this.getCurrentAssistantListItem();
if (currentFocusedItem) {
currentFocusedItem.focus();
}
this.changeDetectorRef.markForCheck();
}, this.PRESELECTION_DELAY);
}
private searchAccounts(aSearchTerm: string): Observable<ISearchResultItem[]> { private searchAccounts(aSearchTerm: string): Observable<ISearchResultItem[]> {
return this.dataService return this.dataService
.fetchAccounts({ .fetchAccounts({

2
libs/ui/src/lib/holdings-table/holdings-table.component.ts

@ -1,4 +1,3 @@
import { GfSymbolPipe } from '@ghostfolio/client/pipes/symbol/symbol.pipe';
import { getLocale } from '@ghostfolio/common/helper'; import { getLocale } from '@ghostfolio/common/helper';
import { import {
AssetProfileIdentifier, AssetProfileIdentifier,
@ -34,7 +33,6 @@ import { GfValueComponent } from '../value/value.component';
imports: [ imports: [
CommonModule, CommonModule,
GfEntityLogoComponent, GfEntityLogoComponent,
GfSymbolPipe,
GfValueComponent, GfValueComponent,
MatButtonModule, MatButtonModule,
MatDialogModule, MatDialogModule,

4
package-lock.json

@ -1,12 +1,12 @@
{ {
"name": "ghostfolio", "name": "ghostfolio",
"version": "2.205.0", "version": "2.206.0",
"lockfileVersion": 3, "lockfileVersion": 3,
"requires": true, "requires": true,
"packages": { "packages": {
"": { "": {
"name": "ghostfolio", "name": "ghostfolio",
"version": "2.205.0", "version": "2.206.0",
"hasInstallScript": true, "hasInstallScript": true,
"license": "AGPL-3.0", "license": "AGPL-3.0",
"dependencies": { "dependencies": {

2
package.json

@ -1,6 +1,6 @@
{ {
"name": "ghostfolio", "name": "ghostfolio",
"version": "2.205.0", "version": "2.206.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",

Loading…
Cancel
Save