Add detailed filter information

Adds detailed filter information into the table custom head bar.
Filters are scroll-able horizontally
This commit is contained in:
Sean Engelhardt 2019-06-27 16:38:21 +02:00
parent ae618fce20
commit b4cbf5646f
7 changed files with 218 additions and 88 deletions

View File

@ -38,6 +38,14 @@ export interface OsFilterOption {
isActive?: boolean; 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 * 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; 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 * The observable output for the filtered data
*/ */
@ -83,20 +113,6 @@ export abstract class BaseFilterListService<V extends BaseViewModel> {
return this.outputSubject.asObservable(); 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 * 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; return !!this.filterDefinitions && this.filterDefinitions.length > 0;
} }
/**
* Stack OsFilters
*/
private _filterStack: OsFilterIndicator[] = [];
/**
* get stacked filters
*/
public get filterStack(): OsFilterIndicator[] {
return this._filterStack;
}
/** /**
* Constructor. * Constructor.
* *
@ -124,6 +152,7 @@ export abstract class BaseFilterListService<V extends BaseViewModel> {
if (storedFilter && this.isOsFilter(storedFilter)) { if (storedFilter && this.isOsFilter(storedFilter)) {
this.filterDefinitions = storedFilter; this.filterDefinitions = storedFilter;
this.activeFiltersToStack();
} else { } else {
this.filterDefinitions = this.getFilterDefinitions(); this.filterDefinitions = this.getFilterDefinitions();
this.storeActiveFilters(); 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 * 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 { public setFilterDefinitions(): void {
if (this.filterDefinitions) { if (this.filterDefinitions) {
const newDefinitions = this.getFilterDefinitions(); 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) { for (const newDef of newDefinitions) {
let count = 0; 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) { for (const option of newDef.options) {
if (typeof option === 'object') { if (typeof option === 'object') {
if (matchingExistingFilter && matchingExistingFilter.options) { if (matchingExistingFilter && matchingExistingFilter.options) {
@ -270,6 +317,7 @@ export abstract class BaseFilterListService<V extends BaseViewModel> {
} }
this.outputSubject.next(filteredData); this.outputSubject.next(filteredData);
this.activeFiltersToStack();
} }
/** /**
@ -304,8 +352,11 @@ export abstract class BaseFilterListService<V extends BaseViewModel> {
const filterOption = filter.options.find( const filterOption = filter.options.find(
o => typeof o !== 'string' && o.condition === option.condition o => typeof o !== 'string' && o.condition === option.condition
) as OsFilterOption; ) as OsFilterOption;
if (filterOption && !filterOption.isActive) { if (filterOption && !filterOption.isActive) {
filterOption.isActive = true; filterOption.isActive = true;
this._filterStack.push({ property: filterProperty, option: option });
if (!filter.count) { if (!filter.count) {
filter.count = 1; filter.count = 1;
} else { } else {
@ -329,6 +380,18 @@ export abstract class BaseFilterListService<V extends BaseViewModel> {
) as OsFilterOption; ) as OsFilterOption;
if (filterOption && filterOption.isActive) { if (filterOption && filterOption.isActive) {
filterOption.isActive = false; 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) { if (filter.count) {
filter.count -= 1; filter.count -= 1;
} }

View File

@ -1,31 +1,33 @@
<mat-accordion (keyup)="checkKeyEvent($event)"> <div class="filter-menu">
<mat-expansion-panel *ngFor="let filter of service.filterDefinitions"> <mat-accordion (keyup)="checkKeyEvent($event)">
<mat-expansion-panel-header *ngIf="filter.options && filter.options.length"> <mat-expansion-panel *ngFor="let filter of service.filterDefinitions">
<mat-panel-title> <mat-expansion-panel-header *ngIf="filter.options && filter.options.length">
<mat-icon> <mat-panel-title>
{{ filter.count ? 'checked' : '' }} <span>{{ service.getFilterName(filter) | translate }}</span>
</mat-icon> <mat-basic-chip disableRipple class="lightblue filter-count" *ngIf="filter.count">
<span>{{ service.getFilterName(filter) | translate }}</span> <span>{{ filter.count }}</span>
</mat-panel-title> </mat-basic-chip>
</mat-expansion-panel-header> </mat-panel-title>
<div class="indent" *ngIf="filter.options && filter.options.length"> </mat-expansion-panel-header>
<mat-action-list class="filtermenu-expanded"> <div class="indent" *ngIf="filter.options && filter.options.length">
<div *ngFor="let option of filter.options"> <mat-action-list class="filtermenu-expanded">
<div *ngIf="isFilter(option)"> <div *ngFor="let option of filter.options">
<mat-checkbox <div *ngIf="isFilter(option)">
class="filter-title" <mat-checkbox
[checked]="option.isActive" class="filter-title"
(change)="service.toggleFilterOption(filter.property, option)" [checked]="option.isActive"
> (change)="service.toggleFilterOption(filter.property, option)"
{{ option.label | translate }} >
</mat-checkbox> {{ 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>
<div class="filter-subtitle" *ngIf="!isFilter(option)"> </mat-action-list>
<mat-divider *ngIf="option === '-'"></mat-divider> </div>
<span *ngIf="option !== '-'"> {{ option | translate }}</span> </mat-expansion-panel>
</div> </mat-accordion>
</div> </div>
</mat-action-list>
</div>
</mat-expansion-panel>
</mat-accordion>

View File

@ -7,11 +7,20 @@ mat-divider {
margin-bottom: 5px; margin-bottom: 5px;
} }
.filter-menu {
overflow-y: scroll;
height: 100%;
}
.mat-expansion-panel { .mat-expansion-panel {
width: 400px; width: 400px;
max-width: 95vw; max-width: 95vw;
} }
.filter-count {
margin-left: 10px;
}
.filter-subtitle { .filter-subtitle {
margin-top: 5px; margin-top: 5px;
margin-bottom: 5px; margin-bottom: 5px;

View File

@ -7,18 +7,12 @@
</div> </div>
<!-- Current filters --> <!-- Current filters -->
<div class="current-filters" *ngIf="filterService && filterService.activeFilterCount"> <div class="current-filters" *ngIf="filterService">
<div><span translate>Active filters</span>:&nbsp;</div> <div *ngFor="let filter of filterService.filterStack">
<div> <button mat-stroked-button (click)="removeFilterFromStack(filter)">
<button mat-button (click)="filterService.clearAllFilters()"> <os-icon-container icon="close">
<mat-icon inline>cancel</mat-icon> {{ filter.option.label | translate }}
<span translate>Clear all</span> </os-icon-container>
</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>
</button> </button>
</div> </div>
</div> </div>
@ -27,11 +21,8 @@
<div class="action-buttons"> <div class="action-buttons">
<!-- Filter button --> <!-- Filter button -->
<button mat-button *ngIf="hasFilters" (click)="filterMenu.opened ? filterMenu.close() : filterMenu.open()"> <button mat-button *ngIf="hasFilters" (click)="filterMenu.opened ? filterMenu.close() : filterMenu.open()">
<span *ngIf="!filterService.activeFilterCount" class="upper" translate> Filter </span> <span class="upper" [matBadge]="filterAmount" matBadgeColor="accent" [matBadgeOverlap]="false" translate>
<span *ngIf="filterService.activeFilterCount"> Filter
{{ filterService.activeFilterCount }}&nbsp;
<span *ngIf="filterService.activeFilterCount === 1" class="upper" translate>Filter</span>
<span *ngIf="filterService.activeFilterCount > 1" class="upper" translate>Filters</span>
</span> </span>
</button> </button>
@ -59,10 +50,17 @@
</div> </div>
<!-- Header for the filter side bar --> <!-- Header for the filter side bar -->
<mat-drawer #filterMenu mode="push" position="end"> <mat-drawer autoFocus=false #filterMenu mode="push" position="end">
<div class="custom-table-header filter-menu" (click)="this.filterMenu.toggle()"> <div class="custom-table-header filter-menu-head" (click)="this.filterMenu.toggle()">
<span><mat-icon>keyboard_arrow_right</mat-icon></span> <span>
<span class="right-with-margin" translate>Filter options</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> </div>
<os-filter-menu *ngIf="filterService" (dismissed)="this.filterMenu.close()" [service]="filterService"> <os-filter-menu *ngIf="filterService" (dismissed)="this.filterMenu.close()" [service]="filterService">
</os-filter-menu> </os-filter-menu>

View File

@ -1,13 +1,17 @@
.filter-menu { .filter-menu-head {
margin-left: 5px; padding-left: 5px;
justify-content: space-between;
:hover { .mat-button {
cursor: pointer; margin: 0 auto;
} }
} }
.filter-menu-head:hover {
cursor: pointer;
}
.action-buttons { .action-buttons {
margin-left: auto; display: flex;
} }
span.right-with-margin { span.right-with-margin {
@ -16,21 +20,52 @@ span.right-with-margin {
.filter-count { .filter-count {
font-style: italic; font-style: italic;
margin-right: 10px;
margin-left: 10px; margin-left: 10px;
min-width: 50px; margin-right: 10px;
text-overflow: ellipsis;
overflow: hidden;
white-space: nowrap;
} }
.current-filters { .current-filters {
width: 100%;
display: flex; display: flex;
justify-content: flex-start;
margin: 0 5px; > div {
button { display: inherit;
padding: 3px;
font-size: 80%;
margin: 5px;
} }
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;
} }

View File

@ -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 { MatBottomSheet } from '@angular/material';
import { TranslateService } from '@ngx-translate/core'; 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 { OsSortingOption } from 'app/core/ui-services/base-sort-list.service';
import { BaseSortListService } 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 { 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. * 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({ @Component({
selector: 'os-sort-filter-bar', selector: 'os-sort-filter-bar',
templateUrl: './sort-filter-bar.component.html', 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> { export class SortFilterBarComponent<V extends BaseViewModel> {
/** /**
@ -125,6 +126,13 @@ export class SortFilterBarComponent<V extends BaseViewModel> {
return this.sortService.sortOptions; 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>) { public set sortOption(option: OsSortingOption<V>) {
this.sortService.sortProperty = option.property; this.sortService.sortProperty = option.property;
} }
@ -143,6 +151,22 @@ export class SortFilterBarComponent<V extends BaseViewModel> {
this.filterMenu = new FilterMenuComponent(); 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) * Handles the sorting menu/bottom sheet (depending on state of mobile/desktop)
*/ */

View File

@ -393,7 +393,6 @@ mat-card {
text-align: right; text-align: right;
border-bottom: 1px solid rgba(0, 0, 0, 0.12); border-bottom: 1px solid rgba(0, 0, 0, 0.12);
display: flex; display: flex;
justify-content: flex-end;
button { button {
border-radius: 0%; border-radius: 0%;