diff --git a/CHANGELOG.md b/CHANGELOG.md index 64f8439a..a1166d13 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,12 @@ 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/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). +## Unreleased + +### Changed + +- Changed the currency selector in the create or update account dialog to `@angular/material/autocomplete` + ## 2.14.0 - 2023-10-21 ### Added diff --git a/apps/client/src/app/pages/accounts/create-or-update-account-dialog/create-or-update-account-dialog.component.ts b/apps/client/src/app/pages/accounts/create-or-update-account-dialog/create-or-update-account-dialog.component.ts index e2c63f19..9e153d17 100644 --- a/apps/client/src/app/pages/accounts/create-or-update-account-dialog/create-or-update-account-dialog.component.ts +++ b/apps/client/src/app/pages/accounts/create-or-update-account-dialog/create-or-update-account-dialog.component.ts @@ -15,6 +15,7 @@ import { MAT_DIALOG_DATA, MatDialogRef } from '@angular/material/dialog'; import { CreateAccountDto } from '@ghostfolio/api/app/account/create-account.dto'; import { UpdateAccountDto } from '@ghostfolio/api/app/account/update-account.dto'; import { DataService } from '@ghostfolio/client/services/data.service'; +import { Currency } from '@ghostfolio/common/interfaces/currency.interface'; import { Platform } from '@prisma/client'; import { Observable, Subject } from 'rxjs'; import { map, startWith } from 'rxjs/operators'; @@ -30,7 +31,7 @@ import { CreateOrUpdateAccountDialogParams } from './interfaces/interfaces'; }) export class CreateOrUpdateAccountDialog implements OnDestroy { public accountForm: FormGroup; - public currencies: string[] = []; + public currencies: Currency[] = []; public filteredPlatforms: Observable; public platforms: Platform[]; @@ -46,7 +47,10 @@ export class CreateOrUpdateAccountDialog implements OnDestroy { public ngOnInit() { const { currencies, platforms } = this.dataService.fetchInfo(); - this.currencies = currencies; + this.currencies = currencies.map((currency) => ({ + label: currency, + value: currency + })); this.platforms = platforms; this.accountForm = this.formBuilder.group({ @@ -101,7 +105,7 @@ export class CreateOrUpdateAccountDialog implements OnDestroy { const account: CreateAccountDto | UpdateAccountDto = { balance: this.accountForm.controls['balance'].value, comment: this.accountForm.controls['comment'].value, - currency: this.accountForm.controls['currency'].value, + currency: this.accountForm.controls['currency'].value?.value, id: this.accountForm.controls['accountId'].value, isExcluded: this.accountForm.controls['isExcluded'].value, name: this.accountForm.controls['name'].value, diff --git a/apps/client/src/app/pages/accounts/create-or-update-account-dialog/create-or-update-account-dialog.html b/apps/client/src/app/pages/accounts/create-or-update-account-dialog/create-or-update-account-dialog.html index d4469343..35074ec9 100644 --- a/apps/client/src/app/pages/accounts/create-or-update-account-dialog/create-or-update-account-dialog.html +++ b/apps/client/src/app/pages/accounts/create-or-update-account-dialog/create-or-update-account-dialog.html @@ -20,11 +20,10 @@
Currency - - {{ currency }} - +
@@ -37,7 +36,7 @@ (keydown.enter)="$event.stopPropagation()" /> {{ accountForm.controls['currency'].value }}{{ accountForm.controls['currency']?.value?.value }}
diff --git a/apps/client/src/app/pages/accounts/create-or-update-account-dialog/create-or-update-account-dialog.module.ts b/apps/client/src/app/pages/accounts/create-or-update-account-dialog/create-or-update-account-dialog.module.ts index 22ec5e1f..2ccf5675 100644 --- a/apps/client/src/app/pages/accounts/create-or-update-account-dialog/create-or-update-account-dialog.module.ts +++ b/apps/client/src/app/pages/accounts/create-or-update-account-dialog/create-or-update-account-dialog.module.ts @@ -7,8 +7,8 @@ import { MatCheckboxModule } from '@angular/material/checkbox'; import { MatDialogModule } from '@angular/material/dialog'; import { MatFormFieldModule } from '@angular/material/form-field'; import { MatInputModule } from '@angular/material/input'; -import { MatSelectModule } from '@angular/material/select'; import { GfSymbolIconModule } from '@ghostfolio/client/components/symbol-icon/symbol-icon.module'; +import { GfCurrencySelectorModule } from '@ghostfolio/ui/currency-selector/currency-selector.module'; import { CreateOrUpdateAccountDialog } from './create-or-update-account-dialog.component'; @@ -17,6 +17,7 @@ import { CreateOrUpdateAccountDialog } from './create-or-update-account-dialog.c imports: [ CommonModule, FormsModule, + GfCurrencySelectorModule, GfSymbolIconModule, MatAutocompleteModule, MatButtonModule, @@ -24,7 +25,6 @@ import { CreateOrUpdateAccountDialog } from './create-or-update-account-dialog.c MatDialogModule, MatFormFieldModule, MatInputModule, - MatSelectModule, ReactiveFormsModule ] }) diff --git a/libs/common/src/lib/interfaces/currency.interface.ts b/libs/common/src/lib/interfaces/currency.interface.ts new file mode 100644 index 00000000..619144c0 --- /dev/null +++ b/libs/common/src/lib/interfaces/currency.interface.ts @@ -0,0 +1,4 @@ +export interface Currency { + label: string; + value: string; +} diff --git a/libs/ui/src/lib/currency-selector/currency-selector.component.html b/libs/ui/src/lib/currency-selector/currency-selector.component.html new file mode 100644 index 00000000..38fc6c43 --- /dev/null +++ b/libs/ui/src/lib/currency-selector/currency-selector.component.html @@ -0,0 +1,21 @@ + + + + + {{ currencyItem.label }} + + diff --git a/libs/ui/src/lib/currency-selector/currency-selector.component.scss b/libs/ui/src/lib/currency-selector/currency-selector.component.scss new file mode 100644 index 00000000..5d4e87f3 --- /dev/null +++ b/libs/ui/src/lib/currency-selector/currency-selector.component.scss @@ -0,0 +1,3 @@ +:host { + display: block; +} diff --git a/libs/ui/src/lib/currency-selector/currency-selector.component.ts b/libs/ui/src/lib/currency-selector/currency-selector.component.ts new file mode 100644 index 00000000..f75b684d --- /dev/null +++ b/libs/ui/src/lib/currency-selector/currency-selector.component.ts @@ -0,0 +1,167 @@ +import { FocusMonitor } from '@angular/cdk/a11y'; +import { + ChangeDetectionStrategy, + ChangeDetectorRef, + Component, + ElementRef, + Input, + OnDestroy, + OnInit, + ViewChild +} from '@angular/core'; +import { FormControl, FormGroupDirective, NgControl } from '@angular/forms'; +import { + MatAutocomplete, + MatAutocompleteSelectedEvent +} from '@angular/material/autocomplete'; +import { MatFormFieldControl } from '@angular/material/form-field'; +import { MatInput } from '@angular/material/input'; +import { Currency } from '@ghostfolio/common/interfaces/currency.interface'; +import { AbstractMatFormField } from '@ghostfolio/ui/shared/abstract-mat-form-field'; +import { Subject } from 'rxjs'; +import { map, startWith, takeUntil } from 'rxjs/operators'; +@Component({ + changeDetection: ChangeDetectionStrategy.OnPush, + host: { + '[attr.aria-describedBy]': 'describedBy', + '[id]': 'id' + }, + providers: [ + { + provide: MatFormFieldControl, + useExisting: CurrencySelectorComponent + } + ], + selector: 'gf-currency-selector', + styleUrls: ['./currency-selector.component.scss'], + templateUrl: 'currency-selector.component.html' +}) +export class CurrencySelectorComponent + extends AbstractMatFormField + implements OnInit, OnDestroy +{ + @Input() private currencies: Currency[] = []; + @Input() private formControlName: string; + + @ViewChild(MatInput) private input: MatInput; + + @ViewChild('currencyAutocomplete') + public currencyAutocomplete: MatAutocomplete; + + public control = new FormControl(); + public filteredCurrencies: Currency[] = []; + + private unsubscribeSubject = new Subject(); + + public constructor( + public readonly _elementRef: ElementRef, + public readonly _focusMonitor: FocusMonitor, + public readonly changeDetectorRef: ChangeDetectorRef, + private readonly formGroupDirective: FormGroupDirective, + public readonly ngControl: NgControl + ) { + super(_elementRef, _focusMonitor, ngControl); + + this.controlType = 'currency-selector'; + } + + public ngOnInit() { + if (this.disabled) { + this.control.disable(); + } + + const formGroup = this.formGroupDirective.form; + + if (formGroup) { + const control = formGroup.get(this.formControlName); + + if (control) { + this.value = this.currencies.find(({ value }) => { + return value === control.value; + }); + } + } + + this.control.valueChanges + .pipe(takeUntil(this.unsubscribeSubject)) + .subscribe(() => { + if (super.value?.value) { + super.value.value = null; + } + }); + + this.control.valueChanges + .pipe( + takeUntil(this.unsubscribeSubject), + startWith(''), + map((value) => { + return value ? this.filter(value) : this.currencies.slice(); + }) + ) + .subscribe((values) => { + this.filteredCurrencies = values; + }); + } + + public displayFn(currency: Currency) { + return currency?.label ?? ''; + } + + public get empty() { + return this.input?.empty; + } + + public focus() { + this.input.focus(); + } + + public ngDoCheck() { + if (this.ngControl) { + this.validateRequired(); + this.errorState = this.ngControl.invalid && this.ngControl.touched; + this.stateChanges.next(); + } + } + + public onUpdateCurrency(event: MatAutocompleteSelectedEvent) { + super.value = { + label: event.option.value.label, + value: event.option.value.value + } as Currency; + } + + public set value(value: Currency) { + const newValue = + typeof value === 'object' && value !== null ? { ...value } : value; + this.control.setValue(newValue); + super.value = newValue; + } + + public ngOnDestroy() { + super.ngOnDestroy(); + + this.unsubscribeSubject.next(); + this.unsubscribeSubject.complete(); + } + + private filter(value: Currency | string) { + const filterValue = + typeof value === 'string' + ? value?.toLowerCase() + : value?.value.toLowerCase(); + + return this.currencies.filter((currency) => { + return currency.value.toLowerCase().startsWith(filterValue); + }); + } + + private validateRequired() { + const requiredCheck = super.required + ? !super.value.label || !super.value.value + : false; + + if (requiredCheck) { + this.ngControl.control.setErrors({ invalidData: true }); + } + } +} diff --git a/libs/ui/src/lib/currency-selector/currency-selector.module.ts b/libs/ui/src/lib/currency-selector/currency-selector.module.ts new file mode 100644 index 00000000..ac4d1209 --- /dev/null +++ b/libs/ui/src/lib/currency-selector/currency-selector.module.ts @@ -0,0 +1,23 @@ +import { CommonModule } from '@angular/common'; +import { CUSTOM_ELEMENTS_SCHEMA, NgModule } from '@angular/core'; +import { FormsModule, ReactiveFormsModule } from '@angular/forms'; +import { MatAutocompleteModule } from '@angular/material/autocomplete'; +import { MatFormFieldModule } from '@angular/material/form-field'; +import { MatInputModule } from '@angular/material/input'; + +import { CurrencySelectorComponent } from './currency-selector.component'; + +@NgModule({ + declarations: [CurrencySelectorComponent], + exports: [CurrencySelectorComponent], + imports: [ + CommonModule, + FormsModule, + MatAutocompleteModule, + MatFormFieldModule, + MatInputModule, + ReactiveFormsModule + ], + schemas: [CUSTOM_ELEMENTS_SCHEMA] +}) +export class GfCurrencySelectorModule {} diff --git a/libs/ui/src/lib/symbol-autocomplete/abstract-mat-form-field.ts b/libs/ui/src/lib/shared/abstract-mat-form-field.ts similarity index 100% rename from libs/ui/src/lib/symbol-autocomplete/abstract-mat-form-field.ts rename to libs/ui/src/lib/shared/abstract-mat-form-field.ts 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 654a634f..17cdb853 100644 --- a/libs/ui/src/lib/symbol-autocomplete/symbol-autocomplete.component.ts +++ b/libs/ui/src/lib/symbol-autocomplete/symbol-autocomplete.component.ts @@ -19,6 +19,7 @@ import { MatInput } from '@angular/material/input'; import { LookupItem } from '@ghostfolio/api/app/symbol/interfaces/lookup-item.interface'; import { DataService } from '@ghostfolio/client/services/data.service'; import { translate } from '@ghostfolio/ui/i18n'; +import { AbstractMatFormField } from '@ghostfolio/ui/shared/abstract-mat-form-field'; import { isString } from 'lodash'; import { Subject, tap } from 'rxjs'; import { @@ -29,8 +30,6 @@ import { takeUntil } from 'rxjs/operators'; -import { AbstractMatFormField } from './abstract-mat-form-field'; - @Component({ changeDetection: ChangeDetectionStrategy.OnPush, host: { @@ -54,7 +53,7 @@ export class SymbolAutocompleteComponent @Input() private includeIndices = false; @Input() public isLoading = false; - @ViewChild(MatInput, { static: false }) private input: MatInput; + @ViewChild(MatInput) private input: MatInput; @ViewChild('symbolAutocomplete') public symbolAutocomplete: MatAutocomplete;