Browse Source

Merge branch 'main' of github.com:luispinho/ghostfolio into feature/translation-pt-2

# Fix conflicts:
#	apps/client/src/locales/messages.pt.xlf
pull/2074/head
Luis Pinho 2 years ago
parent
commit
da71b1b333
  1. 12
      CHANGELOG.md
  2. 59
      apps/client/src/app/pages/features/features-page.html
  3. 75
      apps/client/src/app/pages/portfolio/activities/create-or-update-activity-dialog/create-or-update-activity-dialog.component.ts
  4. 28
      apps/client/src/app/pages/portfolio/activities/create-or-update-activity-dialog/create-or-update-activity-dialog.html
  5. 6
      apps/client/src/app/pages/portfolio/activities/create-or-update-activity-dialog/create-or-update-activity-dialog.module.ts
  6. 4
      apps/client/src/locales/messages.pt.xlf
  7. 14
      libs/ui/src/lib/holdings-table/holdings-table.component.html
  8. 178
      libs/ui/src/lib/symbol-autocomplete/abstract-mat-form-field.ts
  9. 1
      libs/ui/src/lib/symbol-autocomplete/index.ts
  10. 34
      libs/ui/src/lib/symbol-autocomplete/symbol-autocomplete.component.html
  11. 8
      libs/ui/src/lib/symbol-autocomplete/symbol-autocomplete.component.scss
  12. 169
      libs/ui/src/lib/symbol-autocomplete/symbol-autocomplete.component.ts
  13. 26
      libs/ui/src/lib/symbol-autocomplete/symbol-autocomplete.module.ts
  14. 4
      package.json
  15. 36
      yarn.lock

12
CHANGELOG.md

@ -5,6 +5,18 @@ 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
### Added
- Extended the feature overview page by liabilities
### Changed
- Extracted the symbol search to a dedicated component
- Improved the column headers in the holdings table for mobile
- Upgraded `prisma` from version `4.14.1` to `4.15.0`
## 1.280.1 - 2023-06-10
### Added

59
apps/client/src/app/pages/features/features-page.html

@ -1,7 +1,7 @@
<div class="container">
<div class="row">
<div class="col">
<h3 class="d-none d-sm-block mb-3 text-center">Features</h3>
<h3 class="d-none d-sm-block mb-3 text-center" i18n>Features</h3>
<div class="mb-4">
<p>
Check out the numerous features of <strong>Ghostfolio</strong> to
@ -13,7 +13,7 @@
<mat-card appearance="outlined" class="d-flex flex-column h-100">
<mat-card-content>
<div class="flex-grow-1">
<h4>Stocks</h4>
<h4 i18n>Stocks</h4>
<p class="m-0">Keep track of your stock purchases and sales.</p>
</div>
</mat-card-content>
@ -23,7 +23,7 @@
<mat-card appearance="outlined" class="d-flex flex-column h-100">
<mat-card-content>
<div class="flex-grow-1">
<h4>ETFs</h4>
<h4 i18n>ETFs</h4>
<p class="m-0">
Are you into ETFs (Exchange Traded Funds)? Track your ETF
investments.
@ -36,7 +36,7 @@
<mat-card appearance="outlined" class="d-flex flex-column h-100">
<mat-card-content>
<div class="flex-grow-1">
<h4>Bonds</h4>
<h4 i18n>Bonds</h4>
<p class="m-0">
Manage your investment in bonds and other assets with fixed
income.
@ -49,7 +49,7 @@
<mat-card appearance="outlined" class="d-flex flex-column h-100">
<mat-card-content>
<div class="flex-grow-1">
<h4>Cryptocurrencies</h4>
<h4 i18n>Cryptocurrencies</h4>
<p class="m-0">
Keep track of your Bitcoin and Altcoin holdings.
</p>
@ -61,7 +61,7 @@
<mat-card appearance="outlined" class="d-flex flex-column h-100">
<mat-card-content>
<div class="flex-grow-1">
<h4>Dividend</h4>
<h4 i18n>Dividend</h4>
<p class="m-0">
Are you building a dividend portfolio? Track your dividend in
Ghostfolio.
@ -74,7 +74,7 @@
<mat-card appearance="outlined" class="d-flex flex-column h-100">
<mat-card-content>
<div class="flex-grow-1">
<h4 class="align-items-center d-flex">Wealth Items</h4>
<h4 class="align-items-center d-flex" i18n>Wealth Items</h4>
<p class="m-0">
Track all your treasuries, be it your luxury watch or rare
trading cards.
@ -87,7 +87,7 @@
<mat-card appearance="outlined" class="d-flex flex-column h-100">
<mat-card-content>
<div class="flex-grow-1">
<h4 class="align-items-center d-flex">Emergency Fund</h4>
<h4 class="align-items-center d-flex" i18n>Emergency Fund</h4>
<p class="m-0">
Define your emergency fund you are comfortable with for
difficult times.
@ -100,7 +100,22 @@
<mat-card appearance="outlined" class="d-flex flex-column h-100">
<mat-card-content>
<div class="flex-grow-1">
<h4 class="align-items-center d-flex">Import and Export</h4>
<h4 class="align-items-center d-flex" i18n>Liabilities</h4>
<p class="m-0">
Manage your financial liabilities, such as your student loan,
to stay ahead of your financial obligations.
</p>
</div>
</mat-card-content>
</mat-card>
</div>
<div class="col-xs-12 col-md-4 mb-3">
<mat-card appearance="outlined" class="d-flex flex-column h-100">
<mat-card-content>
<div class="flex-grow-1">
<h4 class="align-items-center d-flex" i18n>
Import and Export
</h4>
<p class="m-0">Import and export your investment activities.</p>
</div>
</mat-card-content>
@ -110,7 +125,7 @@
<mat-card appearance="outlined" class="d-flex flex-column h-100">
<mat-card-content>
<div class="flex-grow-1">
<h4>Multi-Accounts</h4>
<h4 i18n>Multi-Accounts</h4>
<p class="m-0">
Keep an eye on all your accounts across multiple platforms
(multi-banking).
@ -124,7 +139,7 @@
<mat-card-content>
<div class="flex-grow-1">
<h4 class="align-items-center d-flex">
<span>Portfolio Calculations</span>
<span i18n>Portfolio Calculations</span>
<gf-premium-indicator
*ngIf="hasPermissionForSubscription"
class="ml-1"
@ -144,7 +159,7 @@
<mat-card-content>
<div class="flex-grow-1">
<h4 class="align-items-center d-flex">
<span>Portfolio Allocations</span>
<span i18n>Portfolio Allocations</span>
<gf-premium-indicator
*ngIf="hasPermissionForSubscription"
class="ml-1"
@ -162,7 +177,7 @@
<mat-card appearance="outlined" class="d-flex flex-column h-100">
<mat-card-content>
<div class="flex-grow-1">
<h4 class="align-items-center d-flex">Dark Mode</h4>
<h4 class="align-items-center d-flex" i18n>Dark Mode</h4>
<p class="m-0">
Ghostfolio automatically switches to a dark color theme based
on your operating system's preferences.
@ -175,7 +190,7 @@
<mat-card appearance="outlined" class="d-flex flex-column h-100">
<mat-card-content>
<div class="flex-grow-1">
<h4 class="align-items-center d-flex">Zen Mode</h4>
<h4 class="align-items-center d-flex" i18n>Zen Mode</h4>
<p class="m-0">
Keep calm and activate Zen Mode if the markets are going
crazy.
@ -192,7 +207,7 @@
<mat-card-content>
<div class="flex-grow-1">
<h4 class="align-items-center d-flex">
<span>Market Mood</span>
<span i18n>Market Mood</span>
<gf-premium-indicator class="ml-1"></gf-premium-indicator>
</h4>
<p class="m-0">
@ -210,7 +225,7 @@
<mat-card-content>
<div class="flex-grow-1">
<h4 class="align-items-center d-flex">
<span>Static Analysis</span>
<span i18n>Static Analysis</span>
<gf-premium-indicator
*ngIf="hasPermissionForSubscription"
class="ml-1"
@ -228,7 +243,7 @@
<mat-card appearance="outlined" class="d-flex flex-column h-100">
<mat-card-content>
<div class="flex-grow-1">
<h4>Multi-Language</h4>
<h4 i18n>Multi-Language</h4>
<p class="m-0">
Use Ghostfolio in multiple languages: English, Dutch, French,
German, Italian<ng-container *ngIf="false"
@ -244,7 +259,7 @@
<mat-card appearance="outlined" class="d-flex flex-column h-100">
<mat-card-content>
<div class="flex-grow-1">
<h4>Community</h4>
<h4 i18n>Community</h4>
<p class="m-0">
Join the Ghostfolio
<a
@ -263,7 +278,7 @@
<mat-card appearance="outlined" class="d-flex flex-column h-100">
<mat-card-content>
<div class="flex-grow-1">
<h4>Open Source Software</h4>
<h4 i18n>Open Source Software</h4>
<p class="m-0">
The source code is fully available as
<a
@ -282,9 +297,9 @@
</div>
<div *ngIf="!user" class="row">
<div class="col mt-3 text-center">
<a color="primary" mat-flat-button [routerLink]="['/register']">
Get Started
</a>
<a color="primary" i18n mat-flat-button [routerLink]="['/register']"
>Get Started</a
>
</div>
</div>
</div>

75
apps/client/src/app/pages/portfolio/activities/create-or-update-activity-dialog/create-or-update-activity-dialog.component.ts

@ -55,8 +55,6 @@ export class CreateOrUpdateActivityDialog implements OnDestroy {
public currencies: string[] = [];
public currentMarketPrice = null;
public defaultDateFormat: string;
public filteredLookupItems: LookupItem[] = [];
public filteredLookupItemsObservable: Observable<LookupItem[]> = of([]);
public filteredTagsObservable: Observable<Tag[]> = of([]);
public isLoading = false;
public platforms: { id: string; name: string }[];
@ -120,10 +118,12 @@ export class CreateOrUpdateActivityDialog implements OnDestroy {
name: [this.data.activity?.SymbolProfile?.name, Validators.required],
quantity: [this.data.activity?.quantity, Validators.required],
searchSymbol: [
{
dataSource: this.data.activity?.SymbolProfile?.dataSource,
symbol: this.data.activity?.SymbolProfile?.symbol
},
!!this.data.activity?.SymbolProfile
? {
dataSource: this.data.activity?.SymbolProfile?.dataSource,
symbol: this.data.activity?.SymbolProfile?.symbol
}
: null,
Validators.required
],
tags: [
@ -238,28 +238,19 @@ export class CreateOrUpdateActivityDialog implements OnDestroy {
this.changeDetectorRef.markForCheck();
});
this.filteredLookupItemsObservable = this.activityForm.controls[
'searchSymbol'
].valueChanges.pipe(
debounceTime(400),
distinctUntilChanged(),
switchMap((query: string) => {
if (isString(query) && query.length > 1) {
const filteredLookupItemsObservable =
this.dataService.fetchSymbols(query);
filteredLookupItemsObservable
.pipe(takeUntil(this.unsubscribeSubject))
.subscribe((filteredLookupItems) => {
this.filteredLookupItems = filteredLookupItems;
});
this.activityForm.controls['searchSymbol'].valueChanges.subscribe(() => {
if (this.activityForm.controls['searchSymbol'].invalid) {
this.data.activity.SymbolProfile = null;
} else {
this.activityForm.controls['dataSource'].setValue(
this.activityForm.controls['searchSymbol'].value.dataSource
);
return filteredLookupItemsObservable;
}
this.updateSymbol();
}
return [];
})
);
this.changeDetectorRef.markForCheck();
});
this.filteredTagsObservable = this.activityForm.controls[
'tags'
@ -393,25 +384,6 @@ export class CreateOrUpdateActivityDialog implements OnDestroy {
this.tagInput.nativeElement.value = '';
}
public onBlurSymbol() {
const currentLookupItem = this.filteredLookupItems.find((lookupItem) => {
return (
lookupItem.symbol ===
this.activityForm.controls['searchSymbol'].value.symbol
);
});
if (currentLookupItem) {
this.updateSymbol(currentLookupItem.symbol);
} else {
this.activityForm.controls['searchSymbol'].setErrors({ incorrect: true });
this.data.activity.SymbolProfile = null;
}
this.changeDetectorRef.markForCheck();
}
public onCancel() {
this.dialogRef.close();
}
@ -455,13 +427,6 @@ export class CreateOrUpdateActivityDialog implements OnDestroy {
this.dialogRef.close({ activity });
}
public onUpdateSymbol(event: MatAutocompleteSelectedEvent) {
this.activityForm.controls['dataSource'].setValue(
event.option.value.dataSource
);
this.updateSymbol(event.option.value.symbol);
}
public ngOnDestroy() {
this.unsubscribeSubject.next();
this.unsubscribeSubject.complete();
@ -477,12 +442,8 @@ export class CreateOrUpdateActivityDialog implements OnDestroy {
});
}
private updateSymbol(symbol: string) {
private updateSymbol() {
this.isLoading = true;
this.activityForm.controls['searchSymbol'].setErrors(null);
this.activityForm.controls['searchSymbol'].setValue({ symbol });
this.changeDetectorRef.markForCheck();
this.dataService

28
apps/client/src/app/pages/portfolio/activities/create-or-update-activity-dialog/create-or-update-activity-dialog.html

@ -48,34 +48,10 @@
>
<mat-form-field appearance="outline" class="w-100">
<mat-label i18n>Name, symbol or ISIN</mat-label>
<input
autocapitalize="off"
autocomplete="off"
autocorrect="off"
<gf-symbol-autocomplete
formControlName="searchSymbol"
matInput
[matAutocomplete]="symbolAutocomplete"
(blur)="onBlurSymbol()"
[isLoading]="isLoading"
/>
<mat-autocomplete
#symbolAutocomplete="matAutocomplete"
[displayWith]="displayFn"
(optionSelected)="onUpdateSymbol($event)"
>
<mat-option
*ngFor="let lookupItem of filteredLookupItemsObservable | async"
class="line-height-1"
[value]="lookupItem"
>
<span><b>{{ lookupItem.name }}</b></span>
<br />
<small class="text-muted"
>{{ lookupItem.symbol | gfSymbol }} · {{ lookupItem.currency
}}</small
>
</mat-option>
</mat-autocomplete>
<mat-spinner *ngIf="isLoading" matSuffix [diameter]="20"></mat-spinner>
</mat-form-field>
</div>
<div

6
apps/client/src/app/pages/portfolio/activities/create-or-update-activity-dialog/create-or-update-activity-dialog.module.ts

@ -9,9 +9,8 @@ import { MatDatepickerModule } from '@angular/material/datepicker';
import { MatDialogModule } from '@angular/material/dialog';
import { MatFormFieldModule } from '@angular/material/form-field';
import { MatInputModule } from '@angular/material/input';
import { MatProgressSpinnerModule } from '@angular/material/progress-spinner';
import { MatSelectModule } from '@angular/material/select';
import { GfSymbolModule } from '@ghostfolio/client/pipes/symbol/symbol.module';
import { GfSymbolAutocompleteModule } from '@ghostfolio/ui/symbol-autocomplete/symbol-autocomplete.module';
import { GfValueModule } from '@ghostfolio/ui/value';
import { CreateOrUpdateActivityDialog } from './create-or-update-activity-dialog.component';
@ -21,7 +20,7 @@ import { CreateOrUpdateActivityDialog } from './create-or-update-activity-dialog
imports: [
CommonModule,
FormsModule,
GfSymbolModule,
GfSymbolAutocompleteModule,
GfValueModule,
MatAutocompleteModule,
MatButtonModule,
@ -31,7 +30,6 @@ import { CreateOrUpdateActivityDialog } from './create-or-update-activity-dialog
MatDialogModule,
MatFormFieldModule,
MatInputModule,
MatProgressSpinnerModule,
MatSelectModule,
ReactiveFormsModule
],

4
apps/client/src/locales/messages.pt.xlf

@ -3243,7 +3243,7 @@
</trans-unit>
<trans-unit id="2963674907100579427" datatype="html">
<source>Higher Risk</source>
<target state="new">Risco mais Elevado</target>
<target state="translated">Risco mais Elevado</target>
<context-group purpose="location">
<context context-type="sourcefile">libs/ui/src/lib/i18n.ts</context>
<context context-type="linenumber">13</context>
@ -3251,7 +3251,7 @@
</trans-unit>
<trans-unit id="4152514811781104574" datatype="html">
<source>Lower Risk</source>
<target state="new">Risco menos Elevado</target>
<target state="translated">Risco menos Elevado</target>
<context-group purpose="location">
<context context-type="sourcefile">libs/ui/src/lib/i18n.ts</context>
<context context-type="linenumber">15</context>

14
libs/ui/src/lib/holdings-table/holdings-table.component.html

@ -92,7 +92,8 @@
mat-header-cell
mat-sort-header
>
<ng-container i18n>Allocation</ng-container>
<span class="d-none d-sm-block" i18n>Allocation</span>
<span class="d-block d-sm-none" title="Allocation">%</span>
</th>
<td *matCellDef="let element" class="px-1" mat-cell>
<div class="d-flex justify-content-end">
@ -108,17 +109,14 @@
<ng-container matColumnDef="performance">
<th
*matHeaderCellDef
class="d-none d-lg-table-cell px-1 justify-content-end"
class="justify-content-end px-1"
mat-header-cell
mat-sort-header="netPerformancePercent"
>
<ng-container i18n>Performance</ng-container>
<span class="d-none d-sm-block" i18n>Performance</span>
<span class="d-block d-sm-none" title="Performance">±</span>
</th>
<td
*matCellDef="let element"
class="d-none d-lg-table-cell px-1"
mat-cell
>
<td *matCellDef="let element" class="px-1" mat-cell>
<div class="d-flex justify-content-end">
<gf-value
[colorizeSign]="true"

178
libs/ui/src/lib/symbol-autocomplete/abstract-mat-form-field.ts

@ -0,0 +1,178 @@
import { FocusMonitor } from '@angular/cdk/a11y';
import { coerceBooleanProperty } from '@angular/cdk/coercion';
import {
Component,
DoCheck,
ElementRef,
HostBinding,
HostListener,
Input,
OnDestroy
} from '@angular/core';
import { ControlValueAccessor, NgControl } from '@angular/forms';
import { MatFormFieldControl } from '@angular/material/form-field';
import { Subject } from 'rxjs';
@Component({
template: ''
})
export abstract class AbstractMatFormField<T>
implements ControlValueAccessor, DoCheck, MatFormFieldControl<T>, OnDestroy
{
@HostBinding()
public id = `${this.controlType}-${AbstractMatFormField.nextId++}`;
@HostBinding('attr.aria-describedBy') public describedBy = '';
public readonly autofilled: boolean;
public errorState: boolean;
public focused = false;
public readonly stateChanges = new Subject<void>();
public readonly userAriaDescribedBy: string;
protected onChange?: (value: T) => void;
protected onTouched?: () => void;
private static nextId: number = 0;
protected constructor(
protected _elementRef: ElementRef,
protected _focusMonitor: FocusMonitor,
public readonly ngControl: NgControl
) {
if (this.ngControl) {
this.ngControl.valueAccessor = this;
}
_focusMonitor
.monitor(this._elementRef.nativeElement, true)
.subscribe((origin) => {
this.focused = !!origin;
this.stateChanges.next();
});
}
private _controlType: string;
public get controlType(): string {
return this._controlType;
}
protected set controlType(value: string) {
this._controlType = value;
this.id = `${this._controlType}-${AbstractMatFormField.nextId++}`;
}
private _value: T;
public get value(): T {
return this._value;
}
public set value(value: T) {
this._value = value;
if (this.onChange) {
this.onChange(value);
}
}
public get empty(): boolean {
return !this._value;
}
public _placeholder: string = '';
public get placeholder() {
return this._placeholder;
}
@Input()
public set placeholder(placeholder: string) {
this._placeholder = placeholder;
this.stateChanges.next();
}
public _required: boolean = false;
public get required() {
return this._required;
}
@Input()
public set required(required: any) {
this._required = coerceBooleanProperty(required);
this.stateChanges.next();
}
public _disabled: boolean = false;
public get disabled() {
if (this.ngControl && this.ngControl.disabled !== null) {
return this.ngControl.disabled;
}
return this._disabled;
}
@Input()
public set disabled(disabled: any) {
this._disabled = coerceBooleanProperty(disabled);
if (this.focused) {
this.focused = false;
this.stateChanges.next();
}
}
public abstract focus(): void;
public get shouldLabelFloat(): boolean {
return this.focused || !this.empty;
}
public ngDoCheck(): void {
if (this.ngControl) {
this.errorState = this.ngControl.invalid && this.ngControl.touched;
this.stateChanges.next();
}
}
public ngOnDestroy(): void {
this.stateChanges.complete();
this._focusMonitor.stopMonitoring(this._elementRef.nativeElement);
}
public registerOnChange(fn: (_: T) => void): void {
this.onChange = fn;
}
public registerOnTouched(fn: () => void): void {
this.onTouched = fn;
}
public setDescribedByIds(ids: string[]): void {
this.describedBy = ids.join(' ');
}
public writeValue(value: T): void {
this.value = value;
}
@HostListener('focusout')
public onBlur() {
this.focused = false;
if (this.onTouched) {
this.onTouched();
}
this.stateChanges.next();
}
public onContainerClick(): void {
if (!this.focused) {
this.focus();
}
}
}

1
libs/ui/src/lib/symbol-autocomplete/index.ts

@ -0,0 +1 @@
export * from './symbol-autocomplete.module';

34
libs/ui/src/lib/symbol-autocomplete/symbol-autocomplete.component.html

@ -0,0 +1,34 @@
<input
autocapitalize="off"
autocomplete="off"
matInput
[formControl]="control"
[matAutocomplete]="symbolAutocomplete"
/>
<mat-autocomplete
#symbolAutocomplete="matAutocomplete"
[displayWith]="displayFn"
(optionSelected)="onUpdateSymbol($event)"
>
<ng-container *ngIf="!isLoading">
<mat-option
*ngFor="let lookupItem of filteredLookupItems"
class="line-height-1"
[value]="lookupItem"
>
<span
><b>{{ lookupItem.name }}</b></span
>
<br />
<small class="text-muted"
>{{ lookupItem.symbol | gfSymbol }} · {{ lookupItem.currency }}</small
>
</mat-option>
</ng-container>
</mat-autocomplete>
<mat-spinner
*ngIf="isLoading"
class="position-absolute"
[diameter]="20"
></mat-spinner>

8
libs/ui/src/lib/symbol-autocomplete/symbol-autocomplete.component.scss

@ -0,0 +1,8 @@
:host {
display: block;
.mat-mdc-progress-spinner {
right: 0;
top: calc(50% - 10px);
}
}

169
libs/ui/src/lib/symbol-autocomplete/symbol-autocomplete.component.ts

@ -0,0 +1,169 @@
import { FocusMonitor } from '@angular/cdk/a11y';
import {
ChangeDetectionStrategy,
ChangeDetectorRef,
Component,
ElementRef,
Input,
OnDestroy,
OnInit,
ViewChild
} from '@angular/core';
import { FormControl, NgControl, Validators } from '@angular/forms';
import {
MatAutocomplete,
MatAutocompleteSelectedEvent
} from '@angular/material/autocomplete';
import { MatFormFieldControl } from '@angular/material/form-field';
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 { isString } from 'lodash';
import { Observable, Subject, of, tap } from 'rxjs';
import {
debounceTime,
distinctUntilChanged,
filter,
switchMap
} from 'rxjs/operators';
import { AbstractMatFormField } from './abstract-mat-form-field';
@Component({
changeDetection: ChangeDetectionStrategy.OnPush,
host: {
'[attr.aria-describedBy]': 'describedBy',
'[id]': 'id'
},
selector: 'gf-symbol-autocomplete',
styleUrls: ['./symbol-autocomplete.component.scss'],
templateUrl: 'symbol-autocomplete.component.html',
providers: [
{
provide: MatFormFieldControl,
useExisting: SymbolAutocompleteComponent
}
]
})
export class SymbolAutocompleteComponent
extends AbstractMatFormField<LookupItem>
implements OnInit, OnDestroy
{
@Input() public isLoading = false;
@ViewChild(MatInput, { static: false }) private input: MatInput;
@ViewChild('symbolAutocomplete') public symbolAutocomplete: MatAutocomplete;
public control = new FormControl();
public filteredLookupItems: LookupItem[] = [];
public filteredLookupItemsObservable: Observable<LookupItem[]> = of([]);
private unsubscribeSubject = new Subject<void>();
public constructor(
public readonly _elementRef: ElementRef,
public readonly _focusMonitor: FocusMonitor,
public readonly changeDetectorRef: ChangeDetectorRef,
public readonly dataService: DataService,
public readonly ngControl: NgControl
) {
super(_elementRef, _focusMonitor, ngControl);
this.controlType = 'symbol-autocomplete';
}
public ngOnInit() {
super.required = this.ngControl.control?.hasValidator(Validators.required);
if (this.disabled) {
this.control.disable();
}
this.control.valueChanges
.pipe(
debounceTime(400),
distinctUntilChanged(),
filter((query) => {
return isString(query) && query.length > 1;
}),
tap(() => {
this.isLoading = true;
this.changeDetectorRef.markForCheck();
}),
switchMap((query: string) => {
return this.dataService.fetchSymbols(query);
})
)
.subscribe((filteredLookupItems) => {
this.filteredLookupItems = filteredLookupItems;
this.isLoading = false;
this.changeDetectorRef.markForCheck();
});
}
public displayFn(aLookupItem: LookupItem) {
return aLookupItem?.symbol ?? '';
}
public get empty() {
return this.input?.empty;
}
public focus() {
this.input.focus();
}
public isValueInOptions(value: string) {
return this.filteredLookupItems.some((item) => {
return item.symbol === value;
});
}
public ngDoCheck() {
if (this.ngControl) {
this.validateRequired();
this.validateSelection();
this.errorState = this.ngControl.invalid && this.ngControl.touched;
this.stateChanges.next();
}
}
public onUpdateSymbol(event: MatAutocompleteSelectedEvent) {
super.value = {
dataSource: event.option.value.dataSource,
symbol: event.option.value.symbol
} as LookupItem;
}
public set value(value: LookupItem) {
this.control.setValue(value);
super.value = value;
}
public ngOnDestroy() {
super.ngOnDestroy();
this.unsubscribeSubject.next();
this.unsubscribeSubject.complete();
}
private validateRequired() {
const requiredCheck = super.required
? !super.value?.dataSource || !super.value?.symbol
: false;
if (requiredCheck) {
this.ngControl.control.setErrors({ invalidData: true });
}
}
private validateSelection() {
const error =
!this.isValueInOptions(this.input?.value) ||
this.input?.value !== super.value?.symbol;
if (error) {
this.ngControl.control.setErrors({ invalidData: true });
}
}
}

26
libs/ui/src/lib/symbol-autocomplete/symbol-autocomplete.module.ts

@ -0,0 +1,26 @@
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 { MatProgressSpinnerModule } from '@angular/material/progress-spinner';
import { GfSymbolModule } from '@ghostfolio/client/pipes/symbol/symbol.module';
import { SymbolAutocompleteComponent } from '@ghostfolio/ui/symbol-autocomplete/symbol-autocomplete.component';
@NgModule({
declarations: [SymbolAutocompleteComponent],
exports: [SymbolAutocompleteComponent],
imports: [
CommonModule,
FormsModule,
GfSymbolModule,
MatAutocompleteModule,
MatFormFieldModule,
MatInputModule,
MatProgressSpinnerModule,
ReactiveFormsModule
],
schemas: [CUSTOM_ELEMENTS_SCHEMA]
})
export class GfSymbolAutocompleteModule {}

4
package.json

@ -79,7 +79,7 @@
"@nestjs/platform-express": "9.1.4",
"@nestjs/schedule": "2.1.0",
"@nestjs/serve-static": "3.0.0",
"@prisma/client": "4.14.1",
"@prisma/client": "4.15.0",
"@simplewebauthn/browser": "5.2.1",
"@simplewebauthn/server": "5.2.1",
"@stripe/stripe-js": "1.47.0",
@ -119,7 +119,7 @@
"passport": "0.6.0",
"passport-google-oauth20": "2.0.0",
"passport-jwt": "4.0.0",
"prisma": "4.14.1",
"prisma": "4.15.0",
"reflect-metadata": "0.1.13",
"rxjs": "7.5.6",
"stripe": "11.12.0",

36
yarn.lock

@ -3936,22 +3936,22 @@
resolved "https://registry.yarnpkg.com/@pkgjs/parseargs/-/parseargs-0.11.0.tgz#a77ea742fab25775145434eb1d2328cf5013ac33"
integrity sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==
"@prisma/client@4.14.1":
version "4.14.1"
resolved "https://registry.yarnpkg.com/@prisma/client/-/client-4.14.1.tgz#61720f385f687f7e88de41fccade1ed62be57a54"
integrity sha512-TZIswkeX1ccsHG/eN2kICzg/csXll0osK3EHu1QKd8VJ3XLcXozbNELKkCNfsCUvKJAwPdDtFCzF+O+raIVldw==
"@prisma/client@4.15.0":
version "4.15.0"
resolved "https://registry.yarnpkg.com/@prisma/client/-/client-4.15.0.tgz#f52ec6ca6fbde37395a54b0a9e5da603a9de15f3"
integrity sha512-xnROvyABcGiwqRNdrObHVZkD9EjkJYHOmVdlKy1yGgI+XOzvMzJ4tRg3dz1pUlsyhKxXGCnjIQjWW+2ur+YXuw==
dependencies:
"@prisma/engines-version" "4.14.0-67.d9a4c5988f480fa576d43970d5a23641aa77bc9c"
"@prisma/engines-version" "4.15.0-28.8fbc245156db7124f997f4cecdd8d1219e360944"
"@prisma/engines-version@4.14.0-67.d9a4c5988f480fa576d43970d5a23641aa77bc9c":
version "4.14.0-67.d9a4c5988f480fa576d43970d5a23641aa77bc9c"
resolved "https://registry.yarnpkg.com/@prisma/engines-version/-/engines-version-4.14.0-67.d9a4c5988f480fa576d43970d5a23641aa77bc9c.tgz#0aeca447c4a5f23c83f68b8033e627b60bc01850"
integrity sha512-3jum8/YSudeSN0zGW5qkpz+wAN2V/NYCQ+BPjvHYDfWatLWlQkqy99toX0GysDeaUoBIJg1vaz2yKqiA3CFcQw==
"@prisma/engines-version@4.15.0-28.8fbc245156db7124f997f4cecdd8d1219e360944":
version "4.15.0-28.8fbc245156db7124f997f4cecdd8d1219e360944"
resolved "https://registry.yarnpkg.com/@prisma/engines-version/-/engines-version-4.15.0-28.8fbc245156db7124f997f4cecdd8d1219e360944.tgz#8d880becf996cffe08c78ad5afab6bc06090c990"
integrity sha512-sVOig4tjGxxlYaFcXgE71f/rtFhzyYrfyfNFUsxCIEJyVKU9rdOWIlIwQ2NQ7PntvGnn+x0XuFo4OC1jvPJKzg==
"@prisma/engines@4.14.1":
version "4.14.1"
resolved "https://registry.yarnpkg.com/@prisma/engines/-/engines-4.14.1.tgz#dac49f8d1f2d4f14a8ed7e6f96b24cd49bd6cd91"
integrity sha512-APqFddPVHYmWNKqc+5J5SqrLFfOghKOLZxobmguDUacxOwdEutLsbXPVhNnpFDmuQWQFbXmrTTPoRrrF6B1MWA==
"@prisma/engines@4.15.0":
version "4.15.0"
resolved "https://registry.yarnpkg.com/@prisma/engines/-/engines-4.15.0.tgz#d8687a9fda615fab88b75b466931280289de9e26"
integrity sha512-FTaOCGs0LL0OW68juZlGxFtYviZa4xdQj/rQEdat2txw0s3Vu/saAPKjNVXfIgUsGXmQ72HPgNr6935/P8FNAA==
"@samverschueren/stream-to-observable@^0.3.0":
version "0.3.1"
@ -15184,12 +15184,12 @@ pretty-hrtime@^1.0.3:
resolved "https://registry.yarnpkg.com/pretty-hrtime/-/pretty-hrtime-1.0.3.tgz#b7e3ea42435a4c9b2759d99e0f201eb195802ee1"
integrity sha512-66hKPCr+72mlfiSjlEB1+45IjXSqvVAIy6mocupoww4tBFE9R9IhwwUGoI4G++Tc9Aq+2rxOt0RFU6gPcrte0A==
prisma@4.14.1:
version "4.14.1"
resolved "https://registry.yarnpkg.com/prisma/-/prisma-4.14.1.tgz#7a6bb4ce847a9d08deabb6acdf3116fff15e1316"
integrity sha512-z6hxzTMYqT9SIKlzD08dhzsLUpxjFKKsLpp5/kBDnSqiOjtUyyl/dC5tzxLcOa3jkEHQ8+RpB/fE3w8bgNP51g==
prisma@4.15.0:
version "4.15.0"
resolved "https://registry.yarnpkg.com/prisma/-/prisma-4.15.0.tgz#4faa94f0d584828b68468953ff0bc88f37912c8c"
integrity sha512-iKZZpobPl48gTcSZVawLMQ3lEy6BnXwtoMj7hluoGFYu2kQ6F9LBuBrUyF95zRVnNo8/3KzLXJXJ5TEnLSJFiA==
dependencies:
"@prisma/engines" "4.14.1"
"@prisma/engines" "4.15.0"
prismjs@^1.28.0:
version "1.29.0"

Loading…
Cancel
Save