Feature/improve transaction filtering (#76)

* add multi-filter support for transaction filtering with auto completion

* update changelog

* fix table for transaction for accounts without platform

* simplify readme file since docker compose build is not required (#75)

* simplify readme file since docker compose build is not required

* add anchor navigation in README.md

* Improve UI

* Refactoring

* Refactoring

* Feature/travis (#77)

* integrate travis

* fix prettier transactions-page.component.ts

* change base branch to main

* fetch all branches in .travis.yml

* Bugfix/keep current menu item active (#78)

* Keep current menu item active

* Update changelog

* Feature/travis (#77)

* integrate travis

* fix prettier transactions-page.component.ts

* change base branch to main

* fetch all branches in .travis.yml

* Keep current menu item active

* Update changelog

Co-authored-by: Valentin Zickner <3200232+vzickner@users.noreply.github.com>

* add multi-filter support for transaction filtering with auto completion

* update changelog

* fix table for transaction for accounts without platform

* Improve UI

* Refactoring

* Refactoring

* Update changelog

Co-authored-by: Thomas <4159106+dtslvr@users.noreply.github.com>
pull/79/head
Valentin Zickner 4 years ago committed by GitHub
parent 1dc94c0027
commit 5d24adfa75
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23

@ -7,6 +7,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
## Unreleased
### Changed
- Improved the transaction filtering with multi filter support
### Fixed
- Fixed the filtering by account name in the transactions table

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

@ -1,12 +1,37 @@
<mat-form-field appearance="outline" class="w-100">
<input
#input
autocomplete="off"
matInput
placeholder="Search for transactions..."
(keyup)="applyFilter($event)"
/>
<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"
matChipRemove
[removable]="true"
(removed)="removeKeyword(searchKeyword)"
>
{{ searchKeyword }}
<ion-icon class="ml-2" matPrefix name="close-outline"></ion-icon>
</mat-chip>
<input
#searchInput
name="close-outline"
placeholder="Search for account, currency, symbol or type..."
[formControl]="searchControl"
[matAutocomplete]="autocomplete"
[matChipInputFor]="chipList"
[matChipInputSeparatorKeyCodes]="separatorKeysCodes"
(matChipInputTokenEnd)="addKeyword($event)"
/>
</mat-chip-list>
<mat-autocomplete
#autocomplete="matAutocomplete"
(optionSelected)="keywordSelected($event)"
>
<mat-option
*ngFor="let transaction of filteredTransactions | async"
[value]="transaction"
>
{{ transaction }}
</mat-option>
</mat-autocomplete>
</mat-form-field>
<table

@ -7,6 +7,10 @@
}
}
.mat-chip {
cursor: pointer;
}
.mat-table {
td {
border: 0;

@ -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',
@ -38,14 +48,24 @@ export class TransactionsTableComponent
@Output() transactionToClone = new EventEmitter<OrderWithAccount>();
@Output() transactionToUpdate = new EventEmitter<OrderWithAccount>();
@ViewChild('autocomplete') matAutocomplete: MatAutocomplete;
@ViewChild('searchInput') searchInput: ElementRef<HTMLInputElement>;
@ViewChild(MatSort) sort: MatSort;
public dataSource: MatTableDataSource<OrderWithAccount> = new MatTableDataSource();
public defaultDateFormat = DEFAULT_DATE_FORMAT;
public displayedColumns = [];
public filteredTransactions$: Subject<string[]> = new BehaviorSubject([]);
public filteredTransactions: Observable<
string[]
> = this.filteredTransactions$.asObservable();
public isLoading = true;
public routeQueryParams: Subscription;
public searchControl = new FormControl();
public searchKeywords: string[] = [];
public separatorKeysCodes: number[] = [ENTER, COMMA];
private allFilteredTransactions: string[];
private unsubscribeSubject = new Subject<void>();
public constructor(
@ -63,6 +83,49 @@ 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
)
);
} else {
this.filteredTransactions$.next(this.allFilteredTransactions);
}
});
}
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 ngOnInit() {}
@ -88,28 +151,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, '')
const 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 +226,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].filter((item) => item != undefined).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].filter((item) => item != undefined);
}
}

@ -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 { ReactiveFormsModule } from '@angular/forms';
@NgModule({
declarations: [TransactionsTableComponent],
@ -23,12 +26,15 @@ import { TransactionsTableComponent } from './transactions-table.component';
GfSymbolIconModule,
GfSymbolModule,
GfValueModule,
MatAutocompleteModule,
MatButtonModule,
MatChipsModule,
MatInputModule,
MatMenuModule,
MatSortModule,
MatTableModule,
NgxSkeletonLoaderModule,
ReactiveFormsModule,
RouterModule
],
providers: [],

File diff suppressed because it is too large Load Diff
Loading…
Cancel
Save