Browse Source

Task/reuse currency selector in user account settings (#7584)

* Reuse currency selector

* Update changelog
pull/7590/head
Thomas Kaul 1 week ago
committed by GitHub
parent
commit
162d43c340
No known key found for this signature in database GPG Key ID: B5690EEEBB952194
  1. 6
      CHANGELOG.md
  2. 36
      apps/client/src/app/components/user-account-settings/user-account-settings.component.ts
  3. 22
      apps/client/src/app/components/user-account-settings/user-account-settings.html
  4. 1
      libs/ui/src/lib/currency-selector/currency-selector.component.html
  5. 26
      libs/ui/src/lib/currency-selector/currency-selector.component.ts
  6. 18
      libs/ui/src/lib/shared/abstract-mat-form-field.ts
  7. 4
      libs/ui/src/lib/symbol-autocomplete/symbol-autocomplete.component.ts

6
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

36
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<string | null>({
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 });
}

22
apps/client/src/app/components/user-account-settings/user-account-settings.html

@ -3,25 +3,17 @@
<div class="row">
<div class="col">
<div class="d-flex py-1">
<form #changeUserSettingsForm="ngForm" class="w-100">
<div class="w-100">
<div class="d-flex mb-2">
<div class="align-items-center d-flex pt-1 pt-1 w-50">
<ng-container i18n>Base Currency</ng-container>
</div>
<div class="pl-1 w-50">
<div class="pl-1 w-50" [formGroup]="baseCurrencyForm">
<mat-form-field appearance="outline" class="w-100 without-hint">
<mat-select
name="baseCurrency"
[disabled]="!hasPermissionToUpdateUserSettings"
[value]="user.settings.baseCurrency"
(selectionChange)="
onChangeUserSetting('baseCurrency', $event.value)
"
>
@for (currency of currencies; track currency) {
<mat-option [value]="currency">{{ currency }}</mat-option>
}
</mat-select>
<gf-currency-selector
formControlName="baseCurrency"
[currencies]="currencies"
/>
</mat-form-field>
</div>
</div>
@ -214,7 +206,7 @@
</mat-form-field>
</div>
</div>
</form>
</div>
</div>
<div class="align-items-center d-flex mt-4 py-1">
<div class="pr-1 w-50">

1
libs/ui/src/lib/currency-selector/currency-selector.component.html

@ -13,6 +13,7 @@
<mat-autocomplete
#currencyAutocomplete="matAutocomplete"
(closed)="onPanelClosed()"
(optionSelected)="onUpdateCurrency($event)"
>
@for (currency of filteredCurrencies; track currency) {

26
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<string> }) {
super.value = option.value;
this.lastSelectedCurrency = option.value;
}
private filter(value: string) {

18
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<T>
@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<void>();
@ -156,6 +162,16 @@ export abstract class AbstractMatFormField<T>
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;
}

4
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(() => {

Loading…
Cancel
Save