From 162d43c340eefa27da9b2df7fce82a4946cbf26c Mon Sep 17 00:00:00 2001 From: Thomas Kaul <4159106+dtslvr@users.noreply.github.com> Date: Mon, 10 Aug 2026 21:06:38 +0200 Subject: [PATCH] Task/reuse currency selector in user account settings (#7584) * Reuse currency selector * Update changelog --- CHANGELOG.md | 6 ++++ .../user-account-settings.component.ts | 36 ++++++++++++++++--- .../user-account-settings.html | 22 ++++-------- .../currency-selector.component.html | 1 + .../currency-selector.component.ts | 26 +++++++++++--- .../src/lib/shared/abstract-mat-form-field.ts | 18 +++++++++- .../symbol-autocomplete.component.ts | 4 --- 7 files changed, 84 insertions(+), 29 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index ad6854433..34f70e33e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -12,6 +12,12 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Extended the toggle component to support a disabled state - Extended the toggle component to support icons - Reused the toggle component on the portfolio holdings page +- Reused the currency selector component in the user account settings + +### Fixed + +- Fixed the handling of the disabled state in the currency selector and symbol autocomplete components +- Fixed the restoration of the current selection in the currency selector component when leaving the field without picking an option ## 3.46.0 - 2026-08-09 diff --git a/apps/client/src/app/components/user-account-settings/user-account-settings.component.ts b/apps/client/src/app/components/user-account-settings/user-account-settings.component.ts index 18ce8e07c..a4d8a1ba1 100644 --- a/apps/client/src/app/components/user-account-settings/user-account-settings.component.ts +++ b/apps/client/src/app/components/user-account-settings/user-account-settings.component.ts @@ -14,6 +14,7 @@ import { downloadAsFile } from '@ghostfolio/common/helper'; import { User } from '@ghostfolio/common/interfaces'; import { hasPermission, permissions } from '@ghostfolio/common/permissions'; import { internalRoutes } from '@ghostfolio/common/routes/routes'; +import { GfCurrencySelectorComponent } from '@ghostfolio/ui/currency-selector'; import { NotificationService } from '@ghostfolio/ui/notifications'; import { DataService } from '@ghostfolio/ui/services'; import { GfValueComponent } from '@ghostfolio/ui/value'; @@ -30,8 +31,9 @@ import { } from '@angular/core'; import { takeUntilDestroyed } from '@angular/core/rxjs-interop'; import { + FormControl, + FormGroup, NonNullableFormBuilder, - FormsModule, ReactiveFormsModule, Validators } from '@angular/forms'; @@ -58,7 +60,7 @@ import { catchError } from 'rxjs/operators'; @Component({ changeDetection: ChangeDetectionStrategy.OnPush, imports: [ - FormsModule, + GfCurrencySelectorComponent, GfValueComponent, IonIcon, MatButtonModule, @@ -77,7 +79,12 @@ import { catchError } from 'rxjs/operators'; }) export class GfUserAccountSettingsComponent implements OnInit { protected readonly appearancePlaceholder = $localize`Auto`; - protected readonly baseCurrency: string; + protected readonly baseCurrencyForm = new FormGroup({ + baseCurrency: new FormControl({ + disabled: true, + value: null + }) + }); protected closeUserAccountMailHref: string; protected readonly currencies: string[] = []; protected readonly deleteOwnUserForm = inject(NonNullableFormBuilder).group({ @@ -129,9 +136,8 @@ export class GfUserAccountSettingsComponent implements OnInit { private readonly webAuthnService = inject(WebAuthnService); public constructor() { - const { baseCurrency, currencies } = this.dataService.fetchInfo(); + const { currencies } = this.dataService.fetchInfo(); - this.baseCurrency = baseCurrency; this.currencies = currencies; this.userService.stateChanged @@ -182,6 +188,16 @@ export class GfUserAccountSettingsComponent implements OnInit { permissions.updateViewMode ); + this.baseCurrencyForm.setValue({ + baseCurrency: this.user.settings.baseCurrency ?? null + }); + + if (this.hasPermissionToUpdateUserSettings) { + this.baseCurrencyForm.enable({ emitEvent: false }); + } else { + this.baseCurrencyForm.disable({ emitEvent: false }); + } + if (this.user.settings.locale) { this.locales.push(this.user.settings.locale); } @@ -194,6 +210,16 @@ export class GfUserAccountSettingsComponent implements OnInit { } }); + this.baseCurrencyForm.controls.baseCurrency.valueChanges + .pipe(takeUntilDestroyed(this.destroyRef)) + .subscribe((value) => { + // The currency selector emits null while the user is typing and only + // emits a currency once an option has been selected + if (value && value !== this.user?.settings.baseCurrency) { + this.onChangeUserSetting('baseCurrency', value); + } + }); + addIcons({ eyeOffOutline, eyeOutline }); } diff --git a/apps/client/src/app/components/user-account-settings/user-account-settings.html b/apps/client/src/app/components/user-account-settings/user-account-settings.html index cb39360f7..1e60ebc85 100644 --- a/apps/client/src/app/components/user-account-settings/user-account-settings.html +++ b/apps/client/src/app/components/user-account-settings/user-account-settings.html @@ -3,25 +3,17 @@
-
+
Base Currency
-
+
- - @for (currency of currencies; track currency) { - {{ currency }} - } - +
@@ -214,7 +206,7 @@
-
+
diff --git a/libs/ui/src/lib/currency-selector/currency-selector.component.html b/libs/ui/src/lib/currency-selector/currency-selector.component.html index 594515e24..2100b01de 100644 --- a/libs/ui/src/lib/currency-selector/currency-selector.component.html +++ b/libs/ui/src/lib/currency-selector/currency-selector.component.html @@ -13,6 +13,7 @@ @for (currency of filteredCurrencies; track currency) { diff --git a/libs/ui/src/lib/currency-selector/currency-selector.component.ts b/libs/ui/src/lib/currency-selector/currency-selector.component.ts index 2eda80aba..27bd3ad79 100644 --- a/libs/ui/src/lib/currency-selector/currency-selector.component.ts +++ b/libs/ui/src/lib/currency-selector/currency-selector.component.ts @@ -81,6 +81,7 @@ export class GfCurrencySelectorComponent private readonly destroyRef = inject(DestroyRef); private readonly formField = inject(MAT_FORM_FIELD); private readonly input = viewChild.required(MatInput); + private lastSelectedCurrency: string | null = null; public constructor( public override readonly _elementRef: ElementRef, @@ -110,9 +111,15 @@ export class GfCurrencySelectorComponent return !this.control.value; } + public override get value() { + return super.value; + } + public override set value(value: string | null) { this.control.setValue(value); super.value = value; + + this.lastSelectedCurrency = value; } public focus() { @@ -124,10 +131,6 @@ export class GfCurrencySelectorComponent } public ngOnInit() { - if (this.disabled) { - this.control.disable(); - } - const formGroup = this.formGroupDirective.form; if (formGroup) { @@ -170,8 +173,23 @@ export class GfCurrencySelectorComponent } } + public onPanelClosed() { + // Typing clears the selected currency, so restore the last selection once + // the panel closes without an option having been picked. An empty input is + // left untouched to allow clearing the currency. + if (super.value || !this.control.value) { + return; + } + + this.value = this.lastSelectedCurrency; + + this.changeDetectorRef.markForCheck(); + } + public onUpdateCurrency({ option }: { option: MatOption }) { super.value = option.value; + + this.lastSelectedCurrency = option.value; } private filter(value: string) { diff --git a/libs/ui/src/lib/shared/abstract-mat-form-field.ts b/libs/ui/src/lib/shared/abstract-mat-form-field.ts index a6ef16175..ab24281ac 100644 --- a/libs/ui/src/lib/shared/abstract-mat-form-field.ts +++ b/libs/ui/src/lib/shared/abstract-mat-form-field.ts @@ -9,7 +9,12 @@ import { Input, OnDestroy } from '@angular/core'; -import { ControlValueAccessor, NgControl, Validators } from '@angular/forms'; +import { + ControlValueAccessor, + FormControl, + NgControl, + Validators +} from '@angular/forms'; import { MatFormFieldControl } from '@angular/material/form-field'; import { Subject } from 'rxjs'; @@ -23,6 +28,7 @@ export abstract class AbstractMatFormField @HostBinding('attr.aria-describedBy') public describedBy = ''; public readonly autofilled: boolean; + public abstract readonly control: FormControl; public errorState: boolean; public focused = false; public readonly stateChanges = new Subject(); @@ -156,6 +162,16 @@ export abstract class AbstractMatFormField this.describedBy = ids.join(' '); } + public setDisabledState(isDisabled: boolean) { + if (isDisabled) { + this.control.disable({ emitEvent: false }); + } else { + this.control.enable({ emitEvent: false }); + } + + this.stateChanges.next(); + } + public writeValue(value: T) { this.value = value; } diff --git a/libs/ui/src/lib/symbol-autocomplete/symbol-autocomplete.component.ts b/libs/ui/src/lib/symbol-autocomplete/symbol-autocomplete.component.ts index 704c4ed44..cd7846478 100644 --- a/libs/ui/src/lib/symbol-autocomplete/symbol-autocomplete.component.ts +++ b/libs/ui/src/lib/symbol-autocomplete/symbol-autocomplete.component.ts @@ -115,10 +115,6 @@ export class GfSymbolAutocompleteComponent } public ngOnInit() { - if (this.disabled) { - this.control.disable(); - } - this.control.valueChanges .pipe(takeUntilDestroyed(this.destroyRef)) .subscribe(() => {