mirror of
https://github.com/DSpace/dspace-angular.git
synced 2025-10-13 04:53:06 +00:00
45621: finished filter facets for search sidebar
This commit is contained in:
@@ -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"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@@ -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>
|
@@ -1,2 +1,18 @@
|
||||
@import '../../../../../styles/variables.scss';
|
||||
@import '../../../../../styles/mixins.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;
|
||||
}
|
||||
}
|
@@ -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 = '';
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@@ -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>
|
@@ -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;
|
||||
}
|
||||
}
|
@@ -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);
|
||||
}
|
||||
}
|
||||
|
@@ -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() {
|
||||
|
@@ -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>
|
@@ -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';
|
||||
|
@@ -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>
|
||||
|
||||
|
@@ -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);
|
||||
}
|
||||
|
||||
|
@@ -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
|
||||
|
@@ -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;
|
||||
}
|
||||
|
@@ -5,4 +5,7 @@
|
||||
.results {
|
||||
line-height: $button-height;
|
||||
}
|
||||
ds-view-mode-switch {
|
||||
margin-bottom: $spacer;
|
||||
}
|
||||
}
|
||||
|
@@ -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 }
|
||||
];
|
||||
|
||||
|
@@ -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))
|
||||
]);
|
||||
|
@@ -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);});
|
||||
}
|
||||
|
Reference in New Issue
Block a user