From 8f61f7c1697acb6a79a361af3e02ad5dd857b959 Mon Sep 17 00:00:00 2001 From: Thomas Kaul <4159106+dtslvr@users.noreply.github.com> Date: Sat, 23 Apr 2022 19:22:20 +0200 Subject: [PATCH] Feature/extract activities table filter component (#858) * Extract activities table component * Update changelog --- CHANGELOG.md | 1 + .../activities-filter.component.html | 33 +++++ .../activities-filter.component.scss | 22 ++++ .../activities-filter.component.ts | 108 +++++++++++++++ .../activities-filter.module.ts | 24 ++++ .../activities-table.component.html | 41 +----- .../activities-table.component.scss | 15 --- .../activities-table.component.ts | 124 +++++------------- .../activities-table.module.ts | 10 +- 9 files changed, 228 insertions(+), 150 deletions(-) create mode 100644 libs/ui/src/lib/activities-filter/activities-filter.component.html create mode 100644 libs/ui/src/lib/activities-filter/activities-filter.component.scss create mode 100644 libs/ui/src/lib/activities-filter/activities-filter.component.ts create mode 100644 libs/ui/src/lib/activities-filter/activities-filter.module.ts diff --git a/CHANGELOG.md b/CHANGELOG.md index 413e5bda7..43fb36495 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,6 +9,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Changed +- Extracted the activities table filter to a dedicated component - Changed the url of the _Get Started_ link to `https://ghostfol.io` on the public page - Upgraded `prisma` from version `3.11.1` to `3.12.0` diff --git a/libs/ui/src/lib/activities-filter/activities-filter.component.html b/libs/ui/src/lib/activities-filter/activities-filter.component.html new file mode 100644 index 000000000..ff39a6750 --- /dev/null +++ b/libs/ui/src/lib/activities-filter/activities-filter.component.html @@ -0,0 +1,33 @@ + + + + + {{ searchKeyword | gfSymbol }} + + + + + + + {{ filter | gfSymbol }} + + + diff --git a/libs/ui/src/lib/activities-filter/activities-filter.component.scss b/libs/ui/src/lib/activities-filter/activities-filter.component.scss new file mode 100644 index 000000000..f40444914 --- /dev/null +++ b/libs/ui/src/lib/activities-filter/activities-filter.component.scss @@ -0,0 +1,22 @@ +@import '~apps/client/src/styles/ghostfolio-style'; + +:host { + display: block; + + ::ng-deep { + .mat-form-field-infix { + border-top: 0 solid transparent !important; + } + } + + .mat-chip { + cursor: pointer; + min-height: 1.5rem !important; + } +} + +:host-context(.is-dark-theme) { + .mat-form-field { + color: rgba(var(--light-primary-text)); + } +} diff --git a/libs/ui/src/lib/activities-filter/activities-filter.component.ts b/libs/ui/src/lib/activities-filter/activities-filter.component.ts new file mode 100644 index 000000000..6e8eea64f --- /dev/null +++ b/libs/ui/src/lib/activities-filter/activities-filter.component.ts @@ -0,0 +1,108 @@ +import { COMMA, ENTER } from '@angular/cdk/keycodes'; +import { + ChangeDetectionStrategy, + Component, + ElementRef, + EventEmitter, + Input, + OnChanges, + OnDestroy, + Output, + ViewChild +} from '@angular/core'; +import { FormControl } from '@angular/forms'; +import { + MatAutocomplete, + MatAutocompleteSelectedEvent +} from '@angular/material/autocomplete'; +import { MatChipInputEvent } from '@angular/material/chips'; +import { BehaviorSubject, Observable, Subject } from 'rxjs'; +import { takeUntil } from 'rxjs/operators'; + +@Component({ + changeDetection: ChangeDetectionStrategy.OnPush, + selector: 'gf-activities-filter', + styleUrls: ['./activities-filter.component.scss'], + templateUrl: './activities-filter.component.html' +}) +export class ActivitiesFilterComponent implements OnChanges, OnDestroy { + @Input() allFilters: string[]; + @Input() placeholder: string; + + @Output() valueChanged = new EventEmitter(); + + @ViewChild('autocomplete') matAutocomplete: MatAutocomplete; + @ViewChild('searchInput') searchInput: ElementRef; + + public filters$: Subject = new BehaviorSubject([]); + public filters: Observable = this.filters$.asObservable(); + public searchControl = new FormControl(); + public searchKeywords: string[] = []; + public separatorKeysCodes: number[] = [ENTER, COMMA]; + + private unsubscribeSubject = new Subject(); + + public constructor() { + this.searchControl.valueChanges + .pipe(takeUntil(this.unsubscribeSubject)) + .subscribe((keyword) => { + if (keyword) { + const filterValue = keyword.toLowerCase(); + this.filters$.next( + this.allFilters.filter( + (filter) => filter.toLowerCase().indexOf(filterValue) === 0 + ) + ); + } else { + this.filters$.next(this.allFilters); + } + }); + } + + public ngOnChanges() { + if (this.allFilters) { + this.updateFilter(); + } + } + + public addKeyword({ input, value }: MatChipInputEvent): void { + if (value?.trim()) { + this.searchKeywords.push(value.trim()); + this.updateFilter(); + } + + // Reset the input value + if (input) { + input.value = ''; + } + + this.searchControl.setValue(null); + } + + public keywordSelected(event: MatAutocompleteSelectedEvent): void { + this.searchKeywords.push(event.option.viewValue); + this.updateFilter(); + this.searchInput.nativeElement.value = ''; + this.searchControl.setValue(null); + } + + public removeKeyword(keyword: string): void { + const index = this.searchKeywords.indexOf(keyword); + + if (index >= 0) { + this.searchKeywords.splice(index, 1); + this.updateFilter(); + } + } + + public ngOnDestroy() { + this.unsubscribeSubject.next(); + this.unsubscribeSubject.complete(); + } + + private updateFilter() { + this.filters$.next(this.allFilters); + + this.valueChanged.emit(this.searchKeywords); + } +} diff --git a/libs/ui/src/lib/activities-filter/activities-filter.module.ts b/libs/ui/src/lib/activities-filter/activities-filter.module.ts new file mode 100644 index 000000000..a192296fd --- /dev/null +++ b/libs/ui/src/lib/activities-filter/activities-filter.module.ts @@ -0,0 +1,24 @@ +import { CommonModule } from '@angular/common'; +import { CUSTOM_ELEMENTS_SCHEMA, NgModule } from '@angular/core'; +import { ReactiveFormsModule } from '@angular/forms'; +import { MatAutocompleteModule } from '@angular/material/autocomplete'; +import { MatChipsModule } from '@angular/material/chips'; +import { MatInputModule } from '@angular/material/input'; +import { GfSymbolModule } from '@ghostfolio/client/pipes/symbol/symbol.module'; + +import { ActivitiesFilterComponent } from './activities-filter.component'; + +@NgModule({ + declarations: [ActivitiesFilterComponent], + exports: [ActivitiesFilterComponent], + imports: [ + CommonModule, + GfSymbolModule, + MatAutocompleteModule, + MatChipsModule, + MatInputModule, + ReactiveFormsModule + ], + schemas: [CUSTOM_ELEMENTS_SCHEMA] +}) +export class GfActivitiesFilterModule {} diff --git a/libs/ui/src/lib/activities-table/activities-table.component.html b/libs/ui/src/lib/activities-table/activities-table.component.html index cd21ca88c..a047053dd 100644 --- a/libs/ui/src/lib/activities-table/activities-table.component.html +++ b/libs/ui/src/lib/activities-table/activities-table.component.html @@ -1,40 +1,9 @@ - - - - - {{ searchKeyword | gfSymbol }} - - - - - - - {{ filter | gfSymbol }} - - - + [placeholder]="placeholder" + (valueChanged)="updateFilter($event)" +>
(); @Output() import = new EventEmitter(); - @ViewChild('autocomplete') matAutocomplete: MatAutocomplete; - @ViewChild('searchInput') searchInput: ElementRef; @ViewChild(MatSort) sort: MatSort; + public allFilters: string[]; public dataSource: MatTableDataSource = new MatTableDataSource(); public defaultDateFormat: string; public displayedColumns = []; public endOfToday = endOfToday(); - public filters$: Subject = new BehaviorSubject([]); - public filters: Observable = this.filters$.asObservable(); public hasDrafts = false; public isAfter = isAfter; public isLoading = true; @@ -77,59 +66,12 @@ export class ActivitiesTableComponent implements OnChanges, OnDestroy { public routeQueryParams: Subscription; public searchControl = new FormControl(); public searchKeywords: string[] = []; - public separatorKeysCodes: number[] = [ENTER, COMMA]; public totalFees: number; public totalValue: number; - private allFilters: string[]; private unsubscribeSubject = new Subject(); - public constructor(private router: Router) { - this.searchControl.valueChanges - .pipe(takeUntil(this.unsubscribeSubject)) - .subscribe((keyword) => { - if (keyword) { - const filterValue = keyword.toLowerCase(); - this.filters$.next( - this.allFilters.filter( - (filter) => filter.toLowerCase().indexOf(filterValue) === 0 - ) - ); - } else { - this.filters$.next(this.allFilters); - } - }); - } - - public addKeyword({ input, value }: MatChipInputEvent): void { - if (value?.trim()) { - this.searchKeywords.push(value.trim()); - this.updateFilter(); - } - - // Reset the input value - if (input) { - input.value = ''; - } - - this.searchControl.setValue(null); - } - - public removeKeyword(keyword: string): void { - const index = this.searchKeywords.indexOf(keyword); - - if (index >= 0) { - this.searchKeywords.splice(index, 1); - this.updateFilter(); - } - } - - public keywordSelected(event: MatAutocompleteSelectedEvent): void { - this.searchKeywords.push(event.option.viewValue); - this.updateFilter(); - this.searchInput.nativeElement.value = ''; - this.searchControl.setValue(null); - } + public constructor(private router: Router) {} public ngOnChanges() { this.displayedColumns = [ @@ -230,28 +172,23 @@ export class ActivitiesTableComponent implements OnChanges, OnDestroy { this.activityToUpdate.emit(aActivity); } - public ngOnDestroy() { - this.unsubscribeSubject.next(); - this.unsubscribeSubject.complete(); - } - - private updateFilter() { - this.dataSource.filter = this.searchKeywords.join(SEARCH_STRING_SEPARATOR); - const lowercaseSearchKeywords = this.searchKeywords.map((keyword) => + public updateFilter(filters: string[] = []) { + this.dataSource.filter = filters.join(SEARCH_STRING_SEPARATOR); + const lowercaseSearchKeywords = filters.map((keyword) => keyword.trim().toLowerCase() ); this.placeholder = lowercaseSearchKeywords.length <= 0 ? SEARCH_PLACEHOLDER : ''; + this.searchKeywords = filters; + this.allFilters = this.getSearchableFieldValues(this.activities).filter( (item) => { return !lowercaseSearchKeywords.includes(item.trim().toLowerCase()); } ); - this.filters$.next(this.allFilters); - this.hasDrafts = this.dataSource.data.some((activity) => { return activity.isDraft === true; }); @@ -259,6 +196,31 @@ export class ActivitiesTableComponent implements OnChanges, OnDestroy { this.totalValue = this.getTotalValue(); } + public ngOnDestroy() { + this.unsubscribeSubject.next(); + this.unsubscribeSubject.complete(); + } + + private getFilterableValues( + activity: OrderWithAccount, + fieldValues: Set = new Set() + ): string[] { + fieldValues.add(activity.Account?.name); + fieldValues.add(activity.Account?.Platform?.name); + fieldValues.add(activity.SymbolProfile.currency); + + if (!isUUID(activity.SymbolProfile.symbol)) { + fieldValues.add(activity.SymbolProfile.symbol); + } + + fieldValues.add(activity.type); + fieldValues.add(format(activity.date, 'yyyy')); + + return [...fieldValues].filter((item) => { + return item !== undefined; + }); + } + private getSearchableFieldValues(activities: OrderWithAccount[]): string[] { const fieldValues = new Set(); @@ -287,26 +249,6 @@ export class ActivitiesTableComponent implements OnChanges, OnDestroy { }); } - private getFilterableValues( - activity: OrderWithAccount, - fieldValues: Set = new Set() - ): string[] { - fieldValues.add(activity.Account?.name); - fieldValues.add(activity.Account?.Platform?.name); - fieldValues.add(activity.SymbolProfile.currency); - - if (!isUUID(activity.SymbolProfile.symbol)) { - fieldValues.add(activity.SymbolProfile.symbol); - } - - fieldValues.add(activity.type); - fieldValues.add(format(activity.date, 'yyyy')); - - return [...fieldValues].filter((item) => { - return item !== undefined; - }); - } - private getTotalFees() { let totalFees = new Big(0); diff --git a/libs/ui/src/lib/activities-table/activities-table.module.ts b/libs/ui/src/lib/activities-table/activities-table.module.ts index 56947d7e9..2d009fb4f 100644 --- a/libs/ui/src/lib/activities-table/activities-table.module.ts +++ b/libs/ui/src/lib/activities-table/activities-table.module.ts @@ -1,16 +1,13 @@ import { CommonModule } from '@angular/common'; import { CUSTOM_ELEMENTS_SCHEMA, NgModule } from '@angular/core'; -import { ReactiveFormsModule } from '@angular/forms'; -import { MatAutocompleteModule } from '@angular/material/autocomplete'; import { MatButtonModule } from '@angular/material/button'; -import { MatChipsModule } from '@angular/material/chips'; -import { MatInputModule } from '@angular/material/input'; import { MatMenuModule } from '@angular/material/menu'; import { MatSortModule } from '@angular/material/sort'; import { MatTableModule } from '@angular/material/table'; import { RouterModule } from '@angular/router'; import { GfSymbolIconModule } from '@ghostfolio/client/components/symbol-icon/symbol-icon.module'; import { GfSymbolModule } from '@ghostfolio/client/pipes/symbol/symbol.module'; +import { GfActivitiesFilterModule } from '@ghostfolio/ui/activities-filter/activities-filter.module'; import { GfNoTransactionsInfoModule } from '@ghostfolio/ui/no-transactions-info'; import { GfValueModule } from '@ghostfolio/ui/value'; import { NgxSkeletonLoaderModule } from 'ngx-skeleton-loader'; @@ -22,19 +19,16 @@ import { ActivitiesTableComponent } from './activities-table.component'; exports: [ActivitiesTableComponent], imports: [ CommonModule, + GfActivitiesFilterModule, GfNoTransactionsInfoModule, GfSymbolIconModule, GfSymbolModule, GfValueModule, - MatAutocompleteModule, MatButtonModule, - MatChipsModule, - MatInputModule, MatMenuModule, MatSortModule, MatTableModule, NgxSkeletonLoaderModule, - ReactiveFormsModule, RouterModule ], schemas: [CUSTOM_ELEMENTS_SCHEMA]