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>
This commit is contained in:
parent
1dc94c0027
commit
5d24adfa75
@ -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: [],
|
||||
|
Loading…
x
Reference in New Issue
Block a user