Feature/refactor filters with interface (#883)
* Refactor filtering with an interface * Filter by accounts * Update changelog
This commit is contained in:
parent
ce6b5fb7cb
commit
16dd8f7652
@ -9,9 +9,14 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
||||
|
||||
### Added
|
||||
|
||||
- Added support for filtering by accounts on the allocations page
|
||||
- Added support for private equity
|
||||
- Extended the form to set the asset and asset sub class for (wealth) items
|
||||
|
||||
### Changed
|
||||
|
||||
- Refactored the filtering (activities table and allocations page)
|
||||
|
||||
### Fixed
|
||||
|
||||
- Fixed the tooltip update in the portfolio proportion chart component
|
||||
|
@ -4,6 +4,7 @@ import { DataGatheringService } from '@ghostfolio/api/services/data-gathering.se
|
||||
import { ExchangeRateDataService } from '@ghostfolio/api/services/exchange-rate-data.service';
|
||||
import { PrismaService } from '@ghostfolio/api/services/prisma.service';
|
||||
import { SymbolProfileService } from '@ghostfolio/api/services/symbol-profile.service';
|
||||
import { Filter } from '@ghostfolio/common/interfaces';
|
||||
import { OrderWithAccount } from '@ghostfolio/common/types';
|
||||
import { Injectable } from '@nestjs/common';
|
||||
import {
|
||||
@ -16,6 +17,7 @@ import {
|
||||
} from '@prisma/client';
|
||||
import Big from 'big.js';
|
||||
import { endOfToday, isAfter } from 'date-fns';
|
||||
import { groupBy } from 'lodash';
|
||||
import { v4 as uuidv4 } from 'uuid';
|
||||
|
||||
import { Activity } from './interfaces/activities.interface';
|
||||
@ -166,31 +168,44 @@ export class OrderService {
|
||||
}
|
||||
|
||||
public async getOrders({
|
||||
filters,
|
||||
includeDrafts = false,
|
||||
tags,
|
||||
types,
|
||||
userCurrency,
|
||||
userId
|
||||
}: {
|
||||
filters?: Filter[];
|
||||
includeDrafts?: boolean;
|
||||
tags?: string[];
|
||||
types?: TypeOfOrder[];
|
||||
userCurrency: string;
|
||||
userId: string;
|
||||
}): Promise<Activity[]> {
|
||||
const where: Prisma.OrderWhereInput = { userId };
|
||||
|
||||
const { account: filtersByAccount, tag: filtersByTag } = groupBy(
|
||||
filters,
|
||||
(filter) => {
|
||||
return filter.type;
|
||||
}
|
||||
);
|
||||
|
||||
if (filtersByAccount?.length > 0) {
|
||||
where.accountId = {
|
||||
in: filtersByAccount.map(({ id }) => {
|
||||
return id;
|
||||
})
|
||||
};
|
||||
}
|
||||
|
||||
if (includeDrafts === false) {
|
||||
where.isDraft = false;
|
||||
}
|
||||
|
||||
if (tags?.length > 0) {
|
||||
if (filtersByTag?.length > 0) {
|
||||
where.tags = {
|
||||
some: {
|
||||
OR: tags.map((tag) => {
|
||||
return {
|
||||
name: tag
|
||||
};
|
||||
OR: filtersByTag.map(({ id }) => {
|
||||
return { id };
|
||||
})
|
||||
}
|
||||
};
|
||||
|
@ -11,6 +11,7 @@ import { ExchangeRateDataService } from '@ghostfolio/api/services/exchange-rate-
|
||||
import { baseCurrency } from '@ghostfolio/common/config';
|
||||
import { parseDate } from '@ghostfolio/common/helper';
|
||||
import {
|
||||
Filter,
|
||||
PortfolioChart,
|
||||
PortfolioDetails,
|
||||
PortfolioInvestments,
|
||||
@ -19,7 +20,7 @@ import {
|
||||
PortfolioReport,
|
||||
PortfolioSummary
|
||||
} from '@ghostfolio/common/interfaces';
|
||||
import type { RequestWithUser } from '@ghostfolio/common/types';
|
||||
import type { DateRange, RequestWithUser } from '@ghostfolio/common/types';
|
||||
import {
|
||||
Controller,
|
||||
Get,
|
||||
@ -105,17 +106,36 @@ export class PortfolioController {
|
||||
@UseInterceptors(TransformDataSourceInResponseInterceptor)
|
||||
public async getDetails(
|
||||
@Headers('impersonation-id') impersonationId: string,
|
||||
@Query('range') range,
|
||||
@Query('tags') tags?: string
|
||||
@Query('accounts') filterByAccounts?: string,
|
||||
@Query('range') range?: DateRange,
|
||||
@Query('tags') filterByTags?: string
|
||||
): Promise<PortfolioDetails & { hasError: boolean }> {
|
||||
let hasError = false;
|
||||
|
||||
const accountIds = filterByAccounts?.split(',') ?? [];
|
||||
const tagIds = filterByTags?.split(',') ?? [];
|
||||
|
||||
const filters: Filter[] = [
|
||||
...accountIds.map((accountId) => {
|
||||
return <Filter>{
|
||||
id: accountId,
|
||||
type: 'account'
|
||||
};
|
||||
}),
|
||||
...tagIds.map((tagId) => {
|
||||
return <Filter>{
|
||||
id: tagId,
|
||||
type: 'tag'
|
||||
};
|
||||
})
|
||||
];
|
||||
|
||||
const { accounts, holdings, hasErrors } =
|
||||
await this.portfolioService.getDetails(
|
||||
impersonationId,
|
||||
this.request.user.id,
|
||||
range,
|
||||
tags?.split(',')
|
||||
filters
|
||||
);
|
||||
|
||||
if (hasErrors || hasNotDefinedValuesInObject(holdings)) {
|
||||
@ -163,7 +183,7 @@ export class PortfolioController {
|
||||
|
||||
return {
|
||||
hasError,
|
||||
accounts: tags ? {} : accounts,
|
||||
accounts: filters ? {} : accounts,
|
||||
holdings: isBasicUser ? {} : holdings
|
||||
};
|
||||
}
|
||||
|
@ -29,6 +29,7 @@ import {
|
||||
import { DATE_FORMAT, parseDate } from '@ghostfolio/common/helper';
|
||||
import {
|
||||
Accounts,
|
||||
Filter,
|
||||
PortfolioDetails,
|
||||
PortfolioPerformanceResponse,
|
||||
PortfolioReport,
|
||||
@ -309,7 +310,7 @@ export class PortfolioService {
|
||||
aImpersonationId: string,
|
||||
aUserId: string,
|
||||
aDateRange: DateRange = 'max',
|
||||
tags?: string[]
|
||||
aFilters?: Filter[]
|
||||
): Promise<PortfolioDetails & { hasErrors: boolean }> {
|
||||
const userId = await this.getUserId(aImpersonationId, aUserId);
|
||||
const user = await this.userService.user({ id: userId });
|
||||
@ -324,8 +325,8 @@ export class PortfolioService {
|
||||
|
||||
const { orders, portfolioOrders, transactionPoints } =
|
||||
await this.getTransactionPoints({
|
||||
tags,
|
||||
userId
|
||||
userId,
|
||||
filters: aFilters
|
||||
});
|
||||
|
||||
const portfolioCalculator = new PortfolioCalculator({
|
||||
@ -448,7 +449,7 @@ export class PortfolioService {
|
||||
value: totalValue
|
||||
});
|
||||
|
||||
if (tags === undefined) {
|
||||
if (aFilters === undefined) {
|
||||
for (const symbol of Object.keys(cashPositions)) {
|
||||
holdings[symbol] = cashPositions[symbol];
|
||||
}
|
||||
@ -1195,12 +1196,12 @@ export class PortfolioService {
|
||||
}
|
||||
|
||||
private async getTransactionPoints({
|
||||
filters,
|
||||
includeDrafts = false,
|
||||
tags,
|
||||
userId
|
||||
}: {
|
||||
filters?: Filter[];
|
||||
includeDrafts?: boolean;
|
||||
tags?: string[];
|
||||
userId: string;
|
||||
}): Promise<{
|
||||
transactionPoints: TransactionPoint[];
|
||||
@ -1210,8 +1211,8 @@ export class PortfolioService {
|
||||
const userCurrency = this.request.user?.Settings?.currency ?? baseCurrency;
|
||||
|
||||
const orders = await this.orderService.getOrders({
|
||||
filters,
|
||||
includeDrafts,
|
||||
tags,
|
||||
userCurrency,
|
||||
userId,
|
||||
types: ['BUY', 'SELL']
|
||||
|
@ -8,6 +8,7 @@ import { UserService } from '@ghostfolio/client/services/user/user.service';
|
||||
import { UNKNOWN_KEY } from '@ghostfolio/common/config';
|
||||
import { prettifySymbol } from '@ghostfolio/common/helper';
|
||||
import {
|
||||
Filter,
|
||||
PortfolioDetails,
|
||||
PortfolioPosition,
|
||||
UniqueAsset,
|
||||
@ -32,6 +33,7 @@ export class AllocationsPageComponent implements OnDestroy, OnInit {
|
||||
value: number;
|
||||
};
|
||||
};
|
||||
public allFilters: Filter[];
|
||||
public continents: {
|
||||
[code: string]: { name: string; value: number };
|
||||
};
|
||||
@ -39,7 +41,7 @@ export class AllocationsPageComponent implements OnDestroy, OnInit {
|
||||
[code: string]: { name: string; value: number };
|
||||
};
|
||||
public deviceType: string;
|
||||
public filters$ = new Subject<string[]>();
|
||||
public filters$ = new Subject<Filter[]>();
|
||||
public hasImpersonationId: boolean;
|
||||
public isLoading = false;
|
||||
public markets: {
|
||||
@ -76,7 +78,6 @@ export class AllocationsPageComponent implements OnDestroy, OnInit {
|
||||
value: number;
|
||||
};
|
||||
};
|
||||
public tags: string[] = [];
|
||||
|
||||
public user: User;
|
||||
|
||||
@ -127,10 +128,10 @@ export class AllocationsPageComponent implements OnDestroy, OnInit {
|
||||
this.filters$
|
||||
.pipe(
|
||||
distinctUntilChanged(),
|
||||
switchMap((tags) => {
|
||||
switchMap((filters) => {
|
||||
this.isLoading = true;
|
||||
|
||||
return this.dataService.fetchPortfolioDetails({ tags });
|
||||
return this.dataService.fetchPortfolioDetails({ filters });
|
||||
}),
|
||||
takeUntil(this.unsubscribeSubject)
|
||||
)
|
||||
@ -150,10 +151,26 @@ export class AllocationsPageComponent implements OnDestroy, OnInit {
|
||||
if (state?.user) {
|
||||
this.user = state.user;
|
||||
|
||||
this.tags = this.user.tags.map((tag) => {
|
||||
return tag.name;
|
||||
const accountFilters: Filter[] = this.user.accounts.map(
|
||||
({ id, name }) => {
|
||||
return {
|
||||
id: id,
|
||||
label: name,
|
||||
type: 'account'
|
||||
};
|
||||
}
|
||||
);
|
||||
|
||||
const tagFilters: Filter[] = this.user.tags.map(({ id, name }) => {
|
||||
return {
|
||||
id,
|
||||
label: name,
|
||||
type: 'tag'
|
||||
};
|
||||
});
|
||||
|
||||
this.allFilters = [...accountFilters, ...tagFilters];
|
||||
|
||||
this.changeDetectorRef.markForCheck();
|
||||
}
|
||||
});
|
||||
|
@ -3,9 +3,8 @@
|
||||
<div class="col">
|
||||
<h3 class="d-flex justify-content-center mb-3" i18n>Allocations</h3>
|
||||
<gf-activities-filter
|
||||
[allFilters]="tags"
|
||||
[allFilters]="allFilters"
|
||||
[isLoading]="isLoading"
|
||||
[ngClass]="{ 'd-none': tags.length <= 0 }"
|
||||
[placeholder]="placeholder"
|
||||
(valueChanged)="filters$.next($event)"
|
||||
></gf-activities-filter>
|
||||
|
@ -19,6 +19,7 @@ import {
|
||||
AdminData,
|
||||
AdminMarketData,
|
||||
Export,
|
||||
Filter,
|
||||
InfoItem,
|
||||
PortfolioChart,
|
||||
PortfolioDetails,
|
||||
@ -33,7 +34,7 @@ import { permissions } from '@ghostfolio/common/permissions';
|
||||
import { DateRange } from '@ghostfolio/common/types';
|
||||
import { DataSource, Order as OrderModel } from '@prisma/client';
|
||||
import { parseISO } from 'date-fns';
|
||||
import { cloneDeep } from 'lodash';
|
||||
import { cloneDeep, groupBy } from 'lodash';
|
||||
import { Observable } from 'rxjs';
|
||||
import { map } from 'rxjs/operators';
|
||||
|
||||
@ -182,11 +183,38 @@ export class DataService {
|
||||
);
|
||||
}
|
||||
|
||||
public fetchPortfolioDetails({ tags }: { tags?: string[] }) {
|
||||
public fetchPortfolioDetails({ filters }: { filters?: Filter[] }) {
|
||||
let params = new HttpParams();
|
||||
|
||||
if (tags?.length > 0) {
|
||||
params = params.append('tags', tags.join(','));
|
||||
if (filters?.length > 0) {
|
||||
const { account: filtersByAccount, tag: filtersByTag } = groupBy(
|
||||
filters,
|
||||
(filter) => {
|
||||
return filter.type;
|
||||
}
|
||||
);
|
||||
|
||||
if (filtersByAccount) {
|
||||
params = params.append(
|
||||
'accounts',
|
||||
filtersByAccount
|
||||
.map(({ id }) => {
|
||||
return id;
|
||||
})
|
||||
.join(',')
|
||||
);
|
||||
}
|
||||
|
||||
if (filtersByTag) {
|
||||
params = params.append(
|
||||
'tags',
|
||||
filtersByTag
|
||||
.map(({ id }) => {
|
||||
return id;
|
||||
})
|
||||
.join(',')
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
return this.http.get<PortfolioDetails>('/api/v1/portfolio/details', {
|
||||
|
5
libs/common/src/lib/interfaces/filter.interface.ts
Normal file
5
libs/common/src/lib/interfaces/filter.interface.ts
Normal file
@ -0,0 +1,5 @@
|
||||
export interface Filter {
|
||||
id: string;
|
||||
label?: string;
|
||||
type: 'account' | 'tag';
|
||||
}
|
@ -8,6 +8,7 @@ import {
|
||||
} from './admin-market-data.interface';
|
||||
import { Coupon } from './coupon.interface';
|
||||
import { Export } from './export.interface';
|
||||
import { Filter } from './filter.interface';
|
||||
import { InfoItem } from './info-item.interface';
|
||||
import { PortfolioChart } from './portfolio-chart.interface';
|
||||
import { PortfolioDetails } from './portfolio-details.interface';
|
||||
@ -38,6 +39,7 @@ export {
|
||||
AdminMarketDataItem,
|
||||
Coupon,
|
||||
Export,
|
||||
Filter,
|
||||
InfoItem,
|
||||
PortfolioChart,
|
||||
PortfolioDetails,
|
||||
|
@ -2,13 +2,13 @@
|
||||
<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"
|
||||
*ngFor="let filter of selectedFilters"
|
||||
class="mx-1 my-0 px-2 py-0"
|
||||
matChipRemove
|
||||
[removable]="true"
|
||||
(removed)="removeKeyword(searchKeyword)"
|
||||
(removed)="onRemoveFilter(filter)"
|
||||
>
|
||||
{{ searchKeyword | gfSymbol }}
|
||||
{{ filter.label | gfSymbol }}
|
||||
<ion-icon class="ml-2" matPrefix name="close-outline"></ion-icon>
|
||||
</mat-chip>
|
||||
<input
|
||||
@ -19,15 +19,15 @@
|
||||
[matChipInputFor]="chipList"
|
||||
[matChipInputSeparatorKeyCodes]="separatorKeysCodes"
|
||||
[placeholder]="placeholder"
|
||||
(matChipInputTokenEnd)="addKeyword($event)"
|
||||
(matChipInputTokenEnd)="onAddFilter($event)"
|
||||
/>
|
||||
</mat-chip-list>
|
||||
<mat-autocomplete
|
||||
#autocomplete="matAutocomplete"
|
||||
(optionSelected)="keywordSelected($event)"
|
||||
(optionSelected)="onSelectFilter($event)"
|
||||
>
|
||||
<mat-option *ngFor="let filter of filters | async" [value]="filter">
|
||||
{{ filter | gfSymbol }}
|
||||
{{ filter.label | gfSymbol }}
|
||||
</mat-option>
|
||||
</mat-autocomplete>
|
||||
<mat-spinner
|
||||
|
@ -17,6 +17,7 @@ import {
|
||||
MatAutocompleteSelectedEvent
|
||||
} from '@angular/material/autocomplete';
|
||||
import { MatChipInputEvent } from '@angular/material/chips';
|
||||
import { Filter } from '@ghostfolio/common/interfaces';
|
||||
import { BehaviorSubject, Observable, Subject } from 'rxjs';
|
||||
import { takeUntil } from 'rxjs/operators';
|
||||
|
||||
@ -27,19 +28,19 @@ import { takeUntil } from 'rxjs/operators';
|
||||
templateUrl: './activities-filter.component.html'
|
||||
})
|
||||
export class ActivitiesFilterComponent implements OnChanges, OnDestroy {
|
||||
@Input() allFilters: string[];
|
||||
@Input() allFilters: Filter[];
|
||||
@Input() isLoading: boolean;
|
||||
@Input() placeholder: string;
|
||||
|
||||
@Output() valueChanged = new EventEmitter<string[]>();
|
||||
@Output() valueChanged = new EventEmitter<Filter[]>();
|
||||
|
||||
@ViewChild('autocomplete') matAutocomplete: MatAutocomplete;
|
||||
@ViewChild('searchInput') searchInput: ElementRef<HTMLInputElement>;
|
||||
|
||||
public filters$: Subject<string[]> = new BehaviorSubject([]);
|
||||
public filters: Observable<string[]> = this.filters$.asObservable();
|
||||
public filters$: Subject<Filter[]> = new BehaviorSubject([]);
|
||||
public filters: Observable<Filter[]> = this.filters$.asObservable();
|
||||
public searchControl = new FormControl();
|
||||
public searchKeywords: string[] = [];
|
||||
public selectedFilters: Filter[] = [];
|
||||
public separatorKeysCodes: number[] = [ENTER, COMMA];
|
||||
|
||||
private unsubscribeSubject = new Subject<void>();
|
||||
@ -47,16 +48,23 @@ export class ActivitiesFilterComponent implements OnChanges, OnDestroy {
|
||||
public constructor() {
|
||||
this.searchControl.valueChanges
|
||||
.pipe(takeUntil(this.unsubscribeSubject))
|
||||
.subscribe((keyword) => {
|
||||
if (keyword) {
|
||||
const filterValue = keyword.toLowerCase();
|
||||
.subscribe((currentFilter: string) => {
|
||||
if (currentFilter) {
|
||||
this.filters$.next(
|
||||
this.allFilters.filter(
|
||||
(filter) => filter.toLowerCase().indexOf(filterValue) === 0
|
||||
)
|
||||
this.allFilters
|
||||
.filter((filter) => {
|
||||
// Filter selected filters
|
||||
return !this.selectedFilters.some((selectedFilter) => {
|
||||
return selectedFilter.id === filter.id;
|
||||
});
|
||||
})
|
||||
.filter((filter) => {
|
||||
return filter.label
|
||||
.toLowerCase()
|
||||
.startsWith(currentFilter?.toLowerCase());
|
||||
})
|
||||
.sort((a, b) => a.label.localeCompare(b.label))
|
||||
);
|
||||
} else {
|
||||
this.filters$.next(this.allFilters);
|
||||
}
|
||||
});
|
||||
}
|
||||
@ -67,9 +75,8 @@ export class ActivitiesFilterComponent implements OnChanges, OnDestroy {
|
||||
}
|
||||
}
|
||||
|
||||
public addKeyword({ input, value }: MatChipInputEvent): void {
|
||||
public onAddFilter({ input, value }: MatChipInputEvent): void {
|
||||
if (value?.trim()) {
|
||||
this.searchKeywords.push(value.trim());
|
||||
this.updateFilter();
|
||||
}
|
||||
|
||||
@ -81,31 +88,39 @@ export class ActivitiesFilterComponent implements OnChanges, OnDestroy {
|
||||
this.searchControl.setValue(null);
|
||||
}
|
||||
|
||||
public keywordSelected(event: MatAutocompleteSelectedEvent): void {
|
||||
this.searchKeywords.push(event.option.viewValue);
|
||||
public onRemoveFilter(aFilter: Filter): void {
|
||||
this.selectedFilters = this.selectedFilters.filter((filter) => {
|
||||
return filter.id !== aFilter.id;
|
||||
});
|
||||
|
||||
this.updateFilter();
|
||||
}
|
||||
|
||||
public onSelectFilter(event: MatAutocompleteSelectedEvent): void {
|
||||
this.selectedFilters.push(event.option.value);
|
||||
this.updateFilter();
|
||||
this.searchInput.nativeElement.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 ngOnDestroy() {
|
||||
this.unsubscribeSubject.next();
|
||||
this.unsubscribeSubject.complete();
|
||||
}
|
||||
|
||||
private updateFilter() {
|
||||
this.filters$.next(this.allFilters);
|
||||
this.filters$.next(
|
||||
this.allFilters
|
||||
.filter((filter) => {
|
||||
// Filter selected filters
|
||||
return !this.selectedFilters.some((selectedFilter) => {
|
||||
return selectedFilter.id === filter.id;
|
||||
});
|
||||
})
|
||||
.sort((a, b) => a.label.localeCompare(b.label))
|
||||
);
|
||||
|
||||
// Emit an array with a new reference
|
||||
this.valueChanged.emit([...this.searchKeywords]);
|
||||
this.valueChanged.emit([...this.selectedFilters]);
|
||||
}
|
||||
}
|
||||
|
@ -1,8 +1,9 @@
|
||||
<gf-activities-filter
|
||||
[allFilters]="allFilters"
|
||||
[isLoading]="isLoading"
|
||||
[ngClass]="{ 'd-none': !hasPermissionToFilter }"
|
||||
[placeholder]="placeholder"
|
||||
(valueChanged)="updateFilter($event)"
|
||||
(valueChanged)="filters$.next($event)"
|
||||
></gf-activities-filter>
|
||||
|
||||
<div class="activities">
|
||||
|
@ -14,13 +14,13 @@ import { MatTableDataSource } from '@angular/material/table';
|
||||
import { Router } from '@angular/router';
|
||||
import { Activity } from '@ghostfolio/api/app/order/interfaces/activities.interface';
|
||||
import { getDateFormatString } from '@ghostfolio/common/helper';
|
||||
import { UniqueAsset } from '@ghostfolio/common/interfaces';
|
||||
import { Filter, UniqueAsset } from '@ghostfolio/common/interfaces';
|
||||
import { OrderWithAccount } from '@ghostfolio/common/types';
|
||||
import Big from 'big.js';
|
||||
import { isUUID } from 'class-validator';
|
||||
import { endOfToday, format, isAfter } from 'date-fns';
|
||||
import { isNumber } from 'lodash';
|
||||
import { Subject, Subscription } from 'rxjs';
|
||||
import { distinctUntilChanged, Subject, Subscription, takeUntil } from 'rxjs';
|
||||
|
||||
const SEARCH_PLACEHOLDER = 'Search for account, currency, symbol or type...';
|
||||
const SEARCH_STRING_SEPARATOR = ',';
|
||||
@ -53,11 +53,12 @@ export class ActivitiesTableComponent implements OnChanges, OnDestroy {
|
||||
|
||||
@ViewChild(MatSort) sort: MatSort;
|
||||
|
||||
public allFilters: string[];
|
||||
public allFilters: Filter[];
|
||||
public dataSource: MatTableDataSource<Activity> = new MatTableDataSource();
|
||||
public defaultDateFormat: string;
|
||||
public displayedColumns = [];
|
||||
public endOfToday = endOfToday();
|
||||
public filters$ = new Subject<Filter[]>();
|
||||
public hasDrafts = false;
|
||||
public isAfter = isAfter;
|
||||
public isLoading = true;
|
||||
@ -71,7 +72,13 @@ export class ActivitiesTableComponent implements OnChanges, OnDestroy {
|
||||
|
||||
private unsubscribeSubject = new Subject<void>();
|
||||
|
||||
public constructor(private router: Router) {}
|
||||
public constructor(private router: Router) {
|
||||
this.filters$
|
||||
.pipe(distinctUntilChanged(), takeUntil(this.unsubscribeSubject))
|
||||
.subscribe((filters) => {
|
||||
this.updateFilters(filters);
|
||||
});
|
||||
}
|
||||
|
||||
public ngOnChanges() {
|
||||
this.displayedColumns = [
|
||||
@ -95,11 +102,15 @@ export class ActivitiesTableComponent implements OnChanges, OnDestroy {
|
||||
});
|
||||
}
|
||||
|
||||
this.isLoading = true;
|
||||
|
||||
this.defaultDateFormat = getDateFormatString(this.locale);
|
||||
|
||||
if (this.activities) {
|
||||
this.allFilters = this.getSearchableFieldValues(this.activities).map(
|
||||
(label) => {
|
||||
return { label, id: label, type: 'tag' };
|
||||
}
|
||||
);
|
||||
|
||||
this.dataSource = new MatTableDataSource(this.activities);
|
||||
this.dataSource.filterPredicate = (data, filter) => {
|
||||
const dataString = this.getFilterableValues(data)
|
||||
@ -113,8 +124,8 @@ export class ActivitiesTableComponent implements OnChanges, OnDestroy {
|
||||
return contains;
|
||||
};
|
||||
this.dataSource.sort = this.sort;
|
||||
this.updateFilter();
|
||||
this.isLoading = false;
|
||||
|
||||
this.updateFilters();
|
||||
}
|
||||
}
|
||||
|
||||
@ -172,30 +183,6 @@ export class ActivitiesTableComponent implements OnChanges, OnDestroy {
|
||||
this.activityToUpdate.emit(aActivity);
|
||||
}
|
||||
|
||||
public updateFilter(filters: string[] = []) {
|
||||
this.dataSource.filter = filters.join(SEARCH_STRING_SEPARATOR);
|
||||
const lowercaseSearchKeywords = filters.map((keyword) =>
|
||||
keyword.trim().toLowerCase()
|
||||
);
|
||||
|
||||
this.placeholder =
|
||||
lowercaseSearchKeywords.length <= 0 ? SEARCH_PLACEHOLDER : '';
|
||||
|
||||
this.searchKeywords = filters;
|
||||
|
||||
this.allFilters = this.getSearchableFieldValues(this.activities).filter(
|
||||
(item) => {
|
||||
return !lowercaseSearchKeywords.includes(item.trim().toLowerCase());
|
||||
}
|
||||
);
|
||||
|
||||
this.hasDrafts = this.dataSource.data.some((activity) => {
|
||||
return activity.isDraft === true;
|
||||
});
|
||||
this.totalFees = this.getTotalFees();
|
||||
this.totalValue = this.getTotalValue();
|
||||
}
|
||||
|
||||
public ngOnDestroy() {
|
||||
this.unsubscribeSubject.next();
|
||||
this.unsubscribeSubject.complete();
|
||||
@ -280,4 +267,32 @@ export class ActivitiesTableComponent implements OnChanges, OnDestroy {
|
||||
|
||||
return totalValue.toNumber();
|
||||
}
|
||||
|
||||
private updateFilters(filters: Filter[] = []) {
|
||||
this.isLoading = true;
|
||||
|
||||
this.dataSource.filter = filters
|
||||
.map((filter) => {
|
||||
return filter.label;
|
||||
})
|
||||
.join(SEARCH_STRING_SEPARATOR);
|
||||
const lowercaseSearchKeywords = filters.map((filter) => {
|
||||
return filter.label.trim().toLowerCase();
|
||||
});
|
||||
|
||||
this.placeholder =
|
||||
lowercaseSearchKeywords.length <= 0 ? SEARCH_PLACEHOLDER : '';
|
||||
|
||||
this.searchKeywords = filters.map((filter) => {
|
||||
return filter.label;
|
||||
});
|
||||
|
||||
this.hasDrafts = this.dataSource.filteredData.some((activity) => {
|
||||
return activity.isDraft === true;
|
||||
});
|
||||
this.totalFees = this.getTotalFees();
|
||||
this.totalValue = this.getTotalValue();
|
||||
|
||||
this.isLoading = false;
|
||||
}
|
||||
}
|
||||
|
Loading…
x
Reference in New Issue
Block a user