Browse Source

add multi-filter support for transaction filtering with auto completion

pull/76/head
Valentin Zickner 4 years ago
committed by Thomas
parent
commit
ccaa13e795
  1. 6
      apps/api/src/app/order/interfaces/order-with-account.type.ts
  2. 40
      apps/client/src/app/components/transactions-table/transactions-table.component.html
  3. 126
      apps/client/src/app/components/transactions-table/transactions-table.component.ts
  4. 8
      apps/client/src/app/components/transactions-table/transactions-table.module.ts

6
apps/api/src/app/order/interfaces/order-with-account.type.ts

@ -1,3 +1,5 @@
import { Account, Order } from '@prisma/client';
import { Account, Order, Platform } from '@prisma/client';
export type OrderWithAccount = Order & { Account?: Account };
type AccountWithPlatform = Account & { Platform?: Platform };
export type OrderWithAccount = Order & { Account?: AccountWithPlatform };

40
apps/client/src/app/components/transactions-table/transactions-table.component.html

@ -1,12 +1,36 @@
<mat-form-field appearance="outline" class="w-100">
<input
#input
autocomplete="off"
matInput
placeholder="Search for transactions..."
(keyup)="applyFilter($event)"
/>
<mat-form-field class="w-100">
<ion-icon class="mr-1" matPrefix name="search-outline"></ion-icon>
<mat-chip-list #chipList aria-label="Search keywords">
<mat-chip
*ngFor="let searchKeyword of searchKeywords"
[removable]="true"
(removed)="removeKeyword(searchKeyword)"
matChipRemove
>
{{ searchKeyword }}
<ion-icon class="mr-1" matPrefix name="close-outline"></ion-icon>
</mat-chip>
<input
placeholder="Search for transactions..."
#searchInput
[formControl]="searchControl"
[matAutocomplete]="auto"
[matChipInputFor]="chipList"
[matChipInputSeparatorKeyCodes]="separatorKeysCodes"
(matChipInputTokenEnd)="addKeyword($event)"
/>
</mat-chip-list>
<mat-autocomplete
#auto="matAutocomplete"
(optionSelected)="keywordSelected($event)"
>
<mat-option
*ngFor="let transaction of filteredTransactions | async"
[value]="transaction"
>
{{ transaction }}
</mat-option>
</mat-autocomplete>
</mat-form-field>
<table

126
apps/client/src/app/components/transactions-table/transactions-table.component.ts

@ -1,6 +1,7 @@
import {
ChangeDetectionStrategy,
Component,
ElementRef,
EventEmitter,
Input,
OnChanges,
@ -15,10 +16,19 @@ import { MatTableDataSource } from '@angular/material/table';
import { ActivatedRoute, Router } from '@angular/router';
import { OrderWithAccount } from '@ghostfolio/api/app/order/interfaces/order-with-account.type';
import { DEFAULT_DATE_FORMAT } from '@ghostfolio/helper';
import { Subject, Subscription } from 'rxjs';
import { BehaviorSubject, Observable, Subject, Subscription } from 'rxjs';
import { takeUntil } from 'rxjs/operators';
import { PositionDetailDialog } from '../position/position-detail-dialog/position-detail-dialog.component';
import {
MatAutocomplete,
MatAutocompleteSelectedEvent
} from '@angular/material/autocomplete';
import { FormControl } from '@angular/forms';
import { COMMA, ENTER } from '@angular/cdk/keycodes';
import { MatChipInputEvent } from '@angular/material/chips';
const SEARCH_STRING_SEPARATOR = ',';
@Component({
selector: 'gf-transactions-table',
@ -39,13 +49,23 @@ export class TransactionsTableComponent
@Output() transactionToUpdate = new EventEmitter<OrderWithAccount>();
@ViewChild(MatSort) sort: MatSort;
@ViewChild('searchInput') searchInput: ElementRef<HTMLInputElement>;
@ViewChild('auto') matAutocomplete: MatAutocomplete;
public dataSource: MatTableDataSource<OrderWithAccount> = new MatTableDataSource();
public defaultDateFormat = DEFAULT_DATE_FORMAT;
public displayedColumns = [];
public isLoading = true;
public routeQueryParams: Subscription;
public separatorKeysCodes: number[] = [ENTER, COMMA];
public searchKeywords: string[] = [];
public searchControl = new FormControl();
public filteredTransactions$: Subject<string[]> = new BehaviorSubject([]);
public filteredTransactions: Observable<
string[]
> = this.filteredTransactions$.asObservable();
private allFilteredTransactions: string[];
private unsubscribeSubject = new Subject<void>();
public constructor(
@ -63,6 +83,50 @@ export class TransactionsTableComponent
});
}
});
this.searchControl.valueChanges.subscribe((keyword) => {
if (keyword) {
const filterValue = keyword.toLowerCase();
this.filteredTransactions$.next(
this.allFilteredTransactions.filter(
(filter) => filter.toLowerCase().indexOf(filterValue) === 0
)
);
}
});
}
public addKeyword(event: MatChipInputEvent): void {
const input = event.input;
const value = event.value;
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 ngOnInit() {}
@ -88,28 +152,22 @@ export class TransactionsTableComponent
if (this.transactions) {
this.dataSource = new MatTableDataSource(this.transactions);
this.dataSource.filterPredicate = (data, filter) => {
const accumulator = (currentTerm: string, key: string) => {
return key === 'Account'
? currentTerm + data.Account.name
: currentTerm + data[key];
};
const dataString = Object.keys(data)
.reduce(accumulator, '')
let dataString = TransactionsTableComponent.getFilterableValues(data)
.join(' ')
.toLowerCase();
const transformedFilter = filter.trim().toLowerCase();
return dataString.includes(transformedFilter);
let contains = true;
for (const singleFilter of filter.split(SEARCH_STRING_SEPARATOR)) {
contains =
contains && dataString.includes(singleFilter.trim().toLowerCase());
}
return contains;
};
this.dataSource.sort = this.sort;
this.updateFilter();
this.isLoading = false;
}
}
public applyFilter(event: Event) {
const filterValue = (event.target as HTMLInputElement).value;
this.dataSource.filter = filterValue.trim().toLowerCase();
}
public onDeleteTransaction(aId: string) {
const confirmation = confirm(
'Do you really want to delete this transaction?'
@ -169,4 +227,40 @@ export class TransactionsTableComponent
this.unsubscribeSubject.next();
this.unsubscribeSubject.complete();
}
private updateFilter() {
this.dataSource.filter = this.searchKeywords.join(SEARCH_STRING_SEPARATOR);
const lowercaseSearchKeywords = this.searchKeywords.map((keyword) =>
keyword.trim().toLowerCase()
);
this.allFilteredTransactions = TransactionsTableComponent.getSearchableFieldValues(
this.transactions
).filter((item) => {
return !lowercaseSearchKeywords.includes(item.trim().toLowerCase());
});
this.filteredTransactions$.next(this.allFilteredTransactions);
}
private static getSearchableFieldValues(
transactions: OrderWithAccount[]
): string[] {
const fieldValues = new Set<string>();
for (const transaction of transactions) {
this.getFilterableValues(transaction, fieldValues);
}
return [...fieldValues].sort();
}
private static getFilterableValues(
transaction,
fieldValues: Set<string> = new Set<string>()
): string[] {
fieldValues.add(transaction.currency);
fieldValues.add(transaction.symbol);
fieldValues.add(transaction.type);
fieldValues.add(transaction.Account?.name);
fieldValues.add(transaction.Account?.Platform?.name);
return [...fieldValues];
}
}

8
apps/client/src/app/components/transactions-table/transactions-table.module.ts

@ -13,6 +13,9 @@ import { GfPositionDetailDialogModule } from '../position/position-detail-dialog
import { GfSymbolIconModule } from '../symbol-icon/symbol-icon.module';
import { GfValueModule } from '../value/value.module';
import { TransactionsTableComponent } from './transactions-table.component';
import { MatChipsModule } from '@angular/material/chips';
import { MatAutocompleteModule } from '@angular/material/autocomplete';
import { FormsModule, ReactiveFormsModule } from '@angular/forms';
@NgModule({
declarations: [TransactionsTableComponent],
@ -23,13 +26,16 @@ import { TransactionsTableComponent } from './transactions-table.component';
GfSymbolIconModule,
GfSymbolModule,
GfValueModule,
MatAutocompleteModule,
MatButtonModule,
MatChipsModule,
MatInputModule,
MatMenuModule,
MatSortModule,
MatTableModule,
NgxSkeletonLoaderModule,
RouterModule
RouterModule,
ReactiveFormsModule
],
providers: [],
schemas: [CUSTOM_ELEMENTS_SCHEMA]

Loading…
Cancel
Save