Add detailed filter information
Adds detailed filter information into the table custom head bar. Filters are scroll-able horizontally
This commit is contained in:
parent
ae618fce20
commit
b4cbf5646f
@ -38,6 +38,14 @@ export interface OsFilterOption {
|
||||
isActive?: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
* Unique indicated filter with a label and a filter option
|
||||
*/
|
||||
export interface OsFilterIndicator {
|
||||
property: string;
|
||||
option: OsFilterOption;
|
||||
}
|
||||
|
||||
/**
|
||||
* Define the type of a filter condition
|
||||
*/
|
||||
@ -71,6 +79,28 @@ export abstract class BaseFilterListService<V extends BaseViewModel> {
|
||||
return this.inputData ? this.inputData.length : 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* @returns the amount of items that pass the filter service's filters
|
||||
*/
|
||||
public get filteredCount(): number {
|
||||
return this.outputSubject.getValue().length;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns all OsFilters containing active filters
|
||||
*/
|
||||
public get activeFilters(): OsFilter[] {
|
||||
return this.filterDefinitions.filter(def => def.options.find((option: OsFilterOption) => option.isActive));
|
||||
}
|
||||
|
||||
public get filterCount(): number {
|
||||
if (this.filterDefinitions) {
|
||||
return this.filterDefinitions.reduce((a, b) => a + (b.count || 0), 0);
|
||||
} else {
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* The observable output for the filtered data
|
||||
*/
|
||||
@ -83,20 +113,6 @@ export abstract class BaseFilterListService<V extends BaseViewModel> {
|
||||
return this.outputSubject.asObservable();
|
||||
}
|
||||
|
||||
/**
|
||||
* @returns the amount of items that pass the filter service's filters
|
||||
*/
|
||||
public get filteredCount(): number {
|
||||
return this.outputSubject.getValue().length;
|
||||
}
|
||||
|
||||
/**
|
||||
* @returns the amount of currently active filters
|
||||
*/
|
||||
public get activeFilterCount(): number {
|
||||
return this.filterDefinitions ? this.filterDefinitions.filter(filter => filter.count).length : 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Boolean indicating if there are any filters described in this service
|
||||
*
|
||||
@ -106,6 +122,18 @@ export abstract class BaseFilterListService<V extends BaseViewModel> {
|
||||
return !!this.filterDefinitions && this.filterDefinitions.length > 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* Stack OsFilters
|
||||
*/
|
||||
private _filterStack: OsFilterIndicator[] = [];
|
||||
|
||||
/**
|
||||
* get stacked filters
|
||||
*/
|
||||
public get filterStack(): OsFilterIndicator[] {
|
||||
return this._filterStack;
|
||||
}
|
||||
|
||||
/**
|
||||
* Constructor.
|
||||
*
|
||||
@ -124,6 +152,7 @@ export abstract class BaseFilterListService<V extends BaseViewModel> {
|
||||
|
||||
if (storedFilter && this.isOsFilter(storedFilter)) {
|
||||
this.filterDefinitions = storedFilter;
|
||||
this.activeFiltersToStack();
|
||||
} else {
|
||||
this.filterDefinitions = this.getFilterDefinitions();
|
||||
this.storeActiveFilters();
|
||||
@ -139,6 +168,23 @@ export abstract class BaseFilterListService<V extends BaseViewModel> {
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Recreates the filter stack out of active filter definitions
|
||||
*/
|
||||
private activeFiltersToStack(): void {
|
||||
const stack: OsFilterIndicator[] = [];
|
||||
for (const activeFilter of this.activeFilters) {
|
||||
const activeOptions = activeFilter.options.filter((option: OsFilterOption) => option.isActive);
|
||||
for (const option of activeOptions) {
|
||||
stack.push({
|
||||
property: activeFilter.property,
|
||||
option: option as OsFilterOption
|
||||
});
|
||||
}
|
||||
}
|
||||
this._filterStack = stack;
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if the (stored) filter list matches the current definition of OsFilter
|
||||
*
|
||||
@ -169,10 +215,11 @@ export abstract class BaseFilterListService<V extends BaseViewModel> {
|
||||
public setFilterDefinitions(): void {
|
||||
if (this.filterDefinitions) {
|
||||
const newDefinitions = this.getFilterDefinitions();
|
||||
this.store.get('filter_' + this.name).then((storedDefinition: OsFilter[]) => {
|
||||
|
||||
this.store.get<OsFilter[]>('filter_' + this.name).then(storedFilter => {
|
||||
for (const newDef of newDefinitions) {
|
||||
let count = 0;
|
||||
const matchingExistingFilter = storedDefinition.find(oldDef => oldDef.property === newDef.property);
|
||||
const matchingExistingFilter = storedFilter.find(oldDef => oldDef.property === newDef.property);
|
||||
for (const option of newDef.options) {
|
||||
if (typeof option === 'object') {
|
||||
if (matchingExistingFilter && matchingExistingFilter.options) {
|
||||
@ -270,6 +317,7 @@ export abstract class BaseFilterListService<V extends BaseViewModel> {
|
||||
}
|
||||
|
||||
this.outputSubject.next(filteredData);
|
||||
this.activeFiltersToStack();
|
||||
}
|
||||
|
||||
/**
|
||||
@ -304,8 +352,11 @@ export abstract class BaseFilterListService<V extends BaseViewModel> {
|
||||
const filterOption = filter.options.find(
|
||||
o => typeof o !== 'string' && o.condition === option.condition
|
||||
) as OsFilterOption;
|
||||
|
||||
if (filterOption && !filterOption.isActive) {
|
||||
filterOption.isActive = true;
|
||||
this._filterStack.push({ property: filterProperty, option: option });
|
||||
|
||||
if (!filter.count) {
|
||||
filter.count = 1;
|
||||
} else {
|
||||
@ -329,6 +380,18 @@ export abstract class BaseFilterListService<V extends BaseViewModel> {
|
||||
) as OsFilterOption;
|
||||
if (filterOption && filterOption.isActive) {
|
||||
filterOption.isActive = false;
|
||||
|
||||
// remove filter from stack
|
||||
const removeIndex = this._filterStack
|
||||
.map(stacked => stacked.option)
|
||||
.findIndex(mappedOption => {
|
||||
return mappedOption.condition === option.condition;
|
||||
});
|
||||
|
||||
if (removeIndex > -1) {
|
||||
this._filterStack.splice(removeIndex, 1);
|
||||
}
|
||||
|
||||
if (filter.count) {
|
||||
filter.count -= 1;
|
||||
}
|
||||
|
@ -1,31 +1,33 @@
|
||||
<mat-accordion (keyup)="checkKeyEvent($event)">
|
||||
<mat-expansion-panel *ngFor="let filter of service.filterDefinitions">
|
||||
<mat-expansion-panel-header *ngIf="filter.options && filter.options.length">
|
||||
<mat-panel-title>
|
||||
<mat-icon>
|
||||
{{ filter.count ? 'checked' : '' }}
|
||||
</mat-icon>
|
||||
<span>{{ service.getFilterName(filter) | translate }}</span>
|
||||
</mat-panel-title>
|
||||
</mat-expansion-panel-header>
|
||||
<div class="indent" *ngIf="filter.options && filter.options.length">
|
||||
<mat-action-list class="filtermenu-expanded">
|
||||
<div *ngFor="let option of filter.options">
|
||||
<div *ngIf="isFilter(option)">
|
||||
<mat-checkbox
|
||||
class="filter-title"
|
||||
[checked]="option.isActive"
|
||||
(change)="service.toggleFilterOption(filter.property, option)"
|
||||
>
|
||||
{{ option.label | translate }}
|
||||
</mat-checkbox>
|
||||
<div class="filter-menu">
|
||||
<mat-accordion (keyup)="checkKeyEvent($event)">
|
||||
<mat-expansion-panel *ngFor="let filter of service.filterDefinitions">
|
||||
<mat-expansion-panel-header *ngIf="filter.options && filter.options.length">
|
||||
<mat-panel-title>
|
||||
<span>{{ service.getFilterName(filter) | translate }}</span>
|
||||
<mat-basic-chip disableRipple class="lightblue filter-count" *ngIf="filter.count">
|
||||
<span>{{ filter.count }}</span>
|
||||
</mat-basic-chip>
|
||||
</mat-panel-title>
|
||||
</mat-expansion-panel-header>
|
||||
<div class="indent" *ngIf="filter.options && filter.options.length">
|
||||
<mat-action-list class="filtermenu-expanded">
|
||||
<div *ngFor="let option of filter.options">
|
||||
<div *ngIf="isFilter(option)">
|
||||
<mat-checkbox
|
||||
class="filter-title"
|
||||
[checked]="option.isActive"
|
||||
(change)="service.toggleFilterOption(filter.property, option)"
|
||||
>
|
||||
{{ option.label | translate }}
|
||||
</mat-checkbox>
|
||||
</div>
|
||||
<div class="filter-subtitle" *ngIf="!isFilter(option)">
|
||||
<mat-divider *ngIf="option === '-'"></mat-divider>
|
||||
<span *ngIf="option !== '-'"> {{ option | translate }}</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="filter-subtitle" *ngIf="!isFilter(option)">
|
||||
<mat-divider *ngIf="option === '-'"></mat-divider>
|
||||
<span *ngIf="option !== '-'"> {{ option | translate }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</mat-action-list>
|
||||
</div>
|
||||
</mat-expansion-panel>
|
||||
</mat-accordion>
|
||||
</mat-action-list>
|
||||
</div>
|
||||
</mat-expansion-panel>
|
||||
</mat-accordion>
|
||||
</div>
|
||||
|
@ -7,11 +7,20 @@ mat-divider {
|
||||
margin-bottom: 5px;
|
||||
}
|
||||
|
||||
.filter-menu {
|
||||
overflow-y: scroll;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.mat-expansion-panel {
|
||||
width: 400px;
|
||||
max-width: 95vw;
|
||||
}
|
||||
|
||||
.filter-count {
|
||||
margin-left: 10px;
|
||||
}
|
||||
|
||||
.filter-subtitle {
|
||||
margin-top: 5px;
|
||||
margin-bottom: 5px;
|
||||
|
@ -7,18 +7,12 @@
|
||||
</div>
|
||||
|
||||
<!-- Current filters -->
|
||||
<div class="current-filters" *ngIf="filterService && filterService.activeFilterCount">
|
||||
<div><span translate>Active filters</span>: </div>
|
||||
<div>
|
||||
<button mat-button (click)="filterService.clearAllFilters()">
|
||||
<mat-icon inline>cancel</mat-icon>
|
||||
<span translate>Clear all</span>
|
||||
</button>
|
||||
</div>
|
||||
<div *ngFor="let filter of filterService.filterDefinitions">
|
||||
<button mat-button *ngIf="filter.count" (click)="filterService.clearFilter(filter)">
|
||||
<mat-icon inline>close</mat-icon>
|
||||
<span>{{ filterService.getFilterName(filter) | translate }}</span>
|
||||
<div class="current-filters" *ngIf="filterService">
|
||||
<div *ngFor="let filter of filterService.filterStack">
|
||||
<button mat-stroked-button (click)="removeFilterFromStack(filter)">
|
||||
<os-icon-container icon="close">
|
||||
{{ filter.option.label | translate }}
|
||||
</os-icon-container>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
@ -27,11 +21,8 @@
|
||||
<div class="action-buttons">
|
||||
<!-- Filter button -->
|
||||
<button mat-button *ngIf="hasFilters" (click)="filterMenu.opened ? filterMenu.close() : filterMenu.open()">
|
||||
<span *ngIf="!filterService.activeFilterCount" class="upper" translate> Filter </span>
|
||||
<span *ngIf="filterService.activeFilterCount">
|
||||
{{ filterService.activeFilterCount }}
|
||||
<span *ngIf="filterService.activeFilterCount === 1" class="upper" translate>Filter</span>
|
||||
<span *ngIf="filterService.activeFilterCount > 1" class="upper" translate>Filters</span>
|
||||
<span class="upper" [matBadge]="filterAmount" matBadgeColor="accent" [matBadgeOverlap]="false" translate>
|
||||
Filter
|
||||
</span>
|
||||
</button>
|
||||
|
||||
@ -59,10 +50,17 @@
|
||||
</div>
|
||||
|
||||
<!-- Header for the filter side bar -->
|
||||
<mat-drawer #filterMenu mode="push" position="end">
|
||||
<div class="custom-table-header filter-menu" (click)="this.filterMenu.toggle()">
|
||||
<span><mat-icon>keyboard_arrow_right</mat-icon></span>
|
||||
<span class="right-with-margin" translate>Filter options</span>
|
||||
<mat-drawer autoFocus=false #filterMenu mode="push" position="end">
|
||||
<div class="custom-table-header filter-menu-head" (click)="this.filterMenu.toggle()">
|
||||
<span>
|
||||
<mat-icon>keyboard_arrow_right</mat-icon>
|
||||
</span>
|
||||
|
||||
<button mat-button (click)="onClearAllButton($event)" *ngIf="filterAmount">
|
||||
<os-icon-container icon="clear">
|
||||
<span translate>Clear all filters</span>
|
||||
</os-icon-container>
|
||||
</button>
|
||||
</div>
|
||||
<os-filter-menu *ngIf="filterService" (dismissed)="this.filterMenu.close()" [service]="filterService">
|
||||
</os-filter-menu>
|
||||
|
@ -1,13 +1,17 @@
|
||||
.filter-menu {
|
||||
margin-left: 5px;
|
||||
justify-content: space-between;
|
||||
:hover {
|
||||
cursor: pointer;
|
||||
.filter-menu-head {
|
||||
padding-left: 5px;
|
||||
|
||||
.mat-button {
|
||||
margin: 0 auto;
|
||||
}
|
||||
}
|
||||
|
||||
.filter-menu-head:hover {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.action-buttons {
|
||||
margin-left: auto;
|
||||
display: flex;
|
||||
}
|
||||
|
||||
span.right-with-margin {
|
||||
@ -16,21 +20,52 @@ span.right-with-margin {
|
||||
|
||||
.filter-count {
|
||||
font-style: italic;
|
||||
margin-right: 10px;
|
||||
margin-left: 10px;
|
||||
min-width: 50px;
|
||||
text-overflow: ellipsis;
|
||||
overflow: hidden;
|
||||
white-space: nowrap;
|
||||
margin-right: 10px;
|
||||
}
|
||||
|
||||
.current-filters {
|
||||
width: 100%;
|
||||
display: flex;
|
||||
justify-content: flex-start;
|
||||
margin: 0 5px;
|
||||
button {
|
||||
padding: 3px;
|
||||
font-size: 80%;
|
||||
margin: 5px;
|
||||
|
||||
> div {
|
||||
display: inherit;
|
||||
}
|
||||
|
||||
button {
|
||||
padding: 5px;
|
||||
margin: auto 5px;
|
||||
font-size: 75%;
|
||||
}
|
||||
|
||||
.mat-icon {
|
||||
$size: 12px;
|
||||
font-size: $size;
|
||||
height: $size;
|
||||
width: $size;
|
||||
}
|
||||
|
||||
overflow-x: auto;
|
||||
|
||||
// firefox scrollbar
|
||||
scrollbar-width: 5px;
|
||||
scrollbar-color: #666666;
|
||||
}
|
||||
|
||||
// custom scrollbar. If required on more components, move to style.scss
|
||||
.current-filters::-webkit-scrollbar {
|
||||
width: 5px;
|
||||
height: 5px;
|
||||
}
|
||||
|
||||
.current-filters::-webkit-scrollbar-thumb {
|
||||
background: #666666;
|
||||
height: 5px;
|
||||
border-radius: 5px;
|
||||
}
|
||||
|
||||
.current-filters::-webkit-scrollbar-corner {
|
||||
display: none;
|
||||
height: 0px;
|
||||
width: 0px;
|
||||
}
|
||||
|
@ -1,4 +1,4 @@
|
||||
import { Input, Output, Component, ViewChild, EventEmitter } from '@angular/core';
|
||||
import { Input, Output, Component, ViewChild, EventEmitter, ViewEncapsulation } from '@angular/core';
|
||||
import { MatBottomSheet } from '@angular/material';
|
||||
|
||||
import { TranslateService } from '@ngx-translate/core';
|
||||
@ -9,7 +9,7 @@ import { FilterMenuComponent } from './filter-menu/filter-menu.component';
|
||||
import { OsSortingOption } from 'app/core/ui-services/base-sort-list.service';
|
||||
import { BaseSortListService } from 'app/core/ui-services/base-sort-list.service';
|
||||
import { ViewportService } from 'app/core/ui-services/viewport.service';
|
||||
import { BaseFilterListService } from 'app/core/ui-services/base-filter-list.service';
|
||||
import { BaseFilterListService, OsFilterIndicator } from 'app/core/ui-services/base-filter-list.service';
|
||||
|
||||
/**
|
||||
* Reusable bar for list views, offering sorting and filter options.
|
||||
@ -28,7 +28,8 @@ import { BaseFilterListService } from 'app/core/ui-services/base-filter-list.ser
|
||||
@Component({
|
||||
selector: 'os-sort-filter-bar',
|
||||
templateUrl: './sort-filter-bar.component.html',
|
||||
styleUrls: ['./sort-filter-bar.component.scss']
|
||||
styleUrls: ['./sort-filter-bar.component.scss'],
|
||||
encapsulation: ViewEncapsulation.None
|
||||
})
|
||||
export class SortFilterBarComponent<V extends BaseViewModel> {
|
||||
/**
|
||||
@ -125,6 +126,13 @@ export class SortFilterBarComponent<V extends BaseViewModel> {
|
||||
return this.sortService.sortOptions;
|
||||
}
|
||||
|
||||
public get filterAmount(): number {
|
||||
if (this.filterService) {
|
||||
const filterCount = this.filterService.filterCount;
|
||||
return !!filterCount ? filterCount : null;
|
||||
}
|
||||
}
|
||||
|
||||
public set sortOption(option: OsSortingOption<V>) {
|
||||
this.sortService.sortProperty = option.property;
|
||||
}
|
||||
@ -143,6 +151,22 @@ export class SortFilterBarComponent<V extends BaseViewModel> {
|
||||
this.filterMenu = new FilterMenuComponent();
|
||||
}
|
||||
|
||||
/**
|
||||
* on Click, remove Filter
|
||||
* @param filter
|
||||
*/
|
||||
public removeFilterFromStack(filter: OsFilterIndicator): void {
|
||||
this.filterService.toggleFilterOption(filter.property, filter.option);
|
||||
}
|
||||
|
||||
/**
|
||||
* Clear all filters
|
||||
*/
|
||||
public onClearAllButton(event: MouseEvent): void {
|
||||
event.stopPropagation();
|
||||
this.filterService.clearAllFilters();
|
||||
}
|
||||
|
||||
/**
|
||||
* Handles the sorting menu/bottom sheet (depending on state of mobile/desktop)
|
||||
*/
|
||||
|
@ -393,7 +393,6 @@ mat-card {
|
||||
text-align: right;
|
||||
border-bottom: 1px solid rgba(0, 0, 0, 0.12);
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
|
||||
button {
|
||||
border-radius: 0%;
|
||||
|
Loading…
Reference in New Issue
Block a user