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;
}
/**
* 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;
}

View File

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

View File

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

View File

@ -7,18 +7,12 @@
</div>
<!-- Current filters -->
<div class="current-filters" *ngIf="filterService && filterService.activeFilterCount">
<div><span translate>Active filters</span>:&nbsp;</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 }}&nbsp;
<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>

View File

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

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 { 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)
*/

View File

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