fixed collection page, added SearchFilter Model

This commit is contained in:
lotte
2018-08-23 14:01:37 +02:00
parent f5fed28722
commit 976302389f
13 changed files with 111 additions and 38 deletions

View File

@@ -5,7 +5,6 @@ import { Observable } from 'rxjs/Observable';
import { Subscription } from 'rxjs/Subscription';
import { SortDirection, SortOptions } from '../core/cache/models/sort-options.model';
import { CollectionDataService } from '../core/data/collection-data.service';
import { ItemDataService } from '../core/data/item-data.service';
import { PaginatedList } from '../core/data/paginated-list';
import { RemoteData } from '../core/data/remote-data';
@@ -21,8 +20,8 @@ import { PaginationComponentOptions } from '../shared/pagination/pagination-comp
import { filter, flatMap, map } from 'rxjs/operators';
import { SearchService } from '../+search-page/search-service/search.service';
import { PaginatedSearchOptions } from '../+search-page/paginated-search-options.model';
import { SearchResult } from '../+search-page/search-result.model';
import { toDSpaceObjectListRD } from '../core/shared/operators';
import { DSpaceObjectType } from '../core/shared/dspace-object-type.model';
@Component({
selector: 'ds-collection-page',
@@ -68,18 +67,13 @@ export class CollectionPageComponent implements OnInit, OnDestroy {
this.metadata.processRemoteData(this.collectionRD$);
const page = +params.page || this.paginationConfig.currentPage;
const pageSize = +params.pageSize || this.paginationConfig.pageSize;
const sortDirection = +params.page || this.sortConfig.direction;
const pagination = Object.assign({},
this.paginationConfig,
{ currentPage: page, pageSize: pageSize }
);
const sort = Object.assign({},
this.sortConfig,
{ direction: sortDirection, field: params.sortField }
);
this.updatePage({
pagination: pagination,
sort: sort
sort: this.sortConfig
});
}));
@@ -91,7 +85,7 @@ export class CollectionPageComponent implements OnInit, OnDestroy {
scope: this.collectionId,
pagination: searchOptions.pagination,
sort: searchOptions.sort,
filters: {type: 2}
dsoType: DSpaceObjectType.ITEM
})).pipe(toDSpaceObjectListRD()) as Observable<RemoteData<PaginatedList<Item>>>;
}

View File

@@ -2,8 +2,8 @@
<div class="item-page" *ngIf="itemRD?.hasSucceeded" @fadeInOut>
<div *ngIf="itemRD?.payload as item">
<ds-item-page-title-field [item]="item"></ds-item-page-title-field>
<div class="simple-view-link">
<a class="btn btn-outline-primary col-4" [routerLink]="['/items/' + item.id]">
<div class="simple-view-link my-3">
<a class="btn btn-outline-primary" [routerLink]="['/items/' + item.id]">
{{"item.page.link.simple" | translate}}
</a>
</div>

View File

@@ -1,7 +1,10 @@
@import '../../../styles/variables.scss';
:host {
div.simple-view-link {
text-align: center;
margin: 20px;
a {
min-width: 25%;
}
}
}

View File

@@ -2,17 +2,19 @@ import 'rxjs/add/observable/of';
import { SortDirection, SortOptions } from '../core/cache/models/sort-options.model';
import { PaginationComponentOptions } from '../shared/pagination/pagination-component-options.model';
import { PaginatedSearchOptions } from './paginated-search-options.model';
import { DSpaceObjectType } from '../core/shared/dspace-object-type.model';
import { SearchFilter } from './search-filter.model';
describe('PaginatedSearchOptions', () => {
let options: PaginatedSearchOptions;
const sortOptions = new SortOptions('test.field', SortDirection.DESC);
const pageOptions = Object.assign(new PaginationComponentOptions(), { pageSize: 40, page: 1 });
const filters = { 'f.test': ['value'], 'f.example': ['another value', 'second value'] };
const filters = [new SearchFilter('f.test', ['value']), new SearchFilter('f.example', ['another value', 'second value'])];
const query = 'search query';
const scope = '0fde1ecb-82cc-425a-b600-ac3576d76b47';
const baseUrl = 'www.rest.com';
beforeEach(() => {
options = new PaginatedSearchOptions({sort: sortOptions, pagination: pageOptions, filters: filters, query: query, scope: scope});
options = new PaginatedSearchOptions({sort: sortOptions, pagination: pageOptions, filters: filters, query: query, scope: scope, dsoType: DSpaceObjectType.ITEM});
});
describe('when toRestUrl is called', () => {
@@ -25,6 +27,7 @@ describe('PaginatedSearchOptions', () => {
'size=40&' +
'query=search query&' +
'scope=0fde1ecb-82cc-425a-b600-ac3576d76b47&' +
'dsoType=ITEM&' +
'f.test=value,query&' +
'f.example=another value,query&' +
'f.example=second value,query'

View File

@@ -2,6 +2,8 @@ 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 { SearchOptions } from './search-options.model';
import { SearchFilter } from './search-filter.model';
import { DSpaceObjectType } from '../core/shared/dspace-object-type.model';
/**
* This model class represents all parameters needed to request information about a certain page of a search request, in a certain order
@@ -10,8 +12,8 @@ export class PaginatedSearchOptions extends SearchOptions {
pagination?: PaginationComponentOptions;
sort?: SortOptions;
constructor(options: {scope?: string, query?: string, filters?: any, pagination?: PaginationComponentOptions, sort?: SortOptions}) {
super(options)
constructor(options: {scope?: string, query?: string, dsoType?: DSpaceObjectType, filters?: SearchFilter[], pagination?: PaginationComponentOptions, sort?: SortOptions}) {
super(options);
this.pagination = options.pagination;
this.sort = options.sort;
}

View File

@@ -0,0 +1,20 @@
/**
* Represents a search filter
*/
import { hasValue } from '../shared/empty.util';
export class SearchFilter {
key: string;
values: string[];
operator: string;
constructor(key: string, values: string[], operator?: string) {
this.key = key;
this.values = values;
if (hasValue(operator)) {
this.operator = operator;
} else {
this.operator = 'query';
}
}
}

View File

@@ -1,15 +1,17 @@
import 'rxjs/add/observable/of';
import { PaginatedSearchOptions } from './paginated-search-options.model';
import { SearchOptions } from './search-options.model';
import { SearchFilter } from './search-filter.model';
import { DSpaceObjectType } from '../core/shared/dspace-object-type.model';
describe('SearchOptions', () => {
let options: PaginatedSearchOptions;
const filters = { 'f.test': ['value'], 'f.example': ['another value', 'second value'] };
const filters = [new SearchFilter('f.test', ['value']), new SearchFilter('f.example', ['another value', 'second value'])];
const query = 'search query';
const scope = '0fde1ecb-82cc-425a-b600-ac3576d76b47';
const baseUrl = 'www.rest.com';
beforeEach(() => {
options = new SearchOptions({filters: filters, query: query, scope: scope});
options = new SearchOptions({ filters: filters, query: query, scope: scope , dsoType: DSpaceObjectType.ITEM});
});
describe('when toRestUrl is called', () => {
@@ -19,6 +21,7 @@ describe('SearchOptions', () => {
expect(outcome).toEqual('www.rest.com?' +
'query=search query&' +
'scope=0fde1ecb-82cc-425a-b600-ac3576d76b47&' +
'dsoType=ITEM&' +
'f.test=value,query&' +
'f.example=another value,query&' +
'f.example=second value,query'

View File

@@ -1,6 +1,8 @@
import { isNotEmpty } from '../shared/empty.util';
import { URLCombiner } from '../core/url-combiner/url-combiner';
import 'core-js/library/fn/object/entries';
import { SearchFilter } from './search-filter.model';
import { DSpaceObjectType } from '../core/shared/dspace-object-type.model';
/**
* This model class represents all parameters needed to request information about a certain search request
@@ -8,11 +10,13 @@ import 'core-js/library/fn/object/entries';
export class SearchOptions {
scope?: string;
query?: string;
filters?: any;
dsoType?: DSpaceObjectType;
filters?: SearchFilter[];
constructor(options: {scope?: string, query?: string, filters?: any}) {
constructor(options: {scope?: string, query?: string, dsoType?: DSpaceObjectType, filters?: SearchFilter[]}) {
this.scope = options.scope;
this.query = options.query;
this.dsoType = options.dsoType;
this.filters = options.filters;
}
@@ -27,13 +31,15 @@ export class SearchOptions {
if (isNotEmpty(this.query)) {
args.push(`query=${this.query}`);
}
if (isNotEmpty(this.scope)) {
args.push(`scope=${this.scope}`);
}
if (isNotEmpty(this.dsoType)) {
args.push(`dsoType=${this.dsoType}`);
}
if (isNotEmpty(this.filters)) {
Object.entries(this.filters).forEach(([key, values]) => {
values.forEach((value) => args.push(`${key}=${value},query`));
this.filters.forEach((filter: SearchFilter) => {
filter.values.forEach((value) => args.push(`${filter.key}=${value},${filter.operator}`));
});
}
if (isNotEmpty(args)) {

View File

@@ -178,4 +178,4 @@ describe('SearchPageComponent', () => {
});
});
});
})

View File

@@ -4,26 +4,27 @@ import { PaginationComponentOptions } from '../../shared/pagination/pagination-c
import { SortDirection, SortOptions } from '../../core/cache/models/sort-options.model';
import { PaginatedSearchOptions } from '../paginated-search-options.model';
import { Observable } from 'rxjs/Observable';
import { SearchFilter } from '../search-filter.model';
describe('SearchConfigurationService', () => {
let service: SearchConfigurationService;
const value1 = 'random value';
const value2 = 'another value';
const prefixFilter = {
'f.author': ['another value'],
'f.date.min': ['2013'],
'f.date.max': ['2018']
};
const defaults = new PaginatedSearchOptions( {
const defaults = new PaginatedSearchOptions({
pagination: Object.assign(new PaginationComponentOptions(), { currentPage: 1, pageSize: 20 }),
sort: new SortOptions('score', SortDirection.DESC),
query: '',
scope: ''
});
const backendFilters = { 'f.author': ['another value'], 'f.date': ['[2013 TO 2018]'] };
const backendFilters = [new SearchFilter('f.author', ['another value']), new SearchFilter('f.date', ['[2013 TO 2018]'])];
const spy = jasmine.createSpyObj('RouteService', {
getQueryParameterValue: Observable.of([value1, value2]),
getQueryParameterValue: Observable.of(value1),
getQueryParamsWithPrefix: Observable.of(prefixFilter)
});
@@ -51,6 +52,15 @@ describe('SearchConfigurationService', () => {
});
});
describe('when getCurrentDSOType is called', () => {
beforeEach(() => {
service.getCurrentDSOType();
});
it('should call getQueryParameterValue on the routeService with parameter name \'dsoType\'', () => {
expect((service as any).routeService.getQueryParameterValue).toHaveBeenCalledWith('dsoType');
});
});
describe('when getCurrentFrontendFilters is called', () => {
beforeEach(() => {
service.getCurrentFrontendFilters();
@@ -101,6 +111,7 @@ describe('SearchConfigurationService', () => {
spyOn(service, 'getCurrentSort').and.callThrough();
spyOn(service, 'getCurrentScope').and.callThrough();
spyOn(service, 'getCurrentQuery').and.callThrough();
spyOn(service, 'getCurrentDSOType').and.callThrough();
spyOn(service, 'getCurrentFilters').and.callThrough();
});
@@ -113,6 +124,7 @@ describe('SearchConfigurationService', () => {
expect(service.getCurrentSort).not.toHaveBeenCalled();
expect(service.getCurrentScope).toHaveBeenCalled();
expect(service.getCurrentQuery).toHaveBeenCalled();
expect(service.getCurrentDSOType).toHaveBeenCalled();
expect(service.getCurrentFilters).toHaveBeenCalled();
});
});
@@ -126,6 +138,7 @@ describe('SearchConfigurationService', () => {
expect(service.getCurrentSort).toHaveBeenCalled();
expect(service.getCurrentScope).toHaveBeenCalled();
expect(service.getCurrentQuery).toHaveBeenCalled();
expect(service.getCurrentDSOType).toHaveBeenCalled();
expect(service.getCurrentFilters).toHaveBeenCalled();
});
});

View File

@@ -6,11 +6,13 @@ import { ActivatedRoute, Params } from '@angular/router';
import { PaginatedSearchOptions } from '../paginated-search-options.model';
import { Injectable, OnDestroy } from '@angular/core';
import { RouteService } from '../../shared/services/route.service';
import { hasNoValue, isEmpty, isNotEmpty } from '../../shared/empty.util';
import { hasNoValue, hasValue, isEmpty, isNotEmpty } from '../../shared/empty.util';
import { RemoteData } from '../../core/data/remote-data';
import { BehaviorSubject } from 'rxjs/BehaviorSubject';
import { Subscription } from 'rxjs/Subscription';
import { getSucceededRemoteData } from '../../core/shared/operators';
import { SearchFilter } from '../search-filter.model';
import { DSpaceObjectType } from '../../core/shared/dspace-object-type.model';
/**
* Service that performs all actions that have to do with the current search configuration
@@ -99,6 +101,15 @@ export class SearchConfigurationService implements OnDestroy {
});
}
/**
* @returns {Observable<number>} Emits the current DSpaceObject type as a number
*/
getCurrentDSOType(): Observable<DSpaceObjectType> {
return this.routeService.getQueryParameterValue('dsoType')
.filter((type) => hasValue(type) && hasValue(DSpaceObjectType[type.toUpperCase()]))
.map((type) => DSpaceObjectType[type.toUpperCase()]);
}
/**
* @returns {Observable<string>} Emits the current pagination settings
*/
@@ -133,25 +144,25 @@ export class SearchConfigurationService implements OnDestroy {
/**
* @returns {Observable<Params>} Emits the current active filters with their values as they are sent to the backend
*/
getCurrentFilters(): Observable<Params> {
getCurrentFilters(): Observable<SearchFilter[]> {
return this.routeService.getQueryParamsWithPrefix('f.').map((filterParams) => {
if (isNotEmpty(filterParams)) {
const params = {};
const filters = [];
Object.keys(filterParams).forEach((key) => {
if (key.endsWith('.min') || key.endsWith('.max')) {
const realKey = key.slice(0, -4);
if (isEmpty(params[realKey])) {
if (hasNoValue(filters.find((filter) => filter.key === realKey))) {
const min = filterParams[realKey + '.min'] ? filterParams[realKey + '.min'][0] : '*';
const max = filterParams[realKey + '.max'] ? filterParams[realKey + '.max'][0] : '*';
params[realKey] = ['[' + min + ' TO ' + max + ']'];
filters.push(new SearchFilter(realKey, ['[' + min + ' TO ' + max + ']']));
}
} else {
params[key] = filterParams[key];
filters.push(new SearchFilter(key, filterParams[key]));
}
});
return params;
return filters;
}
return filterParams;
return [];
});
}
@@ -171,6 +182,7 @@ export class SearchConfigurationService implements OnDestroy {
return Observable.merge(
this.getScopePart(defaults.scope),
this.getQueryPart(defaults.query),
this.getDSOTypePart(),
this.getFiltersPart()
).subscribe((update) => {
const currentValue: SearchOptions = this.searchOptions.getValue();
@@ -190,6 +202,7 @@ export class SearchConfigurationService implements OnDestroy {
this.getSortPart(defaults.sort),
this.getScopePart(defaults.scope),
this.getQueryPart(defaults.query),
this.getDSOTypePart(),
this.getFiltersPart()
).subscribe((update) => {
const currentValue: PaginatedSearchOptions = this.paginatedSearchOptions.getValue();
@@ -241,6 +254,15 @@ export class SearchConfigurationService implements OnDestroy {
});
}
/**
* @returns {Observable<string>} Emits the current query string as a partial SearchOptions object
*/
private getDSOTypePart(): Observable<any> {
return this.getCurrentDSOType().map((dsoType) => {
return { dsoType }
});
}
/**
* @returns {Observable<string>} Emits the current pagination settings as a partial SearchOptions object
*/

View File

@@ -0,0 +1,7 @@
export enum DSpaceObjectType {
BUNDLE = 'BUNDLE',
BITSTREAM = 'BITSTREAM',
ITEM = 'ITEM',
COLLECTION = 'COLLECTION',
COMMUNITY = 'COMMUNITY',
}

View File

@@ -1,4 +1,4 @@
<div class="thumbnail">
<img *ngIf="thumbnail" [src]="thumbnail.content" (error)="errorHandler($event)"/>
<img *ngIf="!thumbnail" [src]="holderSource | dsSafeUrl"/>
<img *ngIf="thumbnail" [src]="thumbnail.content" (error)="errorHandler($event)" class="img-fluid"/>
<img *ngIf="!thumbnail" [src]="holderSource | dsSafeUrl" class="img-fluid"/>
</div>