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;
|
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;
|
||||||
}
|
}
|
||||||
|
@ -1,11 +1,12 @@
|
|||||||
|
<div class="filter-menu">
|
||||||
<mat-accordion (keyup)="checkKeyEvent($event)">
|
<mat-accordion (keyup)="checkKeyEvent($event)">
|
||||||
<mat-expansion-panel *ngFor="let filter of service.filterDefinitions">
|
<mat-expansion-panel *ngFor="let filter of service.filterDefinitions">
|
||||||
<mat-expansion-panel-header *ngIf="filter.options && filter.options.length">
|
<mat-expansion-panel-header *ngIf="filter.options && filter.options.length">
|
||||||
<mat-panel-title>
|
<mat-panel-title>
|
||||||
<mat-icon>
|
|
||||||
{{ filter.count ? 'checked' : '' }}
|
|
||||||
</mat-icon>
|
|
||||||
<span>{{ service.getFilterName(filter) | translate }}</span>
|
<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-panel-title>
|
||||||
</mat-expansion-panel-header>
|
</mat-expansion-panel-header>
|
||||||
<div class="indent" *ngIf="filter.options && filter.options.length">
|
<div class="indent" *ngIf="filter.options && filter.options.length">
|
||||||
@ -29,3 +30,4 @@
|
|||||||
</div>
|
</div>
|
||||||
</mat-expansion-panel>
|
</mat-expansion-panel>
|
||||||
</mat-accordion>
|
</mat-accordion>
|
||||||
|
</div>
|
||||||
|
@ -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;
|
||||||
|
@ -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>: </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 }}
|
|
||||||
<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>
|
||||||
|
@ -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 {
|
||||||
|
display: inherit;
|
||||||
|
}
|
||||||
|
|
||||||
button {
|
button {
|
||||||
padding: 3px;
|
padding: 5px;
|
||||||
font-size: 80%;
|
margin: auto 5px;
|
||||||
margin: 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 { 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)
|
||||||
*/
|
*/
|
||||||
|
@ -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%;
|
||||||
|
Loading…
Reference in New Issue
Block a user