Feature/group filters by type (#922)

* Add groups to activities filter component

* Update changelog
pull/925/head
Thomas Kaul 2 years ago committed by GitHub
parent f1483569a2
commit 160335302a
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23

@ -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

@ -120,7 +120,7 @@ export class AccountService {
where.id = {
in: filters
.filter(({ type }) => {
return type === 'account';
return type === 'ACCOUNT';
})
.map(({ id }) => {
return id;

@ -188,7 +188,7 @@ export class OrderService {
}): Promise<Activity[]> {
const where: Prisma.OrderWhereInput = { userId };
const { account: filtersByAccount, tag: filtersByTag } = groupBy(
const { ACCOUNT: filtersByAccount, TAG: filtersByTag } = groupBy(
filters,
(filter) => {
return filter.type;

@ -119,13 +119,13 @@ export class PortfolioController {
...accountIds.map((accountId) => {
return <Filter>{
id: accountId,
type: 'account'
type: 'ACCOUNT'
};
}),
...tagIds.map((tagId) => {
return <Filter>{
id: tagId,
type: 'tag'
type: 'TAG'
};
})
];

@ -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'
};
});

@ -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;

@ -0,0 +1,6 @@
import { Filter } from './filter.interface';
export interface FilterGroup {
filters: Filter[];
name: Filter['type'];
}

@ -1,5 +1,5 @@
export interface Filter {
id: string;
label?: string;
type: 'account' | 'tag';
type: 'ACCOUNT' | 'SYMBOL' | 'TAG';
}

@ -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,

@ -26,9 +26,17 @@
#autocomplete="matAutocomplete"
(optionSelected)="onSelectFilter($event)"
>
<mat-option *ngFor="let filter of filters | async" [value]="filter">
{{ filter.label | gfSymbol }}
</mat-option>
<mat-optgroup
*ngFor="let filterGroup of filterGroups$ | async"
[label]="filterGroup.name"
>
<mat-option
*ngFor="let filter of filterGroup.filters"
[value]="filter.id"
>
{{ filter.label | gfSymbol }}
</mat-option>
</mat-optgroup>
</mat-autocomplete>
<mat-spinner
matSuffix

@ -17,7 +17,8 @@ import {
MatAutocompleteSelectedEvent
} from '@angular/material/autocomplete';
import { MatChipInputEvent } from '@angular/material/chips';
import { Filter } from '@ghostfolio/common/interfaces';
import { Filter, FilterGroup } from '@ghostfolio/common/interfaces';
import { groupBy } from 'lodash';
import { BehaviorSubject, Observable, Subject } from 'rxjs';
import { takeUntil } from 'rxjs/operators';
@ -37,6 +38,7 @@ export class ActivitiesFilterComponent implements OnChanges, OnDestroy {
@ViewChild('autocomplete') matAutocomplete: MatAutocomplete;
@ViewChild('searchInput') searchInput: ElementRef<HTMLInputElement>;
public filterGroups$: Subject<FilterGroup[]> = new BehaviorSubject([]);
public filters$: Subject<Filter[]> = new BehaviorSubject([]);
public filters: Observable<Filter[]> = 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());
}
return filter.label
.toLowerCase()
.startsWith(filterOrSearchTerm?.label?.toLowerCase());
})
.sort((a, b) => a.label.localeCompare(b.label))
);
const searchTerm =
typeof filterOrSearchTerm === 'string'
? filterOrSearchTerm
: filterOrSearchTerm?.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: <Filter['type']>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]);
}

@ -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<string> = new Set<string>()
): 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<string>();
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();
});

Loading…
Cancel
Save