From 8a862ac2e8fb9ecd6c64fe46613ccd9a799cc30c Mon Sep 17 00:00:00 2001 From: Thomas <4159106+dtslvr@users.noreply.github.com> Date: Fri, 6 May 2022 20:58:16 +0200 Subject: [PATCH] Refactor filtering with an interface --- apps/api/src/app/order/order.service.ts | 2 +- .../allocations/allocations-page.component.ts | 17 ++-- .../allocations/allocations-page.html | 4 +- apps/client/src/app/services/data.service.ts | 17 +++- .../src/lib/interfaces/filter.interface.ts | 5 ++ libs/common/src/lib/interfaces/index.ts | 2 + .../activities-filter.component.html | 12 +-- .../activities-filter.component.ts | 46 +++++------ .../activities-table.component.html | 3 +- .../activities-table.component.ts | 79 +++++++++++-------- 10 files changed, 111 insertions(+), 76 deletions(-) create mode 100644 libs/common/src/lib/interfaces/filter.interface.ts diff --git a/apps/api/src/app/order/order.service.ts b/apps/api/src/app/order/order.service.ts index 762b414ba..0210284f8 100644 --- a/apps/api/src/app/order/order.service.ts +++ b/apps/api/src/app/order/order.service.ts @@ -189,7 +189,7 @@ export class OrderService { some: { OR: tags.map((tag) => { return { - name: tag + id: tag }; }) } diff --git a/apps/client/src/app/pages/portfolio/allocations/allocations-page.component.ts b/apps/client/src/app/pages/portfolio/allocations/allocations-page.component.ts index b8842cea0..82334be68 100644 --- a/apps/client/src/app/pages/portfolio/allocations/allocations-page.component.ts +++ b/apps/client/src/app/pages/portfolio/allocations/allocations-page.component.ts @@ -8,6 +8,7 @@ import { UserService } from '@ghostfolio/client/services/user/user.service'; import { UNKNOWN_KEY } from '@ghostfolio/common/config'; import { prettifySymbol } from '@ghostfolio/common/helper'; import { + Filter, PortfolioDetails, PortfolioPosition, UniqueAsset, @@ -32,6 +33,7 @@ export class AllocationsPageComponent implements OnDestroy, OnInit { value: number; }; }; + public allFilters: Filter[]; public continents: { [code: string]: { name: string; value: number }; }; @@ -39,7 +41,7 @@ export class AllocationsPageComponent implements OnDestroy, OnInit { [code: string]: { name: string; value: number }; }; public deviceType: string; - public filters$ = new Subject(); + public filters$ = new Subject(); public hasImpersonationId: boolean; public isLoading = false; public markets: { @@ -76,7 +78,6 @@ export class AllocationsPageComponent implements OnDestroy, OnInit { value: number; }; }; - public tags: string[] = []; public user: User; @@ -127,10 +128,10 @@ export class AllocationsPageComponent implements OnDestroy, OnInit { this.filters$ .pipe( distinctUntilChanged(), - switchMap((tags) => { + switchMap((filters) => { this.isLoading = true; - return this.dataService.fetchPortfolioDetails({ tags }); + return this.dataService.fetchPortfolioDetails({ filters }); }), takeUntil(this.unsubscribeSubject) ) @@ -150,8 +151,12 @@ export class AllocationsPageComponent implements OnDestroy, OnInit { if (state?.user) { this.user = state.user; - this.tags = this.user.tags.map((tag) => { - return tag.name; + this.allFilters = this.user.tags.map((tag) => { + return { + id: tag.id, + label: tag.name, + type: 'tag' + }; }); this.changeDetectorRef.markForCheck(); diff --git a/apps/client/src/app/pages/portfolio/allocations/allocations-page.html b/apps/client/src/app/pages/portfolio/allocations/allocations-page.html index 74b9330e2..c7fd9854f 100644 --- a/apps/client/src/app/pages/portfolio/allocations/allocations-page.html +++ b/apps/client/src/app/pages/portfolio/allocations/allocations-page.html @@ -3,9 +3,9 @@

Allocations

diff --git a/apps/client/src/app/services/data.service.ts b/apps/client/src/app/services/data.service.ts index c59982824..6c7d2d022 100644 --- a/apps/client/src/app/services/data.service.ts +++ b/apps/client/src/app/services/data.service.ts @@ -19,6 +19,7 @@ import { AdminData, AdminMarketData, Export, + Filter, InfoItem, PortfolioChart, PortfolioDetails, @@ -182,11 +183,21 @@ export class DataService { ); } - public fetchPortfolioDetails({ tags }: { tags?: string[] }) { + public fetchPortfolioDetails({ filters }: { filters?: Filter[] }) { let params = new HttpParams(); - if (tags?.length > 0) { - params = params.append('tags', tags.join(',')); + if (filters?.length > 0) { + params = params.append( + 'tags', + filters + .filter((filter) => { + return filter.type === 'tag'; + }) + .map((filter) => { + return filter.id; + }) + .join(',') + ); } return this.http.get('/api/v1/portfolio/details', { diff --git a/libs/common/src/lib/interfaces/filter.interface.ts b/libs/common/src/lib/interfaces/filter.interface.ts new file mode 100644 index 000000000..4aee7bf91 --- /dev/null +++ b/libs/common/src/lib/interfaces/filter.interface.ts @@ -0,0 +1,5 @@ +export interface Filter { + id: string; + label: string; + type: 'tag'; +} diff --git a/libs/common/src/lib/interfaces/index.ts b/libs/common/src/lib/interfaces/index.ts index d2ad50742..0b20b8f23 100644 --- a/libs/common/src/lib/interfaces/index.ts +++ b/libs/common/src/lib/interfaces/index.ts @@ -8,6 +8,7 @@ import { } from './admin-market-data.interface'; import { Coupon } from './coupon.interface'; import { Export } from './export.interface'; +import { Filter } from './filter.interface'; import { InfoItem } from './info-item.interface'; import { PortfolioChart } from './portfolio-chart.interface'; import { PortfolioDetails } from './portfolio-details.interface'; @@ -38,6 +39,7 @@ export { AdminMarketDataItem, Coupon, Export, + Filter, InfoItem, PortfolioChart, PortfolioDetails, diff --git a/libs/ui/src/lib/activities-filter/activities-filter.component.html b/libs/ui/src/lib/activities-filter/activities-filter.component.html index 98c40d4b8..209e37391 100644 --- a/libs/ui/src/lib/activities-filter/activities-filter.component.html +++ b/libs/ui/src/lib/activities-filter/activities-filter.component.html @@ -2,13 +2,13 @@ - {{ searchKeyword | gfSymbol }} + {{ searchKeyword.label | gfSymbol }} - {{ filter | gfSymbol }} + {{ filter.label | gfSymbol }} (); + @Output() valueChanged = new EventEmitter(); @ViewChild('autocomplete') matAutocomplete: MatAutocomplete; @ViewChild('searchInput') searchInput: ElementRef; - public filters$: Subject = new BehaviorSubject([]); - public filters: Observable = this.filters$.asObservable(); + public filters$: Subject = new BehaviorSubject([]); + public filters: Observable = this.filters$.asObservable(); public searchControl = new FormControl(); - public searchKeywords: string[] = []; + public searchFilters: Filter[] = []; public separatorKeysCodes: number[] = [ENTER, COMMA]; private unsubscribeSubject = new Subject(); @@ -47,13 +48,10 @@ export class ActivitiesFilterComponent implements OnChanges, OnDestroy { public constructor() { this.searchControl.valueChanges .pipe(takeUntil(this.unsubscribeSubject)) - .subscribe((keyword) => { - if (keyword) { - const filterValue = keyword.toLowerCase(); + .subscribe((currentFilter) => { + if (currentFilter) { this.filters$.next( - this.allFilters.filter( - (filter) => filter.toLowerCase().indexOf(filterValue) === 0 - ) + this.allFilters.filter((filter) => filter.id === currentFilter.id) ); } else { this.filters$.next(this.allFilters); @@ -67,9 +65,8 @@ export class ActivitiesFilterComponent implements OnChanges, OnDestroy { } } - public addKeyword({ input, value }: MatChipInputEvent): void { + public onAddFilter({ input, value }: MatChipInputEvent): void { if (value?.trim()) { - this.searchKeywords.push(value.trim()); this.updateFilter(); } @@ -81,20 +78,19 @@ export class ActivitiesFilterComponent implements OnChanges, OnDestroy { this.searchControl.setValue(null); } - public keywordSelected(event: MatAutocompleteSelectedEvent): void { - this.searchKeywords.push(event.option.viewValue); + public onRemoveFilter(aFilter: Filter): void { + this.searchFilters = this.searchFilters.filter((filter) => { + return filter.id !== aFilter.id; + }); + 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 onSelectFilter(event: MatAutocompleteSelectedEvent): void { + this.searchFilters.push(event.option.value); + this.updateFilter(); + this.searchInput.nativeElement.value = ''; + this.searchControl.setValue(null); } public ngOnDestroy() { @@ -106,6 +102,6 @@ export class ActivitiesFilterComponent implements OnChanges, OnDestroy { this.filters$.next(this.allFilters); // Emit an array with a new reference - this.valueChanged.emit([...this.searchKeywords]); + this.valueChanged.emit([...this.searchFilters]); } } 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 a047053dd..60948d60d 100644 --- a/libs/ui/src/lib/activities-table/activities-table.component.html +++ b/libs/ui/src/lib/activities-table/activities-table.component.html @@ -1,8 +1,9 @@
diff --git a/libs/ui/src/lib/activities-table/activities-table.component.ts b/libs/ui/src/lib/activities-table/activities-table.component.ts index 03e8cf048..f2fd54bb8 100644 --- a/libs/ui/src/lib/activities-table/activities-table.component.ts +++ b/libs/ui/src/lib/activities-table/activities-table.component.ts @@ -14,13 +14,13 @@ import { MatTableDataSource } from '@angular/material/table'; import { Router } from '@angular/router'; import { Activity } from '@ghostfolio/api/app/order/interfaces/activities.interface'; import { getDateFormatString } from '@ghostfolio/common/helper'; -import { UniqueAsset } from '@ghostfolio/common/interfaces'; +import { Filter, UniqueAsset } from '@ghostfolio/common/interfaces'; import { OrderWithAccount } from '@ghostfolio/common/types'; import Big from 'big.js'; import { isUUID } from 'class-validator'; import { endOfToday, format, isAfter } from 'date-fns'; import { isNumber } from 'lodash'; -import { Subject, Subscription } from 'rxjs'; +import { distinctUntilChanged, Subject, Subscription, takeUntil } from 'rxjs'; const SEARCH_PLACEHOLDER = 'Search for account, currency, symbol or type...'; const SEARCH_STRING_SEPARATOR = ','; @@ -53,11 +53,12 @@ export class ActivitiesTableComponent implements OnChanges, OnDestroy { @ViewChild(MatSort) sort: MatSort; - public allFilters: string[]; + public allFilters: Filter[]; public dataSource: MatTableDataSource = new MatTableDataSource(); public defaultDateFormat: string; public displayedColumns = []; public endOfToday = endOfToday(); + public filters$ = new Subject(); public hasDrafts = false; public isAfter = isAfter; public isLoading = true; @@ -71,7 +72,13 @@ export class ActivitiesTableComponent implements OnChanges, OnDestroy { private unsubscribeSubject = new Subject(); - public constructor(private router: Router) {} + public constructor(private router: Router) { + this.filters$ + .pipe(distinctUntilChanged(), takeUntil(this.unsubscribeSubject)) + .subscribe((filters) => { + this.updateFilters(filters); + }); + } public ngOnChanges() { this.displayedColumns = [ @@ -95,11 +102,15 @@ export class ActivitiesTableComponent implements OnChanges, OnDestroy { }); } - this.isLoading = true; - this.defaultDateFormat = getDateFormatString(this.locale); if (this.activities) { + this.allFilters = this.getSearchableFieldValues(this.activities).map( + (label) => { + return { label, id: label, type: 'tag' }; + } + ); + this.dataSource = new MatTableDataSource(this.activities); this.dataSource.filterPredicate = (data, filter) => { const dataString = this.getFilterableValues(data) @@ -113,8 +124,8 @@ export class ActivitiesTableComponent implements OnChanges, OnDestroy { return contains; }; this.dataSource.sort = this.sort; - this.updateFilter(); - this.isLoading = false; + + this.updateFilters(); } } @@ -172,30 +183,6 @@ export class ActivitiesTableComponent implements OnChanges, OnDestroy { this.activityToUpdate.emit(aActivity); } - 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.hasDrafts = this.dataSource.data.some((activity) => { - return activity.isDraft === true; - }); - this.totalFees = this.getTotalFees(); - this.totalValue = this.getTotalValue(); - } - public ngOnDestroy() { this.unsubscribeSubject.next(); this.unsubscribeSubject.complete(); @@ -280,4 +267,32 @@ export class ActivitiesTableComponent implements OnChanges, OnDestroy { return totalValue.toNumber(); } + + private updateFilters(filters: Filter[] = []) { + this.isLoading = true; + + this.dataSource.filter = filters + .map((filter) => { + return filter.label; + }) + .join(SEARCH_STRING_SEPARATOR); + const lowercaseSearchKeywords = filters.map((filter) => { + return filter.label.trim().toLowerCase(); + }); + + this.placeholder = + lowercaseSearchKeywords.length <= 0 ? SEARCH_PLACEHOLDER : ''; + + this.searchKeywords = filters.map((filter) => { + return filter.label; + }); + + this.hasDrafts = this.dataSource.filteredData.some((activity) => { + return activity.isDraft === true; + }); + this.totalFees = this.getTotalFees(); + this.totalValue = this.getTotalValue(); + + this.isLoading = false; + } }