From 160335302a5a5455296b43b0a8b6350ce0a76feb Mon Sep 17 00:00:00 2001 From: Thomas Kaul <4159106+dtslvr@users.noreply.github.com> Date: Sun, 15 May 2022 21:51:31 +0200 Subject: [PATCH] Feature/group filters by type (#922) * Add groups to activities filter component * Update changelog --- CHANGELOG.md | 6 ++ apps/api/src/app/account/account.service.ts | 2 +- apps/api/src/app/order/order.service.ts | 2 +- .../src/app/portfolio/portfolio.controller.ts | 4 +- .../allocations/allocations-page.component.ts | 4 +- apps/client/src/app/services/data.service.ts | 2 +- .../lib/interfaces/filter-group.interface.ts | 6 ++ .../src/lib/interfaces/filter.interface.ts | 2 +- libs/common/src/lib/interfaces/index.ts | 2 + .../activities-filter.component.html | 14 ++- .../activities-filter.component.ts | 86 ++++++++++++------- .../activities-table.component.ts | 78 +++++++++-------- 12 files changed, 130 insertions(+), 78 deletions(-) create mode 100644 libs/common/src/lib/interfaces/filter-group.interface.ts diff --git a/CHANGELOG.md b/CHANGELOG.md index d95052f7..baf1b856 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 + +### Added + +- Added groups to the activities filter component + ## 1.148.0 - 14.05.2022 ### Added diff --git a/apps/api/src/app/account/account.service.ts b/apps/api/src/app/account/account.service.ts index 34c2f4a1..240045e8 100644 --- a/apps/api/src/app/account/account.service.ts +++ b/apps/api/src/app/account/account.service.ts @@ -120,7 +120,7 @@ export class AccountService { where.id = { in: filters .filter(({ type }) => { - return type === 'account'; + return type === 'ACCOUNT'; }) .map(({ id }) => { return id; diff --git a/apps/api/src/app/order/order.service.ts b/apps/api/src/app/order/order.service.ts index aceb8de0..3e87c5f5 100644 --- a/apps/api/src/app/order/order.service.ts +++ b/apps/api/src/app/order/order.service.ts @@ -188,7 +188,7 @@ export class OrderService { }): Promise { const where: Prisma.OrderWhereInput = { userId }; - const { account: filtersByAccount, tag: filtersByTag } = groupBy( + const { ACCOUNT: filtersByAccount, TAG: filtersByTag } = groupBy( filters, (filter) => { return filter.type; diff --git a/apps/api/src/app/portfolio/portfolio.controller.ts b/apps/api/src/app/portfolio/portfolio.controller.ts index 68cd03c0..5c648378 100644 --- a/apps/api/src/app/portfolio/portfolio.controller.ts +++ b/apps/api/src/app/portfolio/portfolio.controller.ts @@ -119,13 +119,13 @@ export class PortfolioController { ...accountIds.map((accountId) => { return { id: accountId, - type: 'account' + type: 'ACCOUNT' }; }), ...tagIds.map((tagId) => { return { id: tagId, - type: 'tag' + type: '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 7b4f04e0..a072319f 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 @@ -165,7 +165,7 @@ export class AllocationsPageComponent implements OnDestroy, OnInit { return { id, label: name, - type: 'account' + type: 'ACCOUNT' }; }); @@ -173,7 +173,7 @@ export class AllocationsPageComponent implements OnDestroy, OnInit { return { id, label: name, - type: 'tag' + type: 'TAG' }; }); diff --git a/apps/client/src/app/services/data.service.ts b/apps/client/src/app/services/data.service.ts index 7679c154..3edde74e 100644 --- a/apps/client/src/app/services/data.service.ts +++ b/apps/client/src/app/services/data.service.ts @@ -187,7 +187,7 @@ export class DataService { let params = new HttpParams(); if (filters?.length > 0) { - const { account: filtersByAccount, tag: filtersByTag } = groupBy( + const { ACCOUNT: filtersByAccount, TAG: filtersByTag } = groupBy( filters, (filter) => { return filter.type; diff --git a/libs/common/src/lib/interfaces/filter-group.interface.ts b/libs/common/src/lib/interfaces/filter-group.interface.ts new file mode 100644 index 00000000..7087b99f --- /dev/null +++ b/libs/common/src/lib/interfaces/filter-group.interface.ts @@ -0,0 +1,6 @@ +import { Filter } from './filter.interface'; + +export interface FilterGroup { + filters: Filter[]; + name: Filter['type']; +} diff --git a/libs/common/src/lib/interfaces/filter.interface.ts b/libs/common/src/lib/interfaces/filter.interface.ts index e6d5bb10..a4280f4f 100644 --- a/libs/common/src/lib/interfaces/filter.interface.ts +++ b/libs/common/src/lib/interfaces/filter.interface.ts @@ -1,5 +1,5 @@ export interface Filter { id: string; label?: string; - type: 'account' | 'tag'; + type: 'ACCOUNT' | 'SYMBOL' | 'TAG'; } diff --git a/libs/common/src/lib/interfaces/index.ts b/libs/common/src/lib/interfaces/index.ts index 0f483c10..96275e4d 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 { FilterGroup } from './filter-group.interface'; import { Filter } from './filter.interface'; import { HistoricalDataItem } from './historical-data-item.interface'; import { InfoItem } from './info-item.interface'; @@ -41,6 +42,7 @@ export { Coupon, Export, Filter, + FilterGroup, HistoricalDataItem, InfoItem, PortfolioChart, 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 e0eb5da8..1ade36db 100644 --- a/libs/ui/src/lib/activities-filter/activities-filter.component.html +++ b/libs/ui/src/lib/activities-filter/activities-filter.component.html @@ -26,9 +26,17 @@ #autocomplete="matAutocomplete" (optionSelected)="onSelectFilter($event)" > - - {{ filter.label | gfSymbol }} - + + + {{ filter.label | gfSymbol }} + + ; + public filterGroups$: Subject = new BehaviorSubject([]); public filters$: Subject = new BehaviorSubject([]); public filters: Observable = this.filters$.asObservable(); public searchControl = new FormControl(); @@ -50,40 +52,27 @@ export class ActivitiesFilterComponent implements OnChanges, OnDestroy { .pipe(takeUntil(this.unsubscribeSubject)) .subscribe((filterOrSearchTerm: Filter | string) => { if (filterOrSearchTerm) { - this.filters$.next( - this.allFilters - .filter((filter) => { - // Filter selected filters - return !this.selectedFilters.some((selectedFilter) => { - return selectedFilter.id === filter.id; - }); - }) - .filter((filter) => { - if (typeof filterOrSearchTerm === 'string') { - return filter.label - .toLowerCase() - .startsWith(filterOrSearchTerm.toLowerCase()); - } + const searchTerm = + typeof filterOrSearchTerm === 'string' + ? filterOrSearchTerm + : filterOrSearchTerm?.label; - return filter.label - .toLowerCase() - .startsWith(filterOrSearchTerm?.label?.toLowerCase()); - }) - .sort((a, b) => a.label.localeCompare(b.label)) - ); + this.filterGroups$.next(this.getGroupedFilters(searchTerm)); + } else { + this.filterGroups$.next(this.getGroupedFilters()); } }); } public ngOnChanges(changes: SimpleChanges) { if (changes.allFilters?.currentValue) { - this.updateFilter(); + this.updateFilters(); } } public onAddFilter({ input, value }: MatChipInputEvent): void { if (value?.trim()) { - this.updateFilter(); + this.updateFilters(); } // Reset the input value @@ -99,12 +88,16 @@ export class ActivitiesFilterComponent implements OnChanges, OnDestroy { return filter.id !== aFilter.id; }); - this.updateFilter(); + this.updateFilters(); } public onSelectFilter(event: MatAutocompleteSelectedEvent): void { - this.selectedFilters.push(event.option.value); - this.updateFilter(); + this.selectedFilters.push( + this.allFilters.find((filter) => { + return filter.id === event.option.value; + }) + ); + this.updateFilters(); this.searchInput.nativeElement.value = ''; this.searchControl.setValue(null); } @@ -114,8 +107,8 @@ export class ActivitiesFilterComponent implements OnChanges, OnDestroy { this.unsubscribeSubject.complete(); } - private updateFilter() { - this.filters$.next( + private getGroupedFilters(searchTerm?: string) { + const filterGroupsMap = groupBy( this.allFilters .filter((filter) => { // Filter selected filters @@ -123,9 +116,44 @@ export class ActivitiesFilterComponent implements OnChanges, OnDestroy { return selectedFilter.id === filter.id; }); }) - .sort((a, b) => a.label.localeCompare(b.label)) + .filter((filter) => { + if (searchTerm) { + // Filter by search term + return filter.label + .toLowerCase() + .includes(searchTerm.toLowerCase()); + } + + return filter; + }) + .sort((a, b) => a.label.localeCompare(b.label)), + (filter) => { + return filter.type; + } ); + const filterGroups: FilterGroup[] = []; + + for (const type of Object.keys(filterGroupsMap)) { + filterGroups.push({ + name: type, + filters: filterGroupsMap[type] + }); + } + + return filterGroups + .sort((a, b) => a.name.localeCompare(b.name)) + .map((filterGroup) => { + return { + ...filterGroup, + filters: filterGroup.filters + }; + }); + } + + private updateFilters() { + this.filterGroups$.next(this.getGroupedFilters()); + // Emit an array with a new reference this.valueChanged.emit([...this.selectedFilters]); } 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 959a4e86..6f73b0b4 100644 --- a/libs/ui/src/lib/activities-table/activities-table.component.ts +++ b/libs/ui/src/lib/activities-table/activities-table.component.ts @@ -105,17 +105,17 @@ export class ActivitiesTableComponent implements OnChanges, OnDestroy { this.defaultDateFormat = getDateFormatString(this.locale); if (this.activities) { - this.allFilters = this.getSearchableFieldValues(this.activities).map( - (label) => { - return { label, id: label, type: 'tag' }; - } - ); + this.allFilters = this.getSearchableFieldValues(this.activities); this.dataSource = new MatTableDataSource(this.activities); this.dataSource.filterPredicate = (data, filter) => { const dataString = this.getFilterableValues(data) + .map((currentFilter) => { + return currentFilter.label; + }) .join(' ') .toLowerCase(); + let contains = true; for (const singleFilter of filter.split(SEARCH_STRING_SEPARATOR)) { contains = @@ -190,50 +190,51 @@ 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); + fieldValueMap: { [id: string]: Filter } = {} + ): Filter[] { + fieldValueMap[activity.Account?.id] = { + id: activity.Account?.id, + label: activity.Account?.name, + type: 'ACCOUNT' + }; + + fieldValueMap[activity.SymbolProfile.currency] = { + id: activity.SymbolProfile.currency, + label: activity.SymbolProfile.currency, + type: 'TAG' + }; if (!isUUID(activity.SymbolProfile.symbol)) { - fieldValues.add(activity.SymbolProfile.symbol); + fieldValueMap[activity.SymbolProfile.symbol] = { + id: activity.SymbolProfile.symbol, + label: activity.SymbolProfile.symbol, + type: 'SYMBOL' + }; } - fieldValues.add(activity.type); - fieldValues.add(format(activity.date, 'yyyy')); + fieldValueMap[activity.type] = { + id: activity.type, + label: activity.type, + type: 'TAG' + }; - return [...fieldValues].filter((item) => { - return item !== undefined; - }); + fieldValueMap[format(activity.date, 'yyyy')] = { + id: format(activity.date, 'yyyy'), + label: format(activity.date, 'yyyy'), + type: 'TAG' + }; + + return Object.values(fieldValueMap); } - private getSearchableFieldValues(activities: OrderWithAccount[]): string[] { - const fieldValues = new Set(); + private getSearchableFieldValues(activities: OrderWithAccount[]): Filter[] { + const fieldValueMap: { [id: string]: Filter } = {}; for (const activity of activities) { - this.getFilterableValues(activity, fieldValues); + this.getFilterableValues(activity, fieldValueMap); } - return [...fieldValues] - .filter((item) => { - return item !== undefined; - }) - .sort((a, b) => { - const aFirstChar = a.charAt(0); - const bFirstChar = b.charAt(0); - const isANumber = aFirstChar >= '0' && aFirstChar <= '9'; - const isBNumber = bFirstChar >= '0' && bFirstChar <= '9'; - - // Sort priority: text, followed by numbers - if (isANumber && !isBNumber) { - return 1; - } else if (!isANumber && isBNumber) { - return -1; - } else { - return a.toLowerCase() < b.toLowerCase() ? -1 : 1; - } - }); + return Object.values(fieldValueMap); } private getTotalFees() { @@ -276,6 +277,7 @@ export class ActivitiesTableComponent implements OnChanges, OnDestroy { return filter.label; }) .join(SEARCH_STRING_SEPARATOR); + const lowercaseSearchKeywords = filters.map((filter) => { return filter.label.trim().toLowerCase(); });