mirror of
https://github.com/DSpace/dspace-angular.git
synced 2025-10-07 10:04:11 +00:00
facet, filter, pagination implementation using rest api
This commit is contained in:
@@ -18,7 +18,8 @@ module.exports = {
|
||||
// Caching settings
|
||||
cache: {
|
||||
// NOTE: how long should objects be cached for by default
|
||||
msToLive: 15 * 60 * 1000, // 15 minute
|
||||
msToLive: 15 * 60 * 1000, // 15 minutes
|
||||
// msToLive: 1000, // 15 minutes
|
||||
control: 'max-age=60' // revalidate browser
|
||||
},
|
||||
// Angular Universal settings
|
||||
|
@@ -116,6 +116,7 @@
|
||||
"@angular/compiler-cli": "^5.2.5",
|
||||
"@ngrx/store-devtools": "^5.1.0",
|
||||
"@ngtools/webpack": "^1.10.0",
|
||||
"@types/acorn": "^4.0.3",
|
||||
"@types/cookie-parser": "1.4.1",
|
||||
"@types/deep-freeze": "0.1.1",
|
||||
"@types/express": "^4.11.1",
|
||||
|
@@ -120,6 +120,10 @@
|
||||
"dateIssued": {
|
||||
"placeholder": "Date",
|
||||
"head": "Date"
|
||||
},
|
||||
"has_content_in_original_bundle": {
|
||||
"placeholder": "Has files",
|
||||
"head": "Has files"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
20
src/app/+search-page/paginated-search-options.model.ts
Normal file
20
src/app/+search-page/paginated-search-options.model.ts
Normal file
@@ -0,0 +1,20 @@
|
||||
import { SortOptions } from '../core/cache/models/sort-options.model';
|
||||
import { PaginationComponentOptions } from '../shared/pagination/pagination-component-options.model';
|
||||
import { isNotEmpty } from '../shared/empty.util';
|
||||
import { URLCombiner } from '../core/url-combiner/url-combiner';
|
||||
import { SearchOptions } from './search-options.model';
|
||||
|
||||
export class PaginatedSearchOptions extends SearchOptions {
|
||||
pagination?: PaginationComponentOptions;
|
||||
sort?: SortOptions;
|
||||
toRestUrl(url: string, args: string[] = []): string {
|
||||
if (isNotEmpty(this.sort)) {
|
||||
args.push(`sort=${this.sort.field},${this.sort.direction}`);
|
||||
}
|
||||
if (isNotEmpty(this.pagination)) {
|
||||
args.push(`page=${this.pagination.currentPage - 1}`);
|
||||
args.push(`size=${this.pagination.pageSize}`);
|
||||
}
|
||||
return super.toRestUrl(url, args);
|
||||
}
|
||||
}
|
@@ -2,26 +2,29 @@
|
||||
<div class="filters">
|
||||
<a *ngFor="let value of selectedValues" class="d-block"
|
||||
[routerLink]="[getSearchLink()]"
|
||||
[queryParams]="getQueryParamsWithout(value) | async">
|
||||
[queryParams]="getRemoveParams(value)" queryParamsHandling="merge">
|
||||
<input type="checkbox" [checked]="true"/>
|
||||
<span class="filter-value">{{value}}</span>
|
||||
</a>
|
||||
<a *ngFor="let value of filterValues; let i=index" class="d-block clearfix"
|
||||
<ng-container *ngFor="let page of (filterValues$ | async)">
|
||||
<ng-container *ngFor="let value of (page | async)?.payload.page; let i=index">
|
||||
<a *ngIf="!selectedValues.includes(value.value)" class="d-block clearfix"
|
||||
[routerLink]="[getSearchLink()]"
|
||||
[queryParams]="getQueryParamsWith(value.value) | async">
|
||||
<ng-template [ngIf]="i < (facetCount | async)">
|
||||
[queryParams]="getAddParams(value.value)" queryParamsHandling="merge" >
|
||||
<input type="checkbox" [checked]="false"/>
|
||||
<span class="filter-value">{{value.value}}</span>
|
||||
<span class="float-right filter-value-count">
|
||||
<span class="badge badge-secondary badge-pill">{{value.count}}</span>
|
||||
</span>
|
||||
</ng-template>
|
||||
</a>
|
||||
</ng-container>
|
||||
</ng-container>
|
||||
<div class="clearfix toggle-more-filters">
|
||||
<a class="float-left" *ngIf="filterValues.length > (facetCount | async)"
|
||||
<a class="float-left" *ngIf="!(isLastPage() | async)"
|
||||
(click)="showMore()">{{"search.filters.filter.show-more"
|
||||
| translate}}</a>
|
||||
<a class="float-right" *ngIf="(currentPage | async) > 1" (click)="showFirstPageOnly()">{{"search.filters.filter.show-less"
|
||||
<a class="float-right" *ngIf="(currentPage | async) > 1"
|
||||
(click)="showFirstPageOnly()">{{"search.filters.filter.show-less"
|
||||
| translate}}</a>
|
||||
</div>
|
||||
</div>
|
||||
|
@@ -1,10 +1,16 @@
|
||||
import { Component, Input, OnInit } from '@angular/core';
|
||||
import { Component, Input, OnDestroy, OnInit } from '@angular/core';
|
||||
import { FacetValue } from '../../../search-service/facet-value.model';
|
||||
import { SearchFilterConfig } from '../../../search-service/search-filter-config.model';
|
||||
import { Params, Router } from '@angular/router';
|
||||
import { Router } from '@angular/router';
|
||||
import { Observable } from 'rxjs/Observable';
|
||||
import { SearchFilterService } from '../search-filter.service';
|
||||
import { isNotEmpty } from '../../../../shared/empty.util';
|
||||
import { hasValue, isNotEmpty } from '../../../../shared/empty.util';
|
||||
import { RemoteData } from '../../../../core/data/remote-data';
|
||||
import { PaginatedList } from '../../../../core/data/paginated-list';
|
||||
import { SearchService } from '../../../search-service/search.service';
|
||||
import { SearchOptions } from '../../../search-options.model';
|
||||
import { BehaviorSubject } from 'rxjs/BehaviorSubject';
|
||||
import { Subscription } from 'rxjs/Subscription';
|
||||
|
||||
/**
|
||||
* This component renders a simple item page.
|
||||
@@ -15,21 +21,43 @@ import { isNotEmpty } from '../../../../shared/empty.util';
|
||||
@Component({
|
||||
selector: 'ds-search-facet-filter',
|
||||
styleUrls: ['./search-facet-filter.component.scss'],
|
||||
templateUrl: './search-facet-filter.component.html',
|
||||
templateUrl: './search-facet-filter.component.html'
|
||||
})
|
||||
|
||||
export class SearchFacetFilterComponent implements OnInit {
|
||||
@Input() filterValues: FacetValue[];
|
||||
export class SearchFacetFilterComponent implements OnInit, OnDestroy {
|
||||
@Input() filterConfig: SearchFilterConfig;
|
||||
@Input() selectedValues: string[];
|
||||
filterValues: Array<Observable<RemoteData<PaginatedList<FacetValue>>>> = [];
|
||||
filterValues$: BehaviorSubject<any> = new BehaviorSubject(this.filterValues);
|
||||
currentPage: Observable<number>;
|
||||
filter: string;
|
||||
pageChange = false;
|
||||
sub: Subscription;
|
||||
|
||||
constructor(private filterService: SearchFilterService, private router: Router) {
|
||||
constructor(private searchService: SearchService, private filterService: SearchFilterService, private router: Router) {
|
||||
}
|
||||
|
||||
ngOnInit(): void {
|
||||
this.currentPage = this.filterService.getPage(this.filterConfig.name);
|
||||
this.currentPage = this.getCurrentPage();
|
||||
this.currentPage.distinctUntilChanged().subscribe((page) => this.pageChange = true);
|
||||
this.filterService.getSearchOptions().distinctUntilChanged().subscribe((options) => this.updateFilterValueList(options));
|
||||
}
|
||||
|
||||
updateFilterValueList(options: SearchOptions) {
|
||||
if (!this.pageChange) {
|
||||
this.showFirstPageOnly();
|
||||
}
|
||||
this.pageChange = false;
|
||||
|
||||
this.unsubscribe();
|
||||
|
||||
this.sub = this.currentPage.distinctUntilChanged().map((page) => {
|
||||
return this.searchService.getFacetValuesFor(this.filterConfig, page, options);
|
||||
}).subscribe((newValues$) => {
|
||||
this.filterValues = [...this.filterValues, newValues$];
|
||||
this.filterValues$.next(this.filterValues);
|
||||
});
|
||||
// this.filterValues.subscribe((c) => c.map((a) => a.subscribe((b) => console.log(b))));
|
||||
}
|
||||
|
||||
isChecked(value: FacetValue): Observable<boolean> {
|
||||
@@ -37,23 +65,7 @@ export class SearchFacetFilterComponent implements OnInit {
|
||||
}
|
||||
|
||||
getSearchLink() {
|
||||
return this.filterService.searchLink;
|
||||
}
|
||||
|
||||
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> {
|
||||
const resultCount = this.filterValues.length;
|
||||
return this.currentPage.map((page: number) => {
|
||||
const max = page * this.filterConfig.pageSize;
|
||||
return max > resultCount ? resultCount : max;
|
||||
});
|
||||
return this.searchService.getSearchLink();
|
||||
}
|
||||
|
||||
showMore() {
|
||||
@@ -61,6 +73,7 @@ export class SearchFacetFilterComponent implements OnInit {
|
||||
}
|
||||
|
||||
showFirstPageOnly() {
|
||||
this.filterValues = [];
|
||||
this.filterService.resetPage(this.filterConfig.name);
|
||||
}
|
||||
|
||||
@@ -74,13 +87,39 @@ export class SearchFacetFilterComponent implements OnInit {
|
||||
|
||||
onSubmit(data: any) {
|
||||
if (isNotEmpty(data)) {
|
||||
const sub = this.getQueryParamsWith(data[this.filterConfig.paramName]).first().subscribe((params) => {
|
||||
this.router.navigate([this.getSearchLink()], { queryParams: params }
|
||||
);
|
||||
}
|
||||
);
|
||||
this.router.navigate([this.getSearchLink()], {
|
||||
queryParams:
|
||||
{ [this.filterConfig.paramName]: [...this.selectedValues, data[this.filterConfig.paramName]] },
|
||||
queryParamsHandling: 'merge'
|
||||
});
|
||||
this.filter = '';
|
||||
sub.unsubscribe();
|
||||
}
|
||||
}
|
||||
|
||||
hasValue(o: any): boolean {
|
||||
return hasValue(o);
|
||||
}
|
||||
|
||||
isLastPage(): Observable<boolean> {
|
||||
return Observable.of(false);
|
||||
// return this.filterValues.flatMap((map) => map.pop().map((rd: RemoteData<PaginatedList<FacetValue>>) => rd.payload.currentPage >= rd.payload.totalPages));
|
||||
}
|
||||
|
||||
getRemoveParams(value: string) {
|
||||
return { [this.filterConfig.paramName]: this.selectedValues.filter((v) => v !== value) };
|
||||
}
|
||||
|
||||
getAddParams(value: string) {
|
||||
return { [this.filterConfig.paramName]: [...this.selectedValues, value] };
|
||||
}
|
||||
|
||||
ngOnDestroy(): void {
|
||||
this.unsubscribe();
|
||||
}
|
||||
|
||||
unsubscribe(): void {
|
||||
if (this.sub !== undefined) {
|
||||
this.sub.unsubscribe();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@@ -2,7 +2,6 @@
|
||||
<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" [selectedValues]="getSelectedValues() | async"></ds-search-facet-filter>
|
||||
<ds-search-facet-filter [filterConfig]="filter" [selectedValues]="getSelectedValues() | async"></ds-search-facet-filter>
|
||||
</div>
|
||||
</div>
|
@@ -23,13 +23,11 @@ import { PaginatedList } from '../../../core/data/paginated-list';
|
||||
|
||||
export class SearchFilterComponent implements OnInit {
|
||||
@Input() filter: SearchFilterConfig;
|
||||
filterValues: Observable<RemoteData<FacetValue[] | PaginatedList<FacetValue>>>;
|
||||
|
||||
constructor(private searchService: SearchService, private filterService: SearchFilterService) {
|
||||
constructor(private filterService: SearchFilterService) {
|
||||
}
|
||||
|
||||
ngOnInit() {
|
||||
this.filterValues = this.searchService.getFacetValuesFor(this.filter.name, '', '');
|
||||
const sub = this.filterService.isFilterActive(this.filter.paramName).first().subscribe((isActive) => {
|
||||
if (this.filter.isOpenByDefault || isActive) {
|
||||
this.initialExpand();
|
||||
|
@@ -14,6 +14,11 @@ import { hasValue, } from '../../../shared/empty.util';
|
||||
import { SearchFilterConfig } from '../../search-service/search-filter-config.model';
|
||||
import { SearchService } from '../../search-service/search.service';
|
||||
import { RouteService } from '../../../shared/route.service';
|
||||
import ObjectExpression from 'rollup/dist/typings/ast/nodes/ObjectExpression';
|
||||
import { SortDirection, SortOptions } from '../../../core/cache/models/sort-options.model';
|
||||
import { PaginationComponentOptions } from '../../../shared/pagination/pagination-component-options.model';
|
||||
import { SearchOptions } from '../../search-options.model';
|
||||
import { PaginatedSearchOptions } from '../../paginated-search-options.model';
|
||||
|
||||
const filterStateSelector = (state: SearchFiltersState) => state.searchFilter;
|
||||
|
||||
@@ -21,8 +26,7 @@ const filterStateSelector = (state: SearchFiltersState) => state.searchFilter;
|
||||
export class SearchFilterService {
|
||||
|
||||
constructor(private store: Store<SearchFiltersState>,
|
||||
private routeService: RouteService,
|
||||
private searchService: SearchService) {
|
||||
private routeService: RouteService) {
|
||||
}
|
||||
|
||||
isFilterActiveWithValue(paramName: string, filterValue: string): Observable<boolean> {
|
||||
@@ -33,22 +37,85 @@ export class SearchFilterService {
|
||||
return this.routeService.hasQueryParam(paramName);
|
||||
}
|
||||
|
||||
getQueryParamsWithout(filterConfig: SearchFilterConfig, value: string) {
|
||||
return this.routeService.removeQueryParameterValue(filterConfig.paramName, value);
|
||||
getCurrentScope() {
|
||||
return this.routeService.getQueryParameterValue('scope');
|
||||
}
|
||||
|
||||
getQueryParamsWith(filterConfig: SearchFilterConfig, value: string) {
|
||||
return this.routeService.addQueryParameterValue(filterConfig.paramName, value);
|
||||
getCurrentQuery() {
|
||||
return this.routeService.getQueryParameterValue('query');
|
||||
}
|
||||
|
||||
getCurrentPagination(pagination: any = {}): Observable<PaginationComponentOptions> {
|
||||
const page$ = this.routeService.getQueryParameterValue('page');
|
||||
const size$ = this.routeService.getQueryParameterValue('pageSize');
|
||||
return Observable.combineLatest(page$, size$, (page, size) => {
|
||||
return Object.assign(new PaginationComponentOptions(), pagination, {
|
||||
currentPage: page || 1,
|
||||
pageSize: size || pagination.pageSize
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
getCurrentSort(): Observable<SortOptions> {
|
||||
const sortDirection$ = this.routeService.getQueryParameterValue('sortDirection');
|
||||
const sortField$ = this.routeService.getQueryParameterValue('sortField');
|
||||
return Observable.combineLatest(sortDirection$, sortField$, (sortDirection, sortField) => new SortOptions(sortField || undefined, SortDirection[sortDirection]));
|
||||
}
|
||||
|
||||
getCurrentFilters() {
|
||||
return this.routeService.getQueryParamsWithPrefix('f.');
|
||||
}
|
||||
|
||||
getCurrentView() {
|
||||
return this.routeService.getQueryParameterValue('view');
|
||||
}
|
||||
|
||||
getPaginatedSearchOptions(defaults: any = {}): Observable<PaginatedSearchOptions> {
|
||||
return Observable.combineLatest(
|
||||
this.getCurrentPagination(defaults.pagination),
|
||||
this.getCurrentSort(),
|
||||
this.getCurrentView(),
|
||||
this.getCurrentScope(),
|
||||
this.getCurrentQuery(),
|
||||
this.getCurrentFilters(),
|
||||
(pagination, sort, view, scope, query, filters) => {
|
||||
return Object.assign(new SearchOptions(),
|
||||
defaults,
|
||||
{
|
||||
pagination: pagination,
|
||||
sort: sort,
|
||||
view: view,
|
||||
scope: scope,
|
||||
query: query,
|
||||
filters: filters
|
||||
})
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
getSearchOptions(defaults: any = {}): Observable<SearchOptions> {
|
||||
return Observable.combineLatest(
|
||||
this.getCurrentView(),
|
||||
this.getCurrentScope(),
|
||||
this.getCurrentQuery(),
|
||||
this.getCurrentFilters(),
|
||||
(view, scope, query, filters) => {
|
||||
return Object.assign(new SearchOptions(),
|
||||
defaults,
|
||||
{
|
||||
view: view,
|
||||
scope: scope,
|
||||
query: query,
|
||||
filters: filters
|
||||
})
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
getSelectedValuesForFilter(filterConfig: SearchFilterConfig): Observable<string[]> {
|
||||
return this.routeService.getQueryParameterValues(filterConfig.paramName);
|
||||
}
|
||||
|
||||
get searchLink() {
|
||||
return this.searchService.uiSearchRoute;
|
||||
}
|
||||
|
||||
isCollapsed(filterName: string): Observable<boolean> {
|
||||
return this.store.select(filterByNameSelector(filterName))
|
||||
.map((object: SearchFilterState) => {
|
||||
|
@@ -1,5 +1,5 @@
|
||||
<h3>{{"search.filters.head" | translate}}</h3>
|
||||
<div *ngIf="(filters | async).hasSucceeded">
|
||||
<div *ngIf="(filters | async)?.hasSucceeded">
|
||||
<div *ngFor="let filter of (filters | async).payload">
|
||||
<ds-search-filter class="d-block mb-3 p-3" [filter]="filter"></ds-search-filter>
|
||||
</div>
|
||||
|
@@ -1,5 +1,5 @@
|
||||
import { SortOptions } from '../core/cache/models/sort-options.model';
|
||||
import { PaginationComponentOptions } from '../shared/pagination/pagination-component-options.model';
|
||||
import { isNotEmpty } from '../shared/empty.util';
|
||||
import { URLCombiner } from '../core/url-combiner/url-combiner';
|
||||
|
||||
export enum ViewMode {
|
||||
List = 'list',
|
||||
@@ -7,7 +7,28 @@ export enum ViewMode {
|
||||
}
|
||||
|
||||
export class SearchOptions {
|
||||
pagination?: PaginationComponentOptions;
|
||||
sort?: SortOptions;
|
||||
view?: ViewMode = ViewMode.List;
|
||||
scope?: string;
|
||||
query?: string;
|
||||
filters?: any;
|
||||
|
||||
toRestUrl(url: string, args: string[] = []): string {
|
||||
|
||||
if (isNotEmpty(this.query)) {
|
||||
args.push(`query=${this.query}`);
|
||||
}
|
||||
|
||||
if (isNotEmpty(this.scope)) {
|
||||
args.push(`scope=${this.scope}`);
|
||||
}
|
||||
if (isNotEmpty(this.filters)) {
|
||||
Object.entries(this.filters).forEach(([key, values]) => {
|
||||
values.forEach((value) => args.push(`${key}=${value},equals`));
|
||||
});
|
||||
}
|
||||
if (isNotEmpty(args)) {
|
||||
url = new URLCombiner(url, `?${args.join('&')}`).toString();
|
||||
}
|
||||
return url;
|
||||
}
|
||||
}
|
||||
|
@@ -15,6 +15,7 @@ import { SearchOptions, ViewMode } from './search-options.model';
|
||||
import { SearchResult } from './search-result.model';
|
||||
import { SearchService } from './search-service/search.service';
|
||||
import { SearchSidebarService } from './search-sidebar/search-sidebar.service';
|
||||
import { SearchFilterService } from './search-filters/search-filter/search-filter.service';
|
||||
|
||||
/**
|
||||
* This component renders a simple item page.
|
||||
@@ -36,85 +37,43 @@ export class SearchPageComponent implements OnInit, OnDestroy {
|
||||
|
||||
query: string;
|
||||
scopeObjectRDObs: Observable<RemoteData<DSpaceObject>>;
|
||||
resultsRDObs: Observable<RemoteData<Array<SearchResult<DSpaceObject>> | PaginatedList<SearchResult<DSpaceObject>>>>;
|
||||
resultsRDObs: Observable<RemoteData<PaginatedList<SearchResult<DSpaceObject>>>>;
|
||||
currentParams = {};
|
||||
searchOptions: SearchOptions;
|
||||
sortConfig: SortOptions;
|
||||
scopeListRDObs: Observable<RemoteData<PaginatedList<Community>>>;
|
||||
isMobileView: Observable<boolean>;
|
||||
pageSize;
|
||||
pageSizeOptions;
|
||||
defaults = {
|
||||
pagination: {
|
||||
id: 'search-results-pagination',
|
||||
pageSize: 10
|
||||
},
|
||||
query: ''
|
||||
};
|
||||
|
||||
constructor(private service: SearchService,
|
||||
private route: ActivatedRoute,
|
||||
private communityService: CommunityDataService,
|
||||
private sidebarService: SearchSidebarService,
|
||||
private windowService: HostWindowService) {
|
||||
private windowService: HostWindowService,
|
||||
private filterService: SearchFilterService) {
|
||||
this.isMobileView = Observable.combineLatest(
|
||||
this.windowService.isXs(),
|
||||
this.windowService.isSm(),
|
||||
((isXs, isSm) => isXs || isSm)
|
||||
);
|
||||
this.scopeListRDObs = communityService.findAll();
|
||||
// Initial pagination config
|
||||
const pagination: PaginationComponentOptions = new PaginationComponentOptions();
|
||||
pagination.id = 'search-results-pagination';
|
||||
pagination.currentPage = 1;
|
||||
pagination.pageSize = 10;
|
||||
|
||||
const sort: SortOptions = new SortOptions();
|
||||
this.sortConfig = sort;
|
||||
this.searchOptions = this.service.searchOptions;
|
||||
}
|
||||
|
||||
ngOnInit(): void {
|
||||
this.sub = this.route
|
||||
.queryParams
|
||||
.subscribe((params) => {
|
||||
// Save current parameters
|
||||
this.currentParams = params;
|
||||
this.query = params.query || '';
|
||||
this.scope = params.scope;
|
||||
const page = +params.page || this.searchOptions.pagination.currentPage;
|
||||
let pageSize = +params.pageSize || this.searchOptions.pagination.pageSize;
|
||||
let pageSizeOptions: number[] = [5, 10, 20, 40, 60, 80, 100];
|
||||
|
||||
if (isNotEmpty(params.view) && params.view === ViewMode.Grid) {
|
||||
pageSizeOptions = [12, 24, 36, 48 , 50, 62, 74, 84];
|
||||
if (pageSizeOptions.indexOf(pageSize) === -1) {
|
||||
pageSize = 12;
|
||||
}
|
||||
}
|
||||
if (isNotEmpty(params.view) && params.view === ViewMode.List) {
|
||||
if (pageSizeOptions.indexOf(pageSize) === -1) {
|
||||
pageSize = 10;
|
||||
}
|
||||
}
|
||||
|
||||
const sortDirection = params.sortDirection || this.searchOptions.sort.direction;
|
||||
const sortField = params.sortField || this.searchOptions.sort.field;
|
||||
const pagination = Object.assign({},
|
||||
this.searchOptions.pagination,
|
||||
{ currentPage: page, pageSize: pageSize, pageSizeOptions: pageSizeOptions}
|
||||
);
|
||||
const sort = Object.assign({},
|
||||
this.searchOptions.sort,
|
||||
{ direction: sortDirection, field: sortField }
|
||||
);
|
||||
|
||||
this.updateSearchResults({
|
||||
pagination: pagination,
|
||||
sort: sort
|
||||
this.sub = this.filterService.getPaginatedSearchOptions(this.defaults).subscribe((options) => {
|
||||
this.updateSearchResults(options);
|
||||
});
|
||||
if (isNotEmpty(this.scope)) {
|
||||
this.scopeObjectRDObs = this.communityService.findById(this.scope);
|
||||
} else {
|
||||
this.scopeObjectRDObs = Observable.of(undefined);
|
||||
}
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
private updateSearchResults(searchOptions) {
|
||||
this.resultsRDObs = this.service.search(this.query, this.scope, searchOptions);
|
||||
this.resultsRDObs = this.service.search(searchOptions);
|
||||
this.searchOptions = searchOptions;
|
||||
}
|
||||
|
||||
|
@@ -1,5 +1,6 @@
|
||||
export enum FilterType {
|
||||
text,
|
||||
range,
|
||||
hierarchy
|
||||
date,
|
||||
hierarchical,
|
||||
standard
|
||||
}
|
||||
|
@@ -1,11 +1,21 @@
|
||||
import { FilterType } from './filter-type.model';
|
||||
import { FilterType } from './filter-type.model';
|
||||
import { autoserialize, autoserializeAs } from 'cerialize';
|
||||
|
||||
export class SearchFilterConfig {
|
||||
export class SearchFilterConfig {
|
||||
|
||||
@autoserialize
|
||||
name: string;
|
||||
|
||||
@autoserializeAs(String, 'facetType')
|
||||
type: FilterType;
|
||||
|
||||
@autoserialize
|
||||
hasFacets: boolean;
|
||||
|
||||
// @autoserializeAs(String, 'facetLimit') - uncomment when fixed in rest
|
||||
pageSize = 5;
|
||||
|
||||
@autoserialize
|
||||
isOpenByDefault: boolean;
|
||||
/**
|
||||
* Name of this configuration that can be used in a url
|
||||
@@ -14,4 +24,4 @@ export class SearchFilterConfig {
|
||||
get paramName(): string {
|
||||
return 'f.' + this.name;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@@ -6,7 +6,8 @@ import { ViewMode } from '../../+search-page/search-options.model';
|
||||
import { RemoteDataBuildService } from '../../core/cache/builders/remote-data-build.service';
|
||||
import { SortOptions } from '../../core/cache/models/sort-options.model';
|
||||
import {
|
||||
FacetValueMapSuccessResponse, FacetValueSuccessResponse,
|
||||
FacetConfigSuccessResponse,
|
||||
FacetValueSuccessResponse,
|
||||
SearchSuccessResponse
|
||||
} from '../../core/cache/response-cache.models';
|
||||
import { ResponseCacheEntry } from '../../core/cache/response-cache.reducer';
|
||||
@@ -34,26 +35,17 @@ import { SearchQueryResponse } from './search-query-response.model';
|
||||
import { PageInfo } from '../../core/shared/page-info.model';
|
||||
import { getSearchResultFor } from './search-result-element-decorator';
|
||||
import { ListableObject } from '../../shared/object-collection/shared/listable-object.model';
|
||||
import { FacetResponseParsingService } from '../../core/data/facet-response-parsing.service';
|
||||
|
||||
function shuffle(array: any[]) {
|
||||
let i = 0;
|
||||
let j = 0;
|
||||
let temp = null;
|
||||
|
||||
for (i = array.length - 1; i > 0; i -= 1) {
|
||||
j = Math.floor(Math.random() * (i + 1));
|
||||
temp = array[i];
|
||||
array[i] = array[j];
|
||||
array[j] = temp;
|
||||
}
|
||||
return array;
|
||||
}
|
||||
import { FacetValueResponseParsingService } from '../../core/data/facet-value-response-parsing.service';
|
||||
import { FacetConfigResponseParsingService } from '../../core/data/facet-config-response-parsing.service';
|
||||
import { SearchFilterService } from '../search-filters/search-filter/search-filter.service';
|
||||
import { PaginatedSearchOptions } from '../paginated-search-options.model';
|
||||
|
||||
@Injectable()
|
||||
export class SearchService implements OnDestroy {
|
||||
private searchLinkPath = 'discover/search/objects';
|
||||
private facetLinkPath = 'discover/search/facets';
|
||||
private facetValueLinkPath = 'discover/search/facets';
|
||||
private facetValueLinkPathPrefix = 'discover/facets/';
|
||||
private facetConfigLinkPath = 'discover/facets';
|
||||
|
||||
private sub;
|
||||
uiSearchRoute = '/search';
|
||||
@@ -62,7 +54,7 @@ export class SearchService implements OnDestroy {
|
||||
// Object.assign(new SearchFilterConfig(),
|
||||
// {
|
||||
// name: 'scope',
|
||||
// type: FilterType.hierarchy,
|
||||
// type: FilterType.hierarchical,
|
||||
// hasFacets: true,
|
||||
// isOpenByDefault: true
|
||||
// }),
|
||||
@@ -76,7 +68,7 @@ export class SearchService implements OnDestroy {
|
||||
Object.assign(new SearchFilterConfig(),
|
||||
{
|
||||
name: 'dateIssued',
|
||||
type: FilterType.range,
|
||||
type: FilterType.date,
|
||||
hasFacets: true,
|
||||
isOpenByDefault: false
|
||||
}),
|
||||
@@ -95,7 +87,6 @@ export class SearchService implements OnDestroy {
|
||||
private route: ActivatedRoute,
|
||||
protected responseCache: ResponseCacheService,
|
||||
protected requestService: RequestService,
|
||||
private routeService: RouteService,
|
||||
private rdb: RemoteDataBuildService,
|
||||
private halService: HALEndpointService) {
|
||||
const pagination: PaginationComponentOptions = new PaginationComponentOptions();
|
||||
@@ -103,36 +94,15 @@ export class SearchService implements OnDestroy {
|
||||
pagination.currentPage = 1;
|
||||
pagination.pageSize = 10;
|
||||
const sort: SortOptions = new SortOptions();
|
||||
this.searchOptions = { pagination: pagination, sort: sort };
|
||||
// this.searchOptions = new BehaviorSubject<SearchOptions>(searchOptions);
|
||||
this.searchOptions = Object.assign(new SearchOptions(), { pagination: pagination, sort: sort });
|
||||
}
|
||||
|
||||
search(query: string, scopeId?: string, searchOptions?: SearchOptions): Observable<RemoteData<Array<SearchResult<DSpaceObject>> | PaginatedList<SearchResult<DSpaceObject>>>> {
|
||||
search(searchOptions?: PaginatedSearchOptions): Observable<RemoteData<PaginatedList<SearchResult<DSpaceObject>>>> {
|
||||
const requestObs = this.halService.getEndpoint(this.searchLinkPath).pipe(
|
||||
map((url: string) => {
|
||||
const args: string[] = [];
|
||||
|
||||
if (isNotEmpty(query)) {
|
||||
args.push(`query=${query}`);
|
||||
if (hasValue(searchOptions)) {
|
||||
url = searchOptions.toRestUrl(url);
|
||||
}
|
||||
|
||||
if (isNotEmpty(scopeId)) {
|
||||
args.push(`scope=${scopeId}`);
|
||||
}
|
||||
|
||||
if (isNotEmpty(searchOptions)) {
|
||||
if (isNotEmpty(searchOptions.sort)) {
|
||||
args.push(`sort=${searchOptions.sort.field},${searchOptions.sort.direction}`);
|
||||
}
|
||||
if (isNotEmpty(searchOptions.pagination)) {
|
||||
args.push(`page=${searchOptions.pagination.currentPage - 1}`);
|
||||
args.push(`size=${searchOptions.pagination.pageSize}`);
|
||||
}
|
||||
}
|
||||
if (isNotEmpty(args)) {
|
||||
url = new URLCombiner(url, `?${args.join('&')}`).toString();
|
||||
}
|
||||
|
||||
const request = new GetRequest(this.requestService.generateRequestId(), url);
|
||||
return Object.assign(request, {
|
||||
getResponseParser(): GenericConstructor<ResponseParsingService> {
|
||||
@@ -183,55 +153,25 @@ export class SearchService implements OnDestroy {
|
||||
});
|
||||
});
|
||||
|
||||
const pageInfoObs: Observable<PageInfo> = responseCacheObs
|
||||
.filter((entry: ResponseCacheEntry) => entry.response.isSuccessful)
|
||||
.map((entry: ResponseCacheEntry) => {
|
||||
if (hasValue((entry.response as SearchSuccessResponse).pageInfo)) {
|
||||
const resPageInfo = (entry.response as SearchSuccessResponse).pageInfo;
|
||||
if (isNotEmpty(resPageInfo) && resPageInfo.currentPage >= 0) {
|
||||
return Object.assign({}, resPageInfo, { currentPage: resPageInfo.currentPage + 1 });
|
||||
} else {
|
||||
return resPageInfo;
|
||||
}
|
||||
}
|
||||
});
|
||||
const pageInfoObs: Observable<PageInfo> = responseCacheObs.pipe(
|
||||
map((entry: ResponseCacheEntry) => entry.response),
|
||||
map((response: FacetValueSuccessResponse) => response.pageInfo)
|
||||
);
|
||||
|
||||
const payloadObs = Observable.combineLatest(tDomainListObs, pageInfoObs, (tDomainList, pageInfo) => {
|
||||
if (hasValue(pageInfo)) {
|
||||
return new PaginatedList(pageInfo, tDomainList);
|
||||
} else {
|
||||
return tDomainList;
|
||||
}
|
||||
});
|
||||
|
||||
return this.rdb.toRemoteDataObservable(requestEntryObs, responseCacheObs, payloadObs);
|
||||
}
|
||||
|
||||
getConfig(): Observable<RemoteData<SearchFilterConfig[]>> {
|
||||
const requestPending = false;
|
||||
const responsePending = false;
|
||||
const isSuccessful = true;
|
||||
const error = undefined;
|
||||
return Observable.of(new RemoteData(
|
||||
requestPending,
|
||||
responsePending,
|
||||
isSuccessful,
|
||||
error,
|
||||
this.config
|
||||
));
|
||||
}
|
||||
|
||||
getFacetValuesFor(searchFilterConfigName: string, query: string, scopeId: string): Observable<RemoteData<FacetValue[] | PaginatedList<FacetValue>>> {
|
||||
const requestObs = this.halService.getEndpoint(this.facetLinkPath).pipe(
|
||||
getConfig(scope?: string): Observable<RemoteData<SearchFilterConfig[]>> {
|
||||
const requestObs = this.halService.getEndpoint(this.facetConfigLinkPath).pipe(
|
||||
map((url: string) => {
|
||||
const args: string[] = [];
|
||||
|
||||
if (isNotEmpty(query)) {
|
||||
args.push(`query=${query}`);
|
||||
}
|
||||
|
||||
if (isNotEmpty(scopeId)) {
|
||||
args.push(`scope=${scopeId}`);
|
||||
if (isNotEmpty(scope)) {
|
||||
args.push(`scope=${scope}`);
|
||||
}
|
||||
|
||||
if (isNotEmpty(args)) {
|
||||
@@ -241,7 +181,7 @@ export class SearchService implements OnDestroy {
|
||||
const request = new GetRequest(this.requestService.generateRequestId(), url);
|
||||
return Object.assign(request, {
|
||||
getResponseParser(): GenericConstructor<ResponseParsingService> {
|
||||
return FacetResponseParsingService;
|
||||
return FacetConfigResponseParsingService;
|
||||
}
|
||||
});
|
||||
}),
|
||||
@@ -257,56 +197,56 @@ export class SearchService implements OnDestroy {
|
||||
);
|
||||
|
||||
// get search results from response cache
|
||||
const facetValueResponseObs: Observable<FacetValueSuccessResponse> = responseCacheObs.pipe(
|
||||
const facetConfigObs: Observable<SearchFilterConfig[]> = responseCacheObs.pipe(
|
||||
map((entry: ResponseCacheEntry) => entry.response),
|
||||
map((response: FacetValueMapSuccessResponse) => response.results[searchFilterConfigName])
|
||||
map((response: FacetConfigSuccessResponse) => response.results)
|
||||
);
|
||||
|
||||
return this.rdb.toRemoteDataObservable(requestEntryObs, responseCacheObs, facetConfigObs);
|
||||
}
|
||||
|
||||
getFacetValuesFor(filterConfig: SearchFilterConfig, valuePage: number, searchOptions?: SearchOptions): Observable<RemoteData<PaginatedList<FacetValue>>> {
|
||||
console.log('facetvalues');
|
||||
const requestObs = this.halService.getEndpoint(this.facetValueLinkPathPrefix + filterConfig.name).pipe(
|
||||
map((url: string) => {
|
||||
const args: string[] = [`page=${valuePage - 1}`, `size=${filterConfig.pageSize}`];
|
||||
if (hasValue(searchOptions)) {
|
||||
url = searchOptions.toRestUrl(url, args);
|
||||
}
|
||||
const request = new GetRequest(this.requestService.generateRequestId(), url);
|
||||
return Object.assign(request, {
|
||||
getResponseParser(): GenericConstructor<ResponseParsingService> {
|
||||
return FacetValueResponseParsingService;
|
||||
}
|
||||
});
|
||||
}),
|
||||
tap((request: RestRequest) => this.requestService.configure(request)),
|
||||
);
|
||||
|
||||
const requestEntryObs = requestObs.pipe(
|
||||
flatMap((request: RestRequest) => this.requestService.getByHref(request.href))
|
||||
);
|
||||
|
||||
const responseCacheObs = requestObs.pipe(
|
||||
flatMap((request: RestRequest) => this.responseCache.get(request.href))
|
||||
);
|
||||
|
||||
// get search results from response cache
|
||||
const facetValueObs: Observable<FacetValue[]> = facetValueResponseObs.pipe(
|
||||
const facetValueObs: Observable<FacetValue[]> = responseCacheObs.pipe(
|
||||
map((entry: ResponseCacheEntry) => entry.response),
|
||||
map((response: FacetValueSuccessResponse) => response.results)
|
||||
);
|
||||
|
||||
const pageInfoObs: Observable<PageInfo> = facetValueResponseObs.pipe(
|
||||
map((response: FacetValueSuccessResponse) => { console.log(response); return response.pageInfo})
|
||||
const pageInfoObs: Observable<PageInfo> = responseCacheObs.pipe(
|
||||
map((entry: ResponseCacheEntry) => entry.response),
|
||||
map((response: FacetValueSuccessResponse) => response.pageInfo)
|
||||
);
|
||||
const payloadObs = Observable.combineLatest(facetValueObs, pageInfoObs, (facetValue, pageInfo) => {
|
||||
if (hasValue(pageInfo)) {
|
||||
return new PaginatedList(pageInfo, facetValue);
|
||||
} else {
|
||||
return facetValue;
|
||||
}
|
||||
});
|
||||
return this.rdb.toRemoteDataObservable(requestEntryObs, responseCacheObs, payloadObs);
|
||||
|
||||
// const filterConfig = this.config.find((config: SearchFilterConfig) => config.name === searchFilterConfigName);
|
||||
// return this.routeService.getQueryParameterValues(filterConfig.paramName).map((selectedValues: string[]) => {
|
||||
// const payload: FacetValue[] = [];
|
||||
// const totalFilters = 13;
|
||||
// for (let i = 0; i < totalFilters; i++) {
|
||||
// const value = searchFilterConfigName + ' ' + (i + 1);
|
||||
// if (!selectedValues.includes(value)) {
|
||||
// payload.push({
|
||||
// value: value,
|
||||
// count: Math.floor(Math.random() * 20) + 20 * (totalFilters - 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 error = undefined;
|
||||
// return new RemoteData(
|
||||
// requestPending,
|
||||
// responsePending,
|
||||
// isSuccessful,
|
||||
// error,
|
||||
// payload
|
||||
// )
|
||||
// }
|
||||
// )
|
||||
const payloadObs = Observable.combineLatest(facetValueObs, pageInfoObs, (facetValue, pageInfo) => {
|
||||
return new PaginatedList(pageInfo, facetValue);
|
||||
});
|
||||
|
||||
return this.rdb.toRemoteDataObservable(requestEntryObs, responseCacheObs, payloadObs);
|
||||
}
|
||||
|
||||
getViewMode(): Observable<ViewMode> {
|
||||
|
@@ -3,6 +3,7 @@ import { SearchService } from '../search-service/search.service';
|
||||
import { SearchOptions, ViewMode } from '../search-options.model';
|
||||
import { SortDirection } from '../../core/cache/models/sort-options.model';
|
||||
import { ActivatedRoute, NavigationExtras, Router } from '@angular/router';
|
||||
import { PaginatedSearchOptions } from '../paginated-search-options.model';
|
||||
|
||||
@Component({
|
||||
selector: 'ds-search-settings',
|
||||
@@ -11,7 +12,7 @@ import { ActivatedRoute, NavigationExtras, Router } from '@angular/router';
|
||||
})
|
||||
export class SearchSettingsComponent implements OnInit {
|
||||
|
||||
@Input() searchOptions: SearchOptions;
|
||||
@Input() searchOptions: PaginatedSearchOptions;
|
||||
/**
|
||||
* Declare SortDirection enumeration to use it in the template
|
||||
*/
|
||||
|
10
src/app/core/cache/response-cache.models.ts
vendored
10
src/app/core/cache/response-cache.models.ts
vendored
@@ -4,6 +4,7 @@ import { PageInfo } from '../shared/page-info.model';
|
||||
import { BrowseDefinition } from '../shared/browse-definition.model';
|
||||
import { ConfigObject } from '../shared/config/config.model';
|
||||
import { FacetValue } from '../../+search-page/search-service/facet-value.model';
|
||||
import { SearchFilterConfig } from '../../+search-page/search-service/search-filter-config.model';
|
||||
|
||||
/* tslint:disable:max-classes-per-file */
|
||||
export class RestResponse {
|
||||
@@ -33,6 +34,15 @@ export class SearchSuccessResponse extends RestResponse {
|
||||
}
|
||||
}
|
||||
|
||||
export class FacetConfigSuccessResponse extends RestResponse {
|
||||
constructor(
|
||||
public results: SearchFilterConfig[],
|
||||
public statusCode: string
|
||||
) {
|
||||
super(true, statusCode);
|
||||
}
|
||||
}
|
||||
|
||||
export class FacetValueMap {
|
||||
[name: string]: FacetValueSuccessResponse
|
||||
}
|
||||
|
@@ -8,5 +8,5 @@ export const coreEffects = [
|
||||
ResponseCacheEffects,
|
||||
RequestEffects,
|
||||
ObjectCacheEffects,
|
||||
UUIDIndexEffects,
|
||||
UUIDIndexEffects
|
||||
];
|
||||
|
@@ -41,7 +41,9 @@ import { SubmissionFormsConfigService } from './config/submission-forms-config.s
|
||||
import { SubmissionSectionsConfigService } from './config/submission-sections-config.service';
|
||||
import { UUIDService } from './shared/uuid.service';
|
||||
import { HALEndpointService } from './shared/hal-endpoint.service';
|
||||
import { FacetResponseParsingService } from './data/facet-response-parsing.service';
|
||||
import { FacetValueResponseParsingService } from './data/facet-value-response-parsing.service';
|
||||
import { FacetValueMapResponseParsingService } from './data/facet-value-map-response-parsing.service';
|
||||
import { FacetConfigResponseParsingService } from './data/facet-config-response-parsing.service';
|
||||
|
||||
const IMPORTS = [
|
||||
CommonModule,
|
||||
@@ -73,7 +75,9 @@ const PROVIDERS = [
|
||||
RequestService,
|
||||
ResponseCacheService,
|
||||
EndpointMapResponseParsingService,
|
||||
FacetResponseParsingService,
|
||||
FacetValueResponseParsingService,
|
||||
FacetValueMapResponseParsingService,
|
||||
FacetConfigResponseParsingService,
|
||||
DebugResponseParsingService,
|
||||
SearchResponseParsingService,
|
||||
ServerResponseService,
|
||||
|
@@ -117,9 +117,13 @@ export abstract class BaseResponseParsingService {
|
||||
this.objectCache.add(co, this.EnvConfig.cache.msToLive, requestHref);
|
||||
}
|
||||
|
||||
protected processPageInfo(pageObj: any): PageInfo {
|
||||
processPageInfo(pageObj: any): PageInfo {
|
||||
if (isNotEmpty(pageObj)) {
|
||||
return new DSpaceRESTv2Serializer(PageInfo).deserialize(pageObj);
|
||||
const pageInfoObject = new DSpaceRESTv2Serializer(PageInfo).deserialize(pageObj);
|
||||
if (pageInfoObject.currentPage >= 0) {
|
||||
Object.assign(pageInfoObject, { currentPage: pageInfoObject.currentPage + 1 });
|
||||
}
|
||||
return pageInfoObject
|
||||
} else {
|
||||
return undefined;
|
||||
}
|
||||
|
32
src/app/core/data/facet-config-response-parsing.service.ts
Normal file
32
src/app/core/data/facet-config-response-parsing.service.ts
Normal file
@@ -0,0 +1,32 @@
|
||||
import { Inject, Injectable } from '@angular/core';
|
||||
import {
|
||||
FacetConfigSuccessResponse,
|
||||
RestResponse
|
||||
} from '../cache/response-cache.models';
|
||||
import { ResponseParsingService } from './parsing.service';
|
||||
import { RestRequest } from './request.models';
|
||||
import { DSpaceRESTV2Response } from '../dspace-rest-v2/dspace-rest-v2-response.model';
|
||||
import { DSpaceRESTv2Serializer } from '../dspace-rest-v2/dspace-rest-v2.serializer';
|
||||
import { SearchFilterConfig } from '../../+search-page/search-service/search-filter-config.model';
|
||||
import { BaseResponseParsingService } from './base-response-parsing.service';
|
||||
import { ObjectCacheService } from '../cache/object-cache.service';
|
||||
import { GlobalConfig } from '../../../config/global-config.interface';
|
||||
import { GLOBAL_CONFIG } from '../../../config';
|
||||
|
||||
@Injectable()
|
||||
export class FacetConfigResponseParsingService extends BaseResponseParsingService implements ResponseParsingService {
|
||||
objectFactory = {};
|
||||
toCache = false;
|
||||
constructor(
|
||||
@Inject(GLOBAL_CONFIG) protected EnvConfig: GlobalConfig,
|
||||
protected objectCache: ObjectCacheService,
|
||||
) { super();
|
||||
}
|
||||
parse(request: RestRequest, data: DSpaceRESTV2Response): RestResponse {
|
||||
|
||||
const config = data.payload._embedded.facets;
|
||||
const serializer = new DSpaceRESTv2Serializer(SearchFilterConfig);
|
||||
const facetConfig = serializer.deserializeArray(config);
|
||||
return new FacetConfigSuccessResponse(facetConfig, data.statusCode);
|
||||
}
|
||||
}
|
@@ -1,4 +1,4 @@
|
||||
import { Injectable } from '@angular/core';
|
||||
import { Inject, Injectable } from '@angular/core';
|
||||
import {
|
||||
FacetValueMap,
|
||||
FacetValueMapSuccessResponse,
|
||||
@@ -12,9 +12,22 @@ import { DSpaceRESTv2Serializer } from '../dspace-rest-v2/dspace-rest-v2.seriali
|
||||
import { PageInfo } from '../shared/page-info.model';
|
||||
import { isNotEmpty } from '../../shared/empty.util';
|
||||
import { FacetValue } from '../../+search-page/search-service/facet-value.model';
|
||||
import { BaseResponseParsingService } from './base-response-parsing.service';
|
||||
import { ObjectCacheService } from '../cache/object-cache.service';
|
||||
import { GlobalConfig } from '../../../config/global-config.interface';
|
||||
import { GLOBAL_CONFIG } from '../../../config';
|
||||
|
||||
@Injectable()
|
||||
export class FacetResponseParsingService implements ResponseParsingService {
|
||||
export class FacetValueMapResponseParsingService extends BaseResponseParsingService implements ResponseParsingService {
|
||||
objectFactory = {};
|
||||
toCache = false;
|
||||
|
||||
constructor(
|
||||
@Inject(GLOBAL_CONFIG) protected EnvConfig: GlobalConfig,
|
||||
protected objectCache: ObjectCacheService,
|
||||
) { super();
|
||||
}
|
||||
|
||||
parse(request: RestRequest, data: DSpaceRESTV2Response): RestResponse {
|
||||
|
||||
const payload = data.payload;
|
||||
@@ -30,12 +43,4 @@ export class FacetResponseParsingService implements ResponseParsingService {
|
||||
|
||||
return new FacetValueMapSuccessResponse(facetMap, data.statusCode);
|
||||
}
|
||||
|
||||
protected processPageInfo(pageObj: any): PageInfo {
|
||||
if (isNotEmpty(pageObj)) {
|
||||
return new DSpaceRESTv2Serializer(PageInfo).deserialize(pageObj);
|
||||
} else {
|
||||
return undefined;
|
||||
}
|
||||
}
|
||||
}
|
38
src/app/core/data/facet-value-response-parsing.service.ts
Normal file
38
src/app/core/data/facet-value-response-parsing.service.ts
Normal file
@@ -0,0 +1,38 @@
|
||||
import { Inject, Injectable } from '@angular/core';
|
||||
import {
|
||||
FacetValueMap,
|
||||
FacetValueMapSuccessResponse,
|
||||
FacetValueSuccessResponse,
|
||||
RestResponse
|
||||
} from '../cache/response-cache.models';
|
||||
import { ResponseParsingService } from './parsing.service';
|
||||
import { RestRequest } from './request.models';
|
||||
import { DSpaceRESTV2Response } from '../dspace-rest-v2/dspace-rest-v2-response.model';
|
||||
import { DSpaceRESTv2Serializer } from '../dspace-rest-v2/dspace-rest-v2.serializer';
|
||||
import { PageInfo } from '../shared/page-info.model';
|
||||
import { isNotEmpty } from '../../shared/empty.util';
|
||||
import { FacetValue } from '../../+search-page/search-service/facet-value.model';
|
||||
import { BaseResponseParsingService } from './base-response-parsing.service';
|
||||
import { ObjectCacheService } from '../cache/object-cache.service';
|
||||
import { GLOBAL_CONFIG } from '../../../config';
|
||||
import { GlobalConfig } from '../../../config/global-config.interface';
|
||||
|
||||
@Injectable()
|
||||
export class FacetValueResponseParsingService extends BaseResponseParsingService implements ResponseParsingService {
|
||||
objectFactory = {};
|
||||
toCache = false;
|
||||
constructor(
|
||||
@Inject(GLOBAL_CONFIG) protected EnvConfig: GlobalConfig,
|
||||
protected objectCache: ObjectCacheService,
|
||||
) { super();
|
||||
}
|
||||
parse(request: RestRequest, data: DSpaceRESTV2Response): RestResponse {
|
||||
const payload = data.payload;
|
||||
|
||||
const serializer = new DSpaceRESTv2Serializer(FacetValue);
|
||||
const values = payload._embedded.values.map((value) => {value.search = value._links.search.href; return value;});
|
||||
|
||||
const facetValues = serializer.deserializeArray(values);
|
||||
return new FacetValueSuccessResponse(facetValues, data.statusCode, this.processPageInfo(data.payload.page));
|
||||
}
|
||||
}
|
@@ -1,40 +1,52 @@
|
||||
import { PageInfo } from '../shared/page-info.model';
|
||||
import { hasValue } from '../../shared/empty.util';
|
||||
|
||||
export class PaginatedList<T> {
|
||||
|
||||
constructor(
|
||||
private pageInfo: PageInfo,
|
||||
public page: T[]
|
||||
) {
|
||||
constructor(private pageInfo: PageInfo,
|
||||
public page: T[]) {
|
||||
}
|
||||
|
||||
get elementsPerPage(): number {
|
||||
if (hasValue(this.pageInfo)) {
|
||||
return this.pageInfo.elementsPerPage;
|
||||
}
|
||||
return this.page.length;
|
||||
}
|
||||
|
||||
set elementsPerPage(value: number) {
|
||||
this.pageInfo.elementsPerPage = value;
|
||||
}
|
||||
|
||||
get totalElements(): number {
|
||||
if (hasValue(this.pageInfo)) {
|
||||
return this.pageInfo.totalElements;
|
||||
}
|
||||
return this.page.length;
|
||||
}
|
||||
|
||||
set totalElements(value: number) {
|
||||
this.pageInfo.totalElements = value;
|
||||
}
|
||||
|
||||
get totalPages(): number {
|
||||
if (hasValue(this.pageInfo)) {
|
||||
return this.pageInfo.totalPages;
|
||||
}
|
||||
return 1;
|
||||
}
|
||||
|
||||
set totalPages(value: number) {
|
||||
this.pageInfo.totalPages = value;
|
||||
}
|
||||
|
||||
get currentPage(): number {
|
||||
if (hasValue(this.pageInfo)) {
|
||||
return this.pageInfo.currentPage;
|
||||
}
|
||||
return 1;
|
||||
|
||||
}
|
||||
|
||||
set currentPage(value: number) {
|
||||
this.pageInfo.currentPage = value;
|
||||
|
@@ -56,14 +56,6 @@ export class SearchResponseParsingService implements ResponseParsingService {
|
||||
}));
|
||||
payload.objects = objects;
|
||||
const deserialized = new DSpaceRESTv2Serializer(SearchQueryResponse).deserialize(payload);
|
||||
return new SearchSuccessResponse(deserialized, data.statusCode, this.processPageInfo(data.payload.page));
|
||||
}
|
||||
|
||||
protected processPageInfo(pageObj: any): PageInfo {
|
||||
if (isNotEmpty(pageObj)) {
|
||||
return new DSpaceRESTv2Serializer(PageInfo).deserialize(pageObj);
|
||||
} else {
|
||||
return undefined;
|
||||
}
|
||||
return new SearchSuccessResponse(deserialized, data.statusCode, this.dsoParser.processPageInfo(data.payload.page));
|
||||
}
|
||||
}
|
||||
|
@@ -1,12 +1,12 @@
|
||||
import { Observable } from 'rxjs/Observable';
|
||||
import { distinctUntilChanged, map, flatMap, startWith } from 'rxjs/operators';
|
||||
import { distinctUntilChanged, map, flatMap, startWith, tap } from 'rxjs/operators';
|
||||
import { RequestService } from '../data/request.service';
|
||||
import { ResponseCacheService } from '../cache/response-cache.service';
|
||||
import { GlobalConfig } from '../../../config/global-config.interface';
|
||||
import { EndpointMap, EndpointMapSuccessResponse } from '../cache/response-cache.models';
|
||||
import { EndpointMapRequest } from '../data/request.models';
|
||||
import { ResponseCacheEntry } from '../cache/response-cache.reducer';
|
||||
import { isEmpty, isNotEmpty } from '../../shared/empty.util';
|
||||
import { hasNoValue, hasValue, isEmpty, isNotEmpty } from '../../shared/empty.util';
|
||||
import { RESTURLCombiner } from '../url-combiner/rest-url-combiner';
|
||||
import { Inject, Injectable } from '@angular/core';
|
||||
import { GLOBAL_CONFIG } from '../../../config';
|
||||
@@ -21,6 +21,7 @@ export class HALEndpointService {
|
||||
@Inject(GLOBAL_CONFIG) private EnvConfig: GlobalConfig) {
|
||||
|
||||
}
|
||||
|
||||
protected getRootHref(): string {
|
||||
return new RESTURLCombiner(this.EnvConfig, '/').toString();
|
||||
}
|
||||
@@ -34,23 +35,35 @@ export class HALEndpointService {
|
||||
this.requestService.configure(request);
|
||||
return this.responseCache.get(request.href)
|
||||
.map((entry: ResponseCacheEntry) => entry.response)
|
||||
.filter((response: EndpointMapSuccessResponse) => isNotEmpty(response) && isNotEmpty(response.endpointMap))
|
||||
.filter((response: EndpointMapSuccessResponse) => isNotEmpty(response))
|
||||
.map((response: EndpointMapSuccessResponse) => response.endpointMap)
|
||||
.distinctUntilChanged();
|
||||
}
|
||||
|
||||
public getEndpoint(linkPath: string): Observable<string> {
|
||||
return this.getEndpointAt(...linkPath.split('/'));
|
||||
const test = this.getEndpointAt(...linkPath.split('/'));
|
||||
// test.subscribe((test) => console.log(linkPath, test));
|
||||
return test;
|
||||
}
|
||||
|
||||
private getEndpointAt(...path: string[]): Observable<string> {
|
||||
if (isEmpty(path)) {
|
||||
path = ['/'];
|
||||
}
|
||||
let currentPath;
|
||||
const pipeArguments = path
|
||||
.map((subPath: string) => [
|
||||
.map((subPath: string, index: number) => [
|
||||
flatMap((href: string) => this.getEndpointMapAt(href)),
|
||||
map((endpointMap: EndpointMap) => endpointMap[subPath]),
|
||||
map((endpointMap: EndpointMap) => {
|
||||
if (hasValue(endpointMap) && hasValue(endpointMap[subPath])) {
|
||||
currentPath = endpointMap[subPath];
|
||||
return endpointMap[subPath];
|
||||
} else {
|
||||
/*TODO remove if/else block once the rest response contains _links for facets*/
|
||||
currentPath += '/' + subPath;
|
||||
return currentPath;
|
||||
}
|
||||
}),
|
||||
])
|
||||
.reduce((combined, thisElement) => [...combined, ...thisElement], []);
|
||||
return Observable.of(this.getRootHref()).pipe(...pipeArguments, distinctUntilChanged());
|
||||
|
@@ -69,12 +69,12 @@ describe('RouteService', () => {
|
||||
|
||||
describe('addQueryParameterValue', () => {
|
||||
it('should return a list of values that contains the added value when a new value is added and the parameter did not exist yet', () => {
|
||||
service.addQueryParameterValue(nonExistingParamName, nonExistingParamValue).subscribe((params) => {
|
||||
service.resolveRouteWithParameterValue(nonExistingParamName, nonExistingParamValue).subscribe((params) => {
|
||||
expect(params[nonExistingParamName]).toContain(nonExistingParamValue);
|
||||
});
|
||||
});
|
||||
it('should return a list of values that contains the existing values and the added value when a new value is added and the parameter already has values', () => {
|
||||
service.addQueryParameterValue(paramName1, nonExistingParamValue).subscribe((params) => {
|
||||
service.resolveRouteWithParameterValue(paramName1, nonExistingParamValue).subscribe((params) => {
|
||||
const values = params[paramName1];
|
||||
expect(values).toContain(paramValue1);
|
||||
expect(values).toContain(nonExistingParamValue);
|
||||
@@ -84,7 +84,7 @@ describe('RouteService', () => {
|
||||
|
||||
describe('removeQueryParameterValue', () => {
|
||||
it('should return a list of values that does not contain the removed value when the parameter value exists', () => {
|
||||
service.removeQueryParameterValue(paramName2, paramValue2a).subscribe((params) => {
|
||||
service.resolveRouteWithoutParameterValue(paramName2, paramValue2a).subscribe((params) => {
|
||||
const values = params[paramName2];
|
||||
expect(values).toContain(paramValue2b);
|
||||
expect(values).not.toContain(paramValue2a);
|
||||
@@ -92,7 +92,7 @@ describe('RouteService', () => {
|
||||
});
|
||||
|
||||
it('should return a list of values that does contain all existing values when the removed parameter does not exist', () => {
|
||||
service.removeQueryParameterValue(paramName2, nonExistingParamValue).subscribe((params) => {
|
||||
service.resolveRouteWithoutParameterValue(paramName2, nonExistingParamValue).subscribe((params) => {
|
||||
const values = params[paramName2];
|
||||
expect(values).toContain(paramValue2a);
|
||||
expect(values).toContain(paramValue2b);
|
||||
@@ -100,15 +100,15 @@ describe('RouteService', () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe('removeQueryParameter', () => {
|
||||
describe('getWithoutParameter', () => {
|
||||
it('should return a list of values that does not contain any values for the parameter anymore when the parameter exists', () => {
|
||||
service.removeQueryParameter(paramName2).subscribe((params) => {
|
||||
service.resolveRouteWithoutParameter(paramName2).subscribe((params) => {
|
||||
const values = params[paramName2];
|
||||
expect(values).toEqual({});
|
||||
});
|
||||
});
|
||||
it('should return a list of values that does not contain any values for the parameter when the parameter does not exist', () => {
|
||||
service.removeQueryParameter(nonExistingParamName).subscribe((params) => {
|
||||
service.resolveRouteWithoutParameter(nonExistingParamName).subscribe((params) => {
|
||||
const values = params[nonExistingParamName];
|
||||
expect(values).toEqual({});
|
||||
});
|
||||
|
@@ -1,6 +1,9 @@
|
||||
import { Injectable } from '@angular/core';
|
||||
import { Observable } from 'rxjs/Observable';
|
||||
import { ActivatedRoute, convertToParamMap, Params, } from '@angular/router';
|
||||
import {
|
||||
ActivatedRoute, convertToParamMap, NavigationExtras, Params,
|
||||
Router,
|
||||
} from '@angular/router';
|
||||
import { isNotEmpty } from './empty.util';
|
||||
|
||||
@Injectable()
|
||||
@@ -10,7 +13,7 @@ export class RouteService {
|
||||
}
|
||||
|
||||
getQueryParameterValues(paramName: string): Observable<string[]> {
|
||||
return this.route.queryParamMap.map((map) => map.getAll(paramName));
|
||||
return this.route.queryParamMap.map((map) => [...map.getAll(paramName)]);
|
||||
}
|
||||
|
||||
getQueryParameterValue(paramName: string): Observable<string> {
|
||||
@@ -25,31 +28,16 @@ export class RouteService {
|
||||
return this.route.queryParamMap.map((map) => map.getAll(paramName).indexOf(paramValue) > -1);
|
||||
}
|
||||
|
||||
addQueryParameterValue(paramName: string, paramValue: string): Observable<Params> {
|
||||
return this.route.queryParams.map((currentParams) => {
|
||||
const newParam = {};
|
||||
newParam[paramName] = [...convertToParamMap(currentParams).getAll(paramName), paramValue];
|
||||
return Object.assign({}, currentParams, newParam);
|
||||
getQueryParamsWithPrefix(prefix: string): Observable<Params> {
|
||||
return this.route.queryParamMap
|
||||
.map((map) => {
|
||||
const params = {};
|
||||
map.keys
|
||||
.filter((key) => key.startsWith(prefix))
|
||||
.forEach((key) => {
|
||||
params[key] = [...map.getAll(key)];
|
||||
});
|
||||
}
|
||||
|
||||
removeQueryParameterValue(paramName: string, paramValue: string): Observable<Params> {
|
||||
return this.route.queryParams.map((currentParams) => {
|
||||
const newParam = {};
|
||||
const currentFilterParams = convertToParamMap(currentParams).getAll(paramName);
|
||||
if (isNotEmpty(currentFilterParams)) {
|
||||
newParam[paramName] = currentFilterParams.filter((param) => (param !== paramValue));
|
||||
}
|
||||
return Object.assign({}, currentParams, newParam);
|
||||
return params;
|
||||
});
|
||||
}
|
||||
|
||||
removeQueryParameter(paramName: string): Observable<Params> {
|
||||
return this.route.queryParams.map((currentParams) => {
|
||||
const newParam = {};
|
||||
newParam[paramName] = {};
|
||||
return Object.assign({}, currentParams, newParam);
|
||||
});
|
||||
|
||||
}
|
||||
}
|
||||
|
@@ -1,4 +1,4 @@
|
||||
{
|
||||
yarn add{
|
||||
"extends": "../tsconfig.json",
|
||||
"compilerOptions": {
|
||||
"sourceMap": true
|
||||
|
10
yarn.lock
10
yarn.lock
@@ -122,6 +122,12 @@
|
||||
version "2.0.1"
|
||||
resolved "https://registry.yarnpkg.com/@ngx-translate/http-loader/-/http-loader-2.0.1.tgz#aa67788e64bfa8652691a77b022b3b4031209113"
|
||||
|
||||
"@types/acorn@^4.0.3":
|
||||
version "4.0.3"
|
||||
resolved "https://registry.yarnpkg.com/@types/acorn/-/acorn-4.0.3.tgz#d1f3e738dde52536f9aad3d3380d14e448820afd"
|
||||
dependencies:
|
||||
"@types/estree" "*"
|
||||
|
||||
"@types/body-parser@*":
|
||||
version "1.16.8"
|
||||
resolved "https://registry.yarnpkg.com/@types/body-parser/-/body-parser-1.16.8.tgz#687ec34140624a3bec2b1a8ea9268478ae8f3be3"
|
||||
@@ -139,6 +145,10 @@
|
||||
version "0.1.1"
|
||||
resolved "https://registry.yarnpkg.com/@types/deep-freeze/-/deep-freeze-0.1.1.tgz#0e1ee6ceee06f51baeb663deec0bb7780bd72827"
|
||||
|
||||
"@types/estree@*":
|
||||
version "0.0.38"
|
||||
resolved "https://registry.yarnpkg.com/@types/estree/-/estree-0.0.38.tgz#c1be40aa933723c608820a99a373a16d215a1ca2"
|
||||
|
||||
"@types/events@*":
|
||||
version "1.1.0"
|
||||
resolved "https://registry.yarnpkg.com/@types/events/-/events-1.1.0.tgz#93b1be91f63c184450385272c47b6496fd028e02"
|
||||
|
Reference in New Issue
Block a user