Implement sidebar filter and dropdown components

This commit is contained in:
Antoine Snyers
2019-11-08 14:03:58 +01:00
parent 4f7b7637cb
commit 771041a3cf
20 changed files with 471 additions and 34 deletions

View File

@@ -1,7 +1,18 @@
<div class="facet-filter d-block mb-3 p-3" *ngIf="active$ | async">
<div (click)="toggle()" class="filter-name"><h5 class="d-inline-block mb-0">{{'search.filters.filter.' + filter.name + '.head'| translate}}</h5> <span class="filter-toggle fas float-right"
[ngClass]="(collapsed$ | async) ? 'fa-plus' : 'fa-minus'"></span></div>
<div [@slide]="(collapsed$ | async) ? 'collapsed' : 'expanded'" (@slide.start)="startSlide($event)" (@slide.done)="finishSlide($event)" class="search-filter-wrapper" [ngClass]="{'closed' : closed}">
<ds-search-facet-filter-wrapper [filterConfig]="filter" [inPlaceSearch]="inPlaceSearch"></ds-search-facet-filter-wrapper>
</div>
<div (click)="toggle()" class="filter-name">
<h5 class="d-inline-block mb-0">
{{'search.filters.filter.' + filter.name + '.head'| translate}}
</h5>
<span class="filter-toggle fas float-right"
[ngClass]="(collapsed$ | async) ? 'fa-plus' : 'fa-minus'">
</span>
</div>
<div [@slide]="(collapsed$ | async) ? 'collapsed' : 'expanded'"
(@slide.start)="startSlide($event)" (@slide.done)="finishSlide($event)"
class="search-filter-wrapper" [ngClass]="{'closed' : closed}">
<ds-search-facet-filter-wrapper
[filterConfig]="filter"
[inPlaceSearch]="inPlaceSearch">
</ds-search-facet-filter-wrapper>
</div>
</div>

View File

@@ -34,6 +34,7 @@ import { SearchLabelComponent } from './search-labels/search-label/search-label.
import { ConfigurationSearchPageComponent } from './configuration-search-page.component';
import { ConfigurationSearchPageGuard } from './configuration-search-page.guard';
import { FilteredSearchPageComponent } from './filtered-search-page.component';
import { SidebarFilterService } from "../shared/sidebar/filter/sidebar-filter.service";
const effects = [
SidebarEffects
@@ -78,6 +79,7 @@ const components = [
declarations: components,
providers: [
SidebarService,
SidebarFilterService,
SearchFilterService,
SearchFixedFilterService,
ConfigurationSearchPageGuard,

View File

@@ -1,24 +1,32 @@
<ng-container *ngVar="(searchOptions$ | async) as config">
<h3>{{ 'search.sidebar.settings.title' | translate}}</h3>
<div *ngIf="config?.sort" class="setting-option result-order-settings mb-3 p-3">
<h5>{{ 'search.sidebar.settings.sort-by' | translate}}</h5>
<select class="form-control" (change)="reloadOrder($event)">
<option *ngFor="let sortOption of searchOptionPossibilities"
[value]="sortOption.field + ',' + sortOption.direction.toString()"
[selected]="sortOption.field === config?.sort.field && sortOption.direction === (config?.sort.direction)? 'selected': null">
{{'sorting.' + sortOption.field + '.' + sortOption.direction | translate}}
</option>
</select>
</div>
<h3>{{ 'search.sidebar.settings.title' | translate}}</h3>
<div class="setting-option page-size-settings mb-3 p-3">
<h5>{{ 'search.sidebar.settings.rpp' | translate}}</h5>
<select class="form-control" (change)="reloadRPP($event)">
<option *ngFor="let pageSizeOption of config?.pagination.pageSizeOptions"
[value]="pageSizeOption"
[selected]="pageSizeOption === +config?.pagination.pageSize ? 'selected': null">
{{pageSizeOption}}
</option>
</select>
</div>
</ng-container>
<div class="result-order-settings">
<ds-sidebar-dropdown
*ngIf="config?.sort"
[id]="'search-sidebar-sort'"
[label]="'search.sidebar.settings.sort-by'"
(change)="reloadOrder($event)"
>
<option *ngFor="let sortOption of searchOptionPossibilities"
[value]="sortOption.field + ',' + sortOption.direction.toString()"
[selected]="sortOption.field === config?.sort.field && sortOption.direction === (config?.sort.direction)? 'selected': null">
{{'sorting.' + sortOption.field + '.' + sortOption.direction | translate}}
</option>
</ds-sidebar-dropdown>
</div>
<div class="page-size-settings">
<ds-sidebar-dropdown
[id]="'search-sidebar-rpp'"
[label]="'search.sidebar.settings.rpp'"
(change)="reloadRPP($event)"
>
<option *ngFor="let pageSizeOption of config?.pagination.pageSizeOptions"
[value]="pageSizeOption"
[selected]="pageSizeOption === +config?.pagination.pageSize ? 'selected': null">
{{pageSizeOption}}
</option>
</ds-sidebar-dropdown>
</div>
</ng-container>

View File

@@ -1,4 +1,4 @@
import { ActionReducerMap, createSelector, MemoizedSelector } from '@ngrx/store';
import { ActionReducerMap, createSelector, MemoizedSelector, State } from '@ngrx/store';
import * as fromRouter from '@ngrx/router-store';
import { hostWindowReducer, HostWindowState } from './shared/host-window.reducer';
import { formReducer, FormState } from './shared/form/form.reducer';
@@ -6,6 +6,10 @@ import {
SidebarState,
sidebarReducer
} from './shared/sidebar/sidebar.reducer';
import {
SidebarFilterState,
sidebarFilterReducer, SidebarFiltersState
} from './shared/sidebar/filter/sidebar-filter.reducer';
import {
filterReducer,
SearchFiltersState
@@ -37,7 +41,8 @@ export interface AppState {
metadataRegistry: MetadataRegistryState;
bitstreamFormats: BitstreamFormatRegistryState;
notifications: NotificationsState;
searchSidebar: SidebarState;
sidebar: SidebarState;
sidebarFilter: SidebarFiltersState;
searchFilter: SearchFiltersState;
truncatable: TruncatablesState;
cssVariables: CSSVariablesState;
@@ -53,7 +58,8 @@ export const appReducers: ActionReducerMap<AppState> = {
metadataRegistry: metadataRegistryReducer,
bitstreamFormats: bitstreamFormatReducer,
notifications: notificationsReducer,
searchSidebar: sidebarReducer,
sidebar: sidebarReducer,
sidebarFilter: sidebarFilterReducer,
searchFilter: filterReducer,
truncatable: truncatableReducer,
cssVariables: cssVariablesReducer,

View File

@@ -148,6 +148,9 @@ import { PublicationGridElementComponent } from './object-grid/item-grid-element
import { ItemTypeBadgeComponent } from './object-list/item-type-badge/item-type-badge.component';
import { ItemMetadataRepresentationListElementComponent } from './object-list/metadata-representation-list-element/item/item-metadata-representation-list-element.component';
import { PageWithSidebarComponent } from './sidebar/page-with-sidebar.component';
import { SidebarDropdownComponent } from './sidebar/sidebar-dropdown.component';
import { SidebarFilterComponent } from './sidebar/filter/sidebar-filter.component';
import { SidebarFilterSelectedOptionComponent } from './sidebar/filter/sidebar-filter-selected-option.component';
const MODULES = [
// Do NOT include UniversalModule, HttpModule, or JsonpModule here
@@ -235,6 +238,9 @@ const COMPONENTS = [
PaginationComponent,
SearchFormComponent,
PageWithSidebarComponent,
SidebarDropdownComponent,
SidebarFilterComponent,
SidebarFilterSelectedOptionComponent,
ThumbnailComponent,
GridThumbnailComponent,
UploaderComponent,

View File

@@ -0,0 +1,6 @@
<a class="d-flex flex-row" (click)="click.emit($event)">
<label>
<input type="checkbox" [checked]="true" class="my-1 align-self-stretch"/>
<span class="filter-value pl-1 text-capitalize">{{label}}</span>
</label>
</a>

View File

@@ -0,0 +1,11 @@
a {
color: $body-color;
&:hover, &focus {
text-decoration: none;
}
span.badge {
vertical-align: text-top;
}
}

View File

@@ -0,0 +1,15 @@
import { Component, EventEmitter, Input, Output } from '@angular/core';
@Component({
selector: 'ds-sidebar-filter-selected-option',
styleUrls: ['./sidebar-filter-selected-option.component.scss'],
templateUrl: './sidebar-filter-selected-option.component.html',
})
/**
* Represents a single selected option in a sidebar filter
*/
export class SidebarFilterSelectedOptionComponent {
@Input() label:string;
@Output() click:EventEmitter<any> = new EventEmitter<any>();
}

View File

@@ -0,0 +1,74 @@
import { Action } from '@ngrx/store';
import { type } from '../../ngrx/type';
/**
* For each action type in an action group, make a simple
* enum object for all of this group's action types.
*
* The 'type' utility function coerces strings into string
* literal types and runs a simple check to guarantee all
* action types in the application are unique.
*/
export const SidebarFilterActionTypes = {
INITIALIZE: type('dspace/sidebar-filter/INITIALIZE'),
COLLAPSE: type('dspace/sidebar-filter/COLLAPSE'),
EXPAND: type('dspace/sidebar-filter/EXPAND'),
TOGGLE: type('dspace/sidebar-filter/TOGGLE'),
};
export class SidebarFilterAction implements Action {
/**
* Name of the filter the action is performed on, used to identify the filter
*/
filterName: string;
/**
* Type of action that will be performed
*/
type;
/**
* Initialize with the filter's name
* @param {string} name of the filter
*/
constructor(name: string) {
this.filterName = name;
}
}
/* tslint:disable:max-classes-per-file */
/**
* Used to initialize a filter
*/
export class FilterInitializeAction extends SidebarFilterAction {
type = SidebarFilterActionTypes.INITIALIZE;
initiallyExpanded;
constructor(name:string, initiallyExpanded:boolean) {
super(name);
this.initiallyExpanded = initiallyExpanded;
}
}
/**
* Used to collapse a filter
*/
export class FilterCollapseAction extends SidebarFilterAction {
type = SidebarFilterActionTypes.COLLAPSE;
}
/**
* Used to expand a filter
*/
export class FilterExpandAction extends SidebarFilterAction {
type = SidebarFilterActionTypes.EXPAND;
}
/**
* Used to collapse a filter when it's expanded and expand it when it's collapsed
*/
export class FilterToggleAction extends SidebarFilterAction {
type = SidebarFilterActionTypes.TOGGLE;
}
/* tslint:enable:max-classes-per-file */

View File

@@ -0,0 +1,24 @@
<div class="facet-filter d-block mb-3 p-3">
<div (click)="toggle()" class="filter-name">
<h5 class="d-inline-block mb-0">
{{ label | translate }}
</h5>
<span class="filter-toggle fas float-right"
[ngClass]="(collapsed$ | async) ? 'fa-plus' : 'fa-minus'">
</span>
</div>
<div [@slide]="(collapsed$ | async) ? 'collapsed' : 'expanded'"
(@slide.start)="startSlide($event)" (@slide.done)="finishSlide($event)"
class="sidebar-filter-wrapper" [ngClass]="{'closed' : closed}">
<div>
<div class="filters py-2">
<ds-sidebar-filter-selected-option
*ngFor="let value of (selectedValues | async)"
[label]="value"
(click)="removeValue.emit(value)">
</ds-sidebar-filter-selected-option>
</div>
<ng-content></ng-content>
</div>
</div>
</div>

View File

@@ -0,0 +1,12 @@
:host .facet-filter {
border: 1px solid map-get($theme-colors, light);
cursor: pointer;
.sidebar-filter-wrapper.closed {
overflow: hidden;
}
.filter-toggle {
line-height: $line-height-base;
}
}

View File

@@ -0,0 +1,84 @@
import { Component, EventEmitter, Input, OnInit, Output } from '@angular/core';
import { Observable } from 'rxjs';
import { SidebarFilterService } from './sidebar-filter.service';
import { slide } from '../../animations/slide';
@Component({
selector: 'ds-sidebar-filter',
styleUrls: ['./sidebar-filter.component.scss'],
templateUrl: './sidebar-filter.component.html',
animations: [slide],
})
export class SidebarFilterComponent implements OnInit {
@Input() name:string;
@Input() type:string;
@Input() label:string;
@Input() expanded = true;
@Input() selectedValues:Observable<string[]>;
@Output() submitValue:EventEmitter<any> = new EventEmitter<any>();
@Output() removeValue:EventEmitter<any> = new EventEmitter<any>();
/**
* True when the filter is 100% collapsed in the UI
*/
closed = true;
/**
* Emits true when the filter is currently collapsed in the store
*/
collapsed$:Observable<boolean>;
constructor(
protected filterService:SidebarFilterService
) {
}
/**
* Changes the state for this filter to collapsed when it's expanded and to expanded it when it's collapsed
*/
toggle() {
this.filterService.toggle(this.name);
}
/**
* Method to change this.collapsed to false when the slide animation ends and is sliding open
* @param event The animation event
*/
finishSlide(event:any):void {
if (event.fromState === 'collapsed') {
this.closed = false;
}
}
/**
* Method to change this.collapsed to true when the slide animation starts and is sliding closed
* @param event The animation event
*/
startSlide(event:any):void {
if (event.toState === 'collapsed') {
this.closed = true;
}
}
ngOnInit():void {
this.initializeFilter();
this.collapsed$ = this.isCollapsed();
}
/**
* Sets the initial state of the filter
*/
initializeFilter() {
this.filterService.initializeFilter(this.name, this.expanded);
}
/**
* Checks if the filter is currently collapsed
* @returns {Observable<boolean>} Emits true when the current state of the filter is collapsed, false when it's expanded
*/
private isCollapsed():Observable<boolean> {
return this.filterService.isCollapsed(this.name);
}
}

View File

@@ -0,0 +1,70 @@
import {
FilterInitializeAction,
SidebarFilterAction,
SidebarFilterActionTypes
} from './sidebar-filter.actions';
/**
* Interface that represents the state for a single filters
*/
export interface SidebarFilterState {
filterCollapsed:boolean,
}
/**
* Interface that represents the state for all available filters
*/
export interface SidebarFiltersState {
[name:string]:SidebarFilterState
}
const initialState:SidebarFiltersState = Object.create(null);
/**
* Performs a filter action on the current state
* @param {SidebarFiltersState} state The state before the action is performed
* @param {SidebarFilterAction} action The action that should be performed
* @returns {SidebarFiltersState} The state after the action is performed
*/
export function sidebarFilterReducer(state = initialState, action:SidebarFilterAction):SidebarFiltersState {
switch (action.type) {
case SidebarFilterActionTypes.INITIALIZE: {
const initAction = (action as FilterInitializeAction);
return Object.assign({}, state, {
[action.filterName]: {
filterCollapsed: !initAction.initiallyExpanded,
}
});
}
case SidebarFilterActionTypes.COLLAPSE: {
return Object.assign({}, state, {
[action.filterName]: {
filterCollapsed: true,
}
});
}
case SidebarFilterActionTypes.EXPAND: {
return Object.assign({}, state, {
[action.filterName]: {
filterCollapsed: false,
}
});
}
case SidebarFilterActionTypes.TOGGLE: {
return Object.assign({}, state, {
[action.filterName]: {
filterCollapsed: !state[action.filterName].filterCollapsed,
}
});
}
default: {
return state;
}
}
}

View File

@@ -0,0 +1,87 @@
import { Injectable } from '@angular/core';
import {
FilterCollapseAction,
FilterExpandAction, FilterInitializeAction,
FilterToggleAction
} from './sidebar-filter.actions';
import { createSelector, MemoizedSelector, select, Store } from '@ngrx/store';
import { SidebarFiltersState, SidebarFilterState } from './sidebar-filter.reducer';
import { Observable } from 'rxjs';
import { distinctUntilChanged, map } from 'rxjs/operators';
import { hasValue } from '../../empty.util';
@Injectable()
export class SidebarFilterService {
constructor(private store:Store<SidebarFilterState>) {
}
/**
* Dispatches an initialize action to the store for a given filter
* @param {string} filter The filter for which the action is dispatched
* @param {boolean} expanded If the filter should be open from the start
*/
public initializeFilter(filter:string, expanded:boolean):void {
this.store.dispatch(new FilterInitializeAction(filter, expanded));
}
/**
* Dispatches a collapse action to the store for a given filter
* @param {string} filterName The filter for which the action is dispatched
*/
public collapse(filterName:string):void {
this.store.dispatch(new FilterCollapseAction(filterName));
}
/**
* Dispatches an expand action to the store for a given filter
* @param {string} filterName The filter for which the action is dispatched
*/
public expand(filterName:string):void {
this.store.dispatch(new FilterExpandAction(filterName));
}
/**
* Dispatches a toggle action to the store for a given filter
* @param {string} filterName The filter for which the action is dispatched
*/
public toggle(filterName:string):void {
this.store.dispatch(new FilterToggleAction(filterName));
}
/**
* Checks if the state of a given filter is currently collapsed or not
* @param {string} filterName The filtername for which the collapsed state is checked
* @returns {Observable<boolean>} Emits the current collapsed state of the given filter, if it's unavailable, return false
*/
isCollapsed(filterName:string):Observable<boolean> {
return this.store.pipe(
select(filterByNameSelector(filterName)),
map((object:SidebarFilterState) => {
if (object) {
return object.filterCollapsed;
} else {
return false;
}
}),
distinctUntilChanged()
);
}
}
const filterStateSelector = (state:SidebarFiltersState) => state.sidebarFilter;
function filterByNameSelector(name:string):MemoizedSelector<SidebarFiltersState, SidebarFilterState> {
return keySelector<SidebarFilterState>(name);
}
export function keySelector<T>(key:string):MemoizedSelector<SidebarFiltersState, T> {
return createSelector(filterStateSelector, (state:SidebarFilterState) => {
if (hasValue(state)) {
return state[key];
} else {
return undefined;
}
});
}

View File

@@ -2,8 +2,8 @@ import { By } from '@angular/platform-browser';
import { of as observableOf } from 'rxjs';
import { ComponentFixture, TestBed } from '@angular/core/testing';
import { PageWithSidebarComponent } from './page-with-sidebar.component';
import { SidebarService } from './sidebar/sidebar.service';
import { HostWindowService } from '../shared/host-window.service';
import { SidebarService } from './sidebar.service';
import { HostWindowService } from '../host-window.service';
import { NoopAnimationsModule } from '@angular/platform-browser/animations';
describe('PageWithSidebarComponent', () => {

View File

@@ -0,0 +1,6 @@
<div class="setting-option mb-3 p-3">
<h5><label for="{{id}}">{{label | translate}}</label></h5>
<select id="{{id}}" class="form-control" (change)="change.emit($event)">
<ng-content></ng-content>
</select>
</div>

View File

@@ -0,0 +1,3 @@
.setting-option {
border: 1px solid map-get($theme-colors, light);
}

View File

@@ -0,0 +1,12 @@
import { Component, EventEmitter, Input, Output } from '@angular/core';
@Component({
selector: 'ds-sidebar-dropdown',
styleUrls: ['./sidebar-dropdown.component.scss'],
templateUrl: './sidebar-dropdown.component.html',
})
export class SidebarDropdownComponent {
@Input() id:string;
@Input() label:string;
@Output() change:EventEmitter<any> = new EventEmitter<number>();
}

View File

@@ -12,7 +12,7 @@ const initialState: SidebarState = {
};
/**
* Performs a search sidebar action on the current state
* Performs a sidebar action on the current state
* @param {SidebarState} state The state before the action is performed
* @param {SidebarAction} action The action that should be performed
* @returns {SidebarState} The state after the action is performed

View File

@@ -7,7 +7,7 @@ import { AppState } from '../../app.reducer';
import { HostWindowService } from '../host-window.service';
import { map } from 'rxjs/operators';
const sidebarStateSelector = (state: AppState) => state.searchSidebar;
const sidebarStateSelector = (state: AppState) => state.sidebar;
const sidebarCollapsedSelector = createSelector(sidebarStateSelector, (sidebar: SidebarState) => sidebar.sidebarCollapsed);
/**