45621: finished filter facets for search sidebar

This commit is contained in:
Lotte Hofstede
2017-11-13 13:52:21 +01:00
parent be6f5ea9b5
commit 202b045009
18 changed files with 182 additions and 98 deletions

View File

@@ -94,9 +94,9 @@
"filters": {
"head": "Filters",
"reset": "Reset filters",
"facet-filter": {
"filter": {
"show-more": "Show more",
"show-less": "Show less",
"show-less": "Collapse",
"author": {
"placeholder": "Author name",
"head": "Author"
@@ -108,6 +108,10 @@
"subject": {
"placeholder": "Subject",
"head": "Subject"
},
"date": {
"placeholder": "Date",
"head": "Date"
}
}
}

View File

@@ -1,15 +1,32 @@
<a *ngFor="let value of filterValues; let i=index" class="d-block" [routerLink]="[getSearchLink()]"
[queryParams]="getQueryParams(value) | async">
<ng-template [ngIf]="i < (facetCount | async)">
<input type="checkbox" [checked]="isChecked(value) | async"/>
<span class="filter-value">{{value.value}}</span>
<span class="filter-value-count float-right">({{value.count}})</span>
</ng-template>
</a>
<a *ngIf="filterValues.length > (facetCount | async)" (click)="showMore()">{{"search.filters.facet-filter.show-more"
| translate}}</a>
<a *ngIf="(currentPage | async) > 1" (click)="showLess()">{{"search.filters.facet-filter.show-less" |
translate}}</a>
<input type="text"
[placeholder]="'search.filters.facet-filter.' + filterConfig.name + '.placeholder'| translate"/>
<div>
<div class="filters">
<a *ngFor="let value of selectedValues" class="d-block"
[routerLink]="[getSearchLink()]"
[queryParams]="getQueryParamsWithout(value) | async">
<input type="checkbox" [checked]="true"/>
<span class="filter-value">{{value}}</span>
</a>
<a *ngFor="let value of filterValues; let i=index" class="d-block"
[routerLink]="[getSearchLink()]"
[queryParams]="getQueryParamsWith(value.value) | async">
<ng-template [ngIf]="i < (facetCount | async)">
<input type="checkbox" [checked]="false"/>
<span class="filter-value">{{value.value}}</span>
<span class="filter-value-count float-right">({{value.count}})</span>
</ng-template>
</a>
<div class="clearfix toggle-more-filters">
<a class="float-left" *ngIf="filterValues.length > (facetCount | async)"
(click)="showMore()">{{"search.filters.filter.show-more"
| translate}}</a>
<a class="float-right" *ngIf="(currentPage | async) > 1" (click)="showLess()">{{"search.filters.filter.show-less"
| translate}}</a>
</div>
</div>
<form #form="ngForm" (ngSubmit)="onSubmit(form.value)" class="add-filter">
<input type="text" [(ngModel)]="filter" name="filter" class="form-control"
aria-label="New filter input"
[placeholder]="'search.filters.filter.' + filterConfig.name + '.placeholder'| translate"/>
<input type="submit" class="d-none"/>
</form>
</div>

View File

@@ -1,2 +1,18 @@
@import '../../../../../styles/variables.scss';
@import '../../../../../styles/mixins.scss';
.filters {
margin-top: $spacer/2;
margin-bottom: $spacer/2;
a {
color: $body-color;
&:hover {
text-decoration: none;
}
}
.toggle-more-filters a {
color: $link-color;
text-decoration: underline;
cursor: pointer;
}
}

View File

@@ -1,10 +1,10 @@
import { Component, Input, OnInit } from '@angular/core';
import { FacetValue } from '../../../search-service/facet-value.model';
import { SearchFilterConfig } from '../../../search-service/search-filter-config.model';
import { SearchService } from '../../../search-service/search.service';
import { Params } from '@angular/router';
import { Params, Router } from '@angular/router';
import { Observable } from 'rxjs/Observable';
import { SearchFilterService } from '../search-filter.service';
import { isNotEmpty } from '../../../../shared/empty.util';
/**
* This component renders a simple item page.
@@ -21,9 +21,11 @@ import { SearchFilterService } from '../search-filter.service';
export class SidebarFacetFilterComponent implements OnInit {
@Input() filterValues: FacetValue[];
@Input() filterConfig: SearchFilterConfig;
@Input() selectedValues: string[];
currentPage: Observable<number>;
filter: string;
constructor(private filterService: SearchFilterService) {
constructor(private filterService: SearchFilterService, private router: Router) {
}
ngOnInit(): void {
@@ -38,8 +40,12 @@ export class SidebarFacetFilterComponent implements OnInit {
return this.filterService.searchLink;
}
getQueryParams(value: FacetValue): Observable<Params> {
return this.filterService.getFilterValueURL(this.filterConfig, value.value);
getQueryParamsWith(value: string): Observable<Params> {
return this.filterService.getQueryParamsWith(this.filterConfig, value);
}
getQueryParamsWithout(value: string): Observable<Params> {
return this.filterService.getQueryParamsWithout(this.filterConfig, value);
}
get facetCount(): Observable<number> {
@@ -60,6 +66,16 @@ export class SidebarFacetFilterComponent implements OnInit {
getCurrentPage(): Observable<number> {
return this.filterService.getPage(this.filterConfig.name);
}
onSubmit(data: any) {
if (isNotEmpty(data.filter)) {
this.getQueryParamsWith(data.filter).first().subscribe((a) => {
this.router.navigate([this.getSearchLink()], { queryParams: a }
);
}
);
this.filter = '';
}
}
}

View File

@@ -1,8 +1,8 @@
<div>
<div (click)="toggle()" class="filter-name">{{filter.name}} <span class="fa float-right"
<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 fa float-right"
[ngClass]="(isCollapsed() | async) ? 'fa-plus' : 'fa-minus'"></span></div>
<div [@slide]="(isCollapsed() | async) ? 'collapsed' : 'expanded'" class="search-filter-wrapper">
<ds-search-facet-filter [filterConfig]="filter"
[filterValues]="(filterValues | async)?.payload"></ds-search-facet-filter>
[filterValues]="(filterValues | async)?.payload" [selectedValues]="getSelectedValues() | async"></ds-search-facet-filter>
</div>
</div>

View File

@@ -1,6 +1,12 @@
@import '../../../../styles/variables.scss';
@import '../../../../styles/mixins.scss';
.search-filter-wrapper {
overflow: hidden;
:host {
border: 1px solid map-get($theme-colors, light);
.search-filter-wrapper {
overflow: hidden;
}
.filter-toggle {
line-height: $line-height-base;
}
}

View File

@@ -6,6 +6,7 @@ import { FacetValue } from '../../search-service/facet-value.model';
import { SearchFilterService } from './search-filter.service';
import { Observable } from 'rxjs/Observable';
import { slide } from '../../../shared/animations/slide';
import { RouteService } from '../../../shared/route.service';
/**
* This component renders a simple item page.
@@ -17,7 +18,7 @@ import { slide } from '../../../shared/animations/slide';
selector: 'ds-search-filter',
styleUrls: ['./search-filter.component.scss'],
templateUrl: './search-filter.component.html',
animations: [slide]
animations: [slide],
})
export class SidebarFilterComponent implements OnInit {
@@ -51,4 +52,8 @@ export class SidebarFilterComponent implements OnInit {
initialExpand() {
this.filterService.initialExpand(this.filter.name);
}
getSelectedValues(): Observable<string[]> {
return this.filterService.getSelectedValuesForFilter(this.filter);
}
}

View File

@@ -11,7 +11,6 @@ import {
SearchFilterToggleAction
} from './search-filter.actions';
import { hasValue, } from '../../../shared/empty.util';
import { Params } from '@angular/router';
import { SearchFilterConfig } from '../../search-service/search-filter-config.model';
import { SearchService } from '../../search-service/search.service';
import { RouteService } from '../../../shared/route.service';
@@ -30,14 +29,16 @@ export class SearchFilterService {
return this.routeService.hasQueryParamWithValue(paramName, filterValue);
}
getFilterValueURL(filterConfig: SearchFilterConfig, value: string): Observable<Params> {
return this.isFilterActive(filterConfig.paramName, value).flatMap((isActive) => {
if (isActive) {
return this.routeService.removeQueryParameterValue(filterConfig.paramName, value);
} else {
return this.routeService.addQueryParameterValue(filterConfig.paramName, value);
}
})
getQueryParamsWithout(filterConfig: SearchFilterConfig, value: string) {
return this.routeService.removeQueryParameterValue(filterConfig.paramName, value);
}
getQueryParamsWith(filterConfig: SearchFilterConfig, value: string) {
return this.routeService.addQueryParameterValue(filterConfig.paramName, value);
}
getSelectedValuesForFilter(filterConfig: SearchFilterConfig): Observable<string[]> {
return this.routeService.getQueryParameterValues(filterConfig.paramName);
}
get searchLink() {

View File

@@ -1,7 +1,7 @@
<h2>{{"search.filters.head" | translate}}</h2>
<div *ngIf="(filters | async).hasSucceeded">
<div *ngFor="let filter of (filters | async).payload">
<ds-search-filter [filter]="filter"></ds-search-filter>
<ds-search-filter class="d-block mb-3 p-3" [filter]="filter"></ds-search-filter>
</div>
</div>
<a class="btn btn-primary" [routerLink]="[getSearchLink()]" [queryParams]="getClearFiltersQueryParams()" role="button">{{"search.filters.reset" | translate}}</a>

View File

@@ -1,4 +1,4 @@
import { Component, Input } from '@angular/core';
import { Component } from '@angular/core';
import { SearchService } from '../search-service/search.service';
import { RemoteData } from '../../core/data/remote-data';
import { SearchFilterConfig } from '../search-service/search-filter-config.model';

View File

@@ -2,7 +2,7 @@
<div class="search-page row">
<ds-search-sidebar *ngIf="!(isMobileView | async)" class="col-3 sidebar-sm-sticky"
id="search-sidebar"
resultCount="{{(resultsRDObs | async)?.pageInfo?.totalElements}}"></ds-search-sidebar>
[resultCount]="(resultsRDObs | async)?.pageInfo?.totalElements"></ds-search-sidebar>
<div class="col-12 col-sm-9">
<ds-search-form id="search-form"
[query]="query"
@@ -16,7 +16,7 @@
[@pushInOut]="(isSidebarCollapsed() | async) ? 'collapsed' : 'expanded'">
<ds-search-sidebar *ngIf="(isMobileView | async)" class="col-12"
id="search-sidebar-xs"
resultCount="{{(results | async)?.pageInfo.totalElements}}"
[resultCount]="(resultsRDObs | async)?.pageInfo?.totalElements"
(toggleSidebar)="closeSidebar()"
[ngClass]="{'active': !(isSidebarCollapsed() | async)}">
</ds-search-sidebar>
@@ -37,3 +37,4 @@
</div>
</div>
</div>

View File

@@ -90,7 +90,6 @@ export class SearchPageComponent implements OnInit, OnDestroy {
}
private updateSearchResults(searchOptions) {
// Resolve search results
this.resultsRDObs = this.service.search(this.query, this.scope, searchOptions);
}

View File

@@ -5,7 +5,7 @@ export class SearchFilterConfig {
name: string;
type: FilterType;
hasFacets: boolean;
pageSize = 3;
pageSize = 2;
isOpenByDefault: boolean;
/**
* Name of this configuration that can be used in a url

View File

@@ -15,6 +15,7 @@ import { FilterType } from './filter-type.model';
import { FacetValue } from './facet-value.model';
import { ViewMode } from '../../+search-page/search-options.model';
import { Router, NavigationExtras, ActivatedRoute, Params } from '@angular/router';
import { RouteService } from '../../shared/route.service';
function shuffle(array: any[]) {
let i = 0;
@@ -81,6 +82,7 @@ export class SearchService implements OnDestroy {
];
constructor(private itemDataService: ItemDataService,
private routeService: RouteService,
private route: ActivatedRoute,
private router: Router) {
@@ -126,31 +128,31 @@ export class SearchService implements OnDestroy {
.filter((rd: RemoteData<Item[]>) => rd.hasSucceeded)
.map((rd: RemoteData<Item[]>) => {
const totalElements = rd.pageInfo.totalElements > 20 ? 20 : rd.pageInfo.totalElements;
const pageInfo = Object.assign({}, rd.pageInfo, { totalElements: totalElements });
const totalElements = rd.pageInfo.totalElements > 20 ? 20 : rd.pageInfo.totalElements;
const pageInfo = Object.assign({}, rd.pageInfo, { totalElements: totalElements });
const payload = shuffle(rd.payload)
.map((item: Item, index: number) => {
const mockResult: SearchResult<DSpaceObject> = new ItemSearchResult();
mockResult.dspaceObject = item;
const highlight = new Metadatum();
highlight.key = 'dc.description.abstract';
highlight.value = this.mockedHighlights[index % this.mockedHighlights.length];
mockResult.hitHighlights = new Array(highlight);
return mockResult;
});
const payload = shuffle(rd.payload)
.map((item: Item, index: number) => {
const mockResult: SearchResult<DSpaceObject> = new ItemSearchResult();
mockResult.dspaceObject = item;
const highlight = new Metadatum();
highlight.key = 'dc.description.abstract';
highlight.value = this.mockedHighlights[index % this.mockedHighlights.length];
mockResult.hitHighlights = new Array(highlight);
return mockResult;
});
return new RemoteData(
self,
rd.isRequestPending,
rd.isResponsePending,
rd.hasSucceeded,
errorMessage,
statusCode,
pageInfo,
payload
)
}).startWith(new RemoteData(
return new RemoteData(
self,
rd.isRequestPending,
rd.isResponsePending,
rd.hasSucceeded,
errorMessage,
statusCode,
pageInfo,
payload
)
}).startWith(new RemoteData(
'',
true,
false,
@@ -182,39 +184,43 @@ export class SearchService implements OnDestroy {
}
getFacetValuesFor(searchFilterConfigName: string): Observable<RemoteData<FacetValue[]>> {
const filterConfig = this.config.find((config: SearchFilterConfig) => config.name === searchFilterConfigName);
const values: FacetValue[] = [];
for (let i = 0; i < 5; i++) {
const value = searchFilterConfigName + ' ' + (i + 1);
values.push({
value: value,
count: Math.floor(Math.random() * 20) + 20 * (5 - i), // make sure first results have the highest (random) count
search: decodeURI(this.router.url) + (this.router.url.includes('?') ? '&' : '?') + filterConfig.paramName + '=' + value
});
}
const requestPending = false;
const responsePending = false;
const isSuccessful = true;
const errorMessage = undefined;
const statusCode = '200';
const returningPageInfo = new PageInfo();
return Observable.of(new RemoteData(
'https://dspace7.4science.it/dspace-spring-rest/api/search',
requestPending,
responsePending,
isSuccessful,
errorMessage,
statusCode,
returningPageInfo,
values
));
return this.routeService.getQueryParameterValues(filterConfig.paramName).map((selectedValues: string[]) => {
const values: FacetValue[] = [];
for (let i = 0; i < 5; i++) {
const value = searchFilterConfigName + ' ' + (i + 1);
if (!selectedValues.includes(value)) {
values.push({
value: value,
count: Math.floor(Math.random() * 20) + 20 * (5 - i), // make sure first results have the highest (random) count
search: decodeURI(this.router.url) + (this.router.url.includes('?') ? '&' : '?') + filterConfig.paramName + '=' + value
});
}
}
const requestPending = false;
const responsePending = false;
const isSuccessful = true;
const errorMessage = undefined;
const statusCode = '200';
const returningPageInfo = new PageInfo();
return new RemoteData(
'https://dspace7.4science.it/dspace-spring-rest/api/search',
requestPending,
responsePending,
isSuccessful,
errorMessage,
statusCode,
returningPageInfo,
values
)
}
)
}
getViewMode(): Observable<ViewMode> {
return this.route.queryParams.map((params) => {
if (isNotEmpty(params.view) && hasValue(params.view)) {
return params.view;
return this.routeService.getQueryParameterValue('view').map((value) => {
if (hasValue(value)) {
return value as ViewMode;
} else {
return ViewMode.List;
}

View File

@@ -5,4 +5,7 @@
.results {
line-height: $button-height;
}
ds-view-mode-switch {
margin-bottom: $spacer;
}
}

View File

@@ -32,6 +32,7 @@ import { ServerResponseService } from '../shared/server-response.service';
import { NativeWindowFactory, NativeWindowService } from '../shared/window.service';
import { BrowseService } from './browse/browse.service';
import { BrowseResponseParsingService } from './data/browse-response-parsing.service';
import { RouteService } from '../shared/route.service';
const IMPORTS = [
CommonModule,
@@ -65,6 +66,7 @@ const PROVIDERS = [
ServerResponseService,
BrowseResponseParsingService,
BrowseService,
RouteService,
{ provide: NativeWindowService, useFactory: NativeWindowFactory }
];

View File

@@ -1,4 +1,4 @@
import { animate, state, transition, trigger, style } from '@angular/animations';
import { animate, state, transition, trigger, style, stagger, query } from '@angular/animations';
export const slide = trigger('slide', [
@@ -6,5 +6,5 @@ export const slide = trigger('slide', [
state('collapsed', style({ height: 0 })),
transition('expanded <=> collapsed', animate(250)),
transition('expanded <=> collapsed', animate(250))
]);

View File

@@ -1,4 +1,4 @@
import { Injectable, OnDestroy } from '@angular/core';
import { Injectable } from '@angular/core';
import { Observable } from 'rxjs/Observable';
import { ActivatedRoute, convertToParamMap, Params, } from '@angular/router';
import { isNotEmpty } from './empty.util';
@@ -9,6 +9,14 @@ export class RouteService {
constructor(private route: ActivatedRoute) {
}
getQueryParameterValues(paramName: string): Observable<string[]> {
return this.route.queryParamMap.map((map) => map.getAll(paramName));
}
getQueryParameterValue(paramName: string): Observable<string> {
return this.route.queryParamMap.map((map) => map.get(paramName));
}
hasQueryParam(paramName: string): Observable<boolean> {
return this.route.queryParamMap.map((map) => {return map.has(paramName);});
}