mirror of
https://github.com/DSpace/dspace-angular.git
synced 2025-10-10 11:33:04 +00:00
Merge remote-tracking branch 'remotes/origin/master' into configuration
# Conflicts: # src/app/core/core.module.ts
This commit is contained in:
@@ -17,13 +17,13 @@ export class ProtractorPage {
|
|||||||
}
|
}
|
||||||
|
|
||||||
getCurrentScope(): promise.Promise<string> {
|
getCurrentScope(): promise.Promise<string> {
|
||||||
const scopeSelect = element(by.tagName('select'));
|
const scopeSelect = element(by.css('#search-form select'));
|
||||||
browser.wait(protractor.ExpectedConditions.presenceOf(scopeSelect), 10000);
|
browser.wait(protractor.ExpectedConditions.presenceOf(scopeSelect), 10000);
|
||||||
return scopeSelect.getAttribute('value');
|
return scopeSelect.getAttribute('value');
|
||||||
}
|
}
|
||||||
|
|
||||||
getCurrentQuery(): promise.Promise<string> {
|
getCurrentQuery(): promise.Promise<string> {
|
||||||
return element(by.tagName('input')).getAttribute('value');
|
return element(by.css('#search-form input')).getAttribute('value');
|
||||||
}
|
}
|
||||||
|
|
||||||
setCurrentScope(scope: string) {
|
setCurrentScope(scope: string) {
|
||||||
|
@@ -80,16 +80,48 @@
|
|||||||
"search_dspace": "Search DSpace"
|
"search_dspace": "Search DSpace"
|
||||||
},
|
},
|
||||||
"results": {
|
"results": {
|
||||||
"title": "Search Results"
|
"head": "Search Results"
|
||||||
},
|
},
|
||||||
"sidebar": {
|
"sidebar": {
|
||||||
"close": "Back to results",
|
"close": "Back to results",
|
||||||
"open": "Search Tools",
|
"open": "Search Tools",
|
||||||
"results": "results"
|
"results": "results",
|
||||||
|
"filters":{
|
||||||
|
"title":"Filters"
|
||||||
|
},
|
||||||
|
"settings":{
|
||||||
|
"title":"Settings",
|
||||||
|
"sort-by":"Sort By",
|
||||||
|
"rpp":"Results per page"
|
||||||
|
}
|
||||||
},
|
},
|
||||||
"view-switch": {
|
"view-switch": {
|
||||||
"show-list": "Show as list",
|
"show-list": "Show as list",
|
||||||
"show-grid": "Show as grid"
|
"show-grid": "Show as grid"
|
||||||
|
},
|
||||||
|
"filters": {
|
||||||
|
"head": "Filters",
|
||||||
|
"reset": "Reset filters",
|
||||||
|
"filter": {
|
||||||
|
"show-more": "Show more",
|
||||||
|
"show-less": "Collapse",
|
||||||
|
"author": {
|
||||||
|
"placeholder": "Author name",
|
||||||
|
"head": "Author"
|
||||||
|
},
|
||||||
|
"scope": {
|
||||||
|
"placeholder": "Scope filter",
|
||||||
|
"head": "Scope"
|
||||||
|
},
|
||||||
|
"subject": {
|
||||||
|
"placeholder": "Subject",
|
||||||
|
"head": "Subject"
|
||||||
|
},
|
||||||
|
"date": {
|
||||||
|
"placeholder": "Date",
|
||||||
|
"head": "Date"
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"loading": {
|
"loading": {
|
||||||
|
@@ -0,0 +1,35 @@
|
|||||||
|
<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 clearfix"
|
||||||
|
[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="float-right filter-value-count">
|
||||||
|
<span class="badge badge-secondary badge-pill">{{value.count}}</span>
|
||||||
|
</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)="showFirstPageOnly()">{{"search.filters.filter.show-less"
|
||||||
|
| translate}}</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<form #form="ngForm" (ngSubmit)="onSubmit(form.value)" class="add-filter"
|
||||||
|
[action]="getCurrentUrl()">
|
||||||
|
<input type="text" [(ngModel)]="filter" [name]="filterConfig.paramName" class="form-control"
|
||||||
|
aria-label="New filter input"
|
||||||
|
[placeholder]="'search.filters.filter.' + filterConfig.name + '.placeholder'| translate"/>
|
||||||
|
<input type="submit" class="d-none"/>
|
||||||
|
</form>
|
||||||
|
</div>
|
@@ -0,0 +1,18 @@
|
|||||||
|
@import '../../../../../styles/variables.scss';
|
||||||
|
@import '../../../../../styles/mixins.scss';
|
||||||
|
|
||||||
|
.filters {
|
||||||
|
margin-top: $spacer/2;
|
||||||
|
margin-bottom: $spacer/2;
|
||||||
|
a {
|
||||||
|
color: $body-color;
|
||||||
|
&:hover {
|
||||||
|
text-decoration: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.toggle-more-filters a {
|
||||||
|
color: $link-color;
|
||||||
|
text-decoration: underline;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
}
|
@@ -0,0 +1,191 @@
|
|||||||
|
import { ChangeDetectionStrategy, NO_ERRORS_SCHEMA } from '@angular/core';
|
||||||
|
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
|
||||||
|
import { RouterTestingModule } from '@angular/router/testing';
|
||||||
|
import { TranslateModule } from '@ngx-translate/core';
|
||||||
|
import { NoopAnimationsModule } from '@angular/platform-browser/animations';
|
||||||
|
import { SearchFacetFilterComponent } from './search-facet-filter.component';
|
||||||
|
import { SearchFilterService } from '../search-filter.service';
|
||||||
|
import { SearchFilterConfig } from '../../../search-service/search-filter-config.model';
|
||||||
|
import { FilterType } from '../../../search-service/filter-type.model';
|
||||||
|
import { FacetValue } from '../../../search-service/facet-value.model';
|
||||||
|
import { FormsModule } from '@angular/forms';
|
||||||
|
import { Observable } from 'rxjs/Observable';
|
||||||
|
|
||||||
|
describe('SearchFacetFilterComponent', () => {
|
||||||
|
let comp: SearchFacetFilterComponent;
|
||||||
|
let fixture: ComponentFixture<SearchFacetFilterComponent>;
|
||||||
|
const filterName1 = 'test name';
|
||||||
|
const value1 = 'testvalue1';
|
||||||
|
const value2 = 'test2';
|
||||||
|
const value3 = 'another value3';
|
||||||
|
const mockFilterConfig: SearchFilterConfig = Object.assign(new SearchFilterConfig(), {
|
||||||
|
name: filterName1,
|
||||||
|
type: FilterType.text,
|
||||||
|
hasFacets: false,
|
||||||
|
isOpenByDefault: false,
|
||||||
|
pageSize: 2
|
||||||
|
});
|
||||||
|
const values: FacetValue[] = [
|
||||||
|
{
|
||||||
|
value: value1,
|
||||||
|
count: 52,
|
||||||
|
search: ''
|
||||||
|
}, {
|
||||||
|
value: value2,
|
||||||
|
count: 20,
|
||||||
|
search: ''
|
||||||
|
}, {
|
||||||
|
value: value3,
|
||||||
|
count: 5,
|
||||||
|
search: ''
|
||||||
|
}
|
||||||
|
];
|
||||||
|
let filterService;
|
||||||
|
let page = Observable.of(0)
|
||||||
|
beforeEach(async(() => {
|
||||||
|
TestBed.configureTestingModule({
|
||||||
|
imports: [TranslateModule.forRoot(), RouterTestingModule.withRoutes([]), NoopAnimationsModule, FormsModule],
|
||||||
|
declarations: [SearchFacetFilterComponent],
|
||||||
|
providers: [
|
||||||
|
{
|
||||||
|
provide: SearchFilterService,
|
||||||
|
useValue: {
|
||||||
|
isFilterActiveWithValue: (paramName: string, filterValue: string) => true,
|
||||||
|
getQueryParamsWith: (paramName: string, filterValue: string) => '',
|
||||||
|
getQueryParamsWithout: (paramName: string, filterValue: string) => '',
|
||||||
|
getPage: (paramName: string) => page,
|
||||||
|
/* tslint:disable:no-empty */
|
||||||
|
incrementPage: (filterName: string) => {
|
||||||
|
},
|
||||||
|
resetPage: (filterName: string) => {
|
||||||
|
},
|
||||||
|
/* tslint:enable:no-empty */
|
||||||
|
searchLink: '/search',
|
||||||
|
}
|
||||||
|
},
|
||||||
|
],
|
||||||
|
schemas: [NO_ERRORS_SCHEMA]
|
||||||
|
}).overrideComponent(SearchFacetFilterComponent, {
|
||||||
|
set: { changeDetection: ChangeDetectionStrategy.Default }
|
||||||
|
}).compileComponents();
|
||||||
|
}));
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
fixture = TestBed.createComponent(SearchFacetFilterComponent);
|
||||||
|
comp = fixture.componentInstance; // SearchPageComponent test instance
|
||||||
|
comp.filterConfig = mockFilterConfig;
|
||||||
|
comp.filterValues = values;
|
||||||
|
filterService = (comp as any).filterService;
|
||||||
|
fixture.detectChanges();
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('when the isChecked method is called with a value', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
spyOn(filterService, 'isFilterActiveWithValue');
|
||||||
|
comp.isChecked(values[1]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should call isFilterActiveWithValue on the filterService with the correct filter parameter name and the passed value', () => {
|
||||||
|
expect(filterService.isFilterActiveWithValue).toHaveBeenCalledWith(mockFilterConfig.paramName, values[1].value)
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('when the getSearchLink method is triggered', () => {
|
||||||
|
let link: string;
|
||||||
|
beforeEach(() => {
|
||||||
|
link = comp.getSearchLink();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return the value of the searchLink variable in the filter service', () => {
|
||||||
|
expect(link).toEqual(filterService.searchLink);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('when the getQueryParamsWith method is called wih a value', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
spyOn(filterService, 'getQueryParamsWith');
|
||||||
|
comp.getQueryParamsWith(values[1].value);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should call getQueryParamsWith on the filterService with the correct filter parameter name and the passed value', () => {
|
||||||
|
expect(filterService.getQueryParamsWith).toHaveBeenCalledWith(mockFilterConfig, values[1].value)
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('when the getQueryParamsWithout method is called wih a value', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
spyOn(filterService, 'getQueryParamsWithout');
|
||||||
|
comp.getQueryParamsWithout(values[1].value);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should call getQueryParamsWithout on the filterService with the correct filter parameter name and the passed value', () => {
|
||||||
|
expect(filterService.getQueryParamsWithout).toHaveBeenCalledWith(mockFilterConfig, values[1].value)
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('when the facetCount method is triggered when there are less items than the amount of pages should display', () => {
|
||||||
|
let count: Observable<number>;
|
||||||
|
beforeEach(() => {
|
||||||
|
comp.currentPage = Observable.of(3);
|
||||||
|
// 2 x 3 = 6, there are only 3 values
|
||||||
|
count = comp.facetCount;
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return the correct number of items shown (this equals the total amount of values for this filter)', () => {
|
||||||
|
const sub = count.subscribe((c) => expect(c).toBe(values.length));
|
||||||
|
sub.unsubscribe();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('when the facetCount method is triggered when there are more items than the amount of pages should display', () => {
|
||||||
|
let count: Observable<number>;
|
||||||
|
beforeEach(() => {
|
||||||
|
comp.currentPage = Observable.of(1);
|
||||||
|
// 2 x 1 = 2, there are more than 2 (3) items
|
||||||
|
count = comp.facetCount;
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return the correct number of items shown (this equals the page count x page size)', () => {
|
||||||
|
const sub = count.subscribe((c) => {
|
||||||
|
const subsub = comp.currentPage.subscribe((page) => {
|
||||||
|
expect(c).toBe(page * mockFilterConfig.pageSize);
|
||||||
|
});
|
||||||
|
subsub.unsubscribe()
|
||||||
|
});
|
||||||
|
sub.unsubscribe();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('when the showMore method is called', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
spyOn(filterService, 'incrementPage');
|
||||||
|
comp.showMore();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should call incrementPage on the filterService with the correct filter parameter name', () => {
|
||||||
|
expect(filterService.incrementPage).toHaveBeenCalledWith(mockFilterConfig.name)
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('when the showFirstPageOnly method is called', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
spyOn(filterService, 'resetPage');
|
||||||
|
comp.showFirstPageOnly();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should call resetPage on the filterService with the correct filter parameter name', () => {
|
||||||
|
expect(filterService.resetPage).toHaveBeenCalledWith(mockFilterConfig.name)
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('when the getCurrentPage method is called', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
spyOn(filterService, 'getPage');
|
||||||
|
comp.getCurrentPage();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should call getPage on the filterService with the correct filter parameter name', () => {
|
||||||
|
expect(filterService.getPage).toHaveBeenCalledWith(mockFilterConfig.name)
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
@@ -0,0 +1,86 @@
|
|||||||
|
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 { 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.
|
||||||
|
* The route parameter 'id' is used to request the item it represents.
|
||||||
|
* All fields of the item that should be displayed, are defined in its template.
|
||||||
|
*/
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
selector: 'ds-search-facet-filter',
|
||||||
|
styleUrls: ['./search-facet-filter.component.scss'],
|
||||||
|
templateUrl: './search-facet-filter.component.html',
|
||||||
|
})
|
||||||
|
|
||||||
|
export class SearchFacetFilterComponent implements OnInit {
|
||||||
|
@Input() filterValues: FacetValue[];
|
||||||
|
@Input() filterConfig: SearchFilterConfig;
|
||||||
|
@Input() selectedValues: string[];
|
||||||
|
currentPage: Observable<number>;
|
||||||
|
filter: string;
|
||||||
|
|
||||||
|
constructor(private filterService: SearchFilterService, private router: Router) {
|
||||||
|
}
|
||||||
|
|
||||||
|
ngOnInit(): void {
|
||||||
|
this.currentPage = this.filterService.getPage(this.filterConfig.name);
|
||||||
|
}
|
||||||
|
|
||||||
|
isChecked(value: FacetValue): Observable<boolean> {
|
||||||
|
return this.filterService.isFilterActiveWithValue(this.filterConfig.paramName, value.value);
|
||||||
|
}
|
||||||
|
|
||||||
|
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;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
showMore() {
|
||||||
|
this.filterService.incrementPage(this.filterConfig.name);
|
||||||
|
}
|
||||||
|
|
||||||
|
showFirstPageOnly() {
|
||||||
|
this.filterService.resetPage(this.filterConfig.name);
|
||||||
|
}
|
||||||
|
|
||||||
|
getCurrentPage(): Observable<number> {
|
||||||
|
return this.filterService.getPage(this.filterConfig.name);
|
||||||
|
}
|
||||||
|
|
||||||
|
getCurrentUrl() {
|
||||||
|
return this.router.url;
|
||||||
|
}
|
||||||
|
|
||||||
|
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.filter = '';
|
||||||
|
sub.unsubscribe();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@@ -0,0 +1,63 @@
|
|||||||
|
import { Action } from '@ngrx/store';
|
||||||
|
|
||||||
|
import { type } from '../../../shared/ngrx/type';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* For each action type in an action group, make a simple
|
||||||
|
* enum object for all of this group's action types.
|
||||||
|
*
|
||||||
|
* The 'type' utility function coerces strings into string
|
||||||
|
* literal types and runs a simple check to guarantee all
|
||||||
|
* action types in the application are unique.
|
||||||
|
*/
|
||||||
|
export const SearchFilterActionTypes = {
|
||||||
|
COLLAPSE: type('dspace/search-filter/COLLAPSE'),
|
||||||
|
INITIAL_COLLAPSE: type('dspace/search-filter/INITIAL_COLLAPSE'),
|
||||||
|
EXPAND: type('dspace/search-filter/EXPAND'),
|
||||||
|
INITIAL_EXPAND: type('dspace/search-filter/INITIAL_EXPAND'),
|
||||||
|
TOGGLE: type('dspace/search-filter/TOGGLE'),
|
||||||
|
DECREMENT_PAGE: type('dspace/search-filter/DECREMENT_PAGE'),
|
||||||
|
INCREMENT_PAGE: type('dspace/search-filter/INCREMENT_PAGE'),
|
||||||
|
RESET_PAGE: type('dspace/search-filter/RESET_PAGE')
|
||||||
|
};
|
||||||
|
|
||||||
|
export class SearchFilterAction implements Action {
|
||||||
|
filterName: string;
|
||||||
|
type;
|
||||||
|
constructor(name: string) {
|
||||||
|
this.filterName = name;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* tslint:disable:max-classes-per-file */
|
||||||
|
export class SearchFilterCollapseAction extends SearchFilterAction {
|
||||||
|
type = SearchFilterActionTypes.COLLAPSE;
|
||||||
|
}
|
||||||
|
|
||||||
|
export class SearchFilterExpandAction extends SearchFilterAction {
|
||||||
|
type = SearchFilterActionTypes.EXPAND;
|
||||||
|
}
|
||||||
|
|
||||||
|
export class SearchFilterToggleAction extends SearchFilterAction {
|
||||||
|
type = SearchFilterActionTypes.TOGGLE;
|
||||||
|
}
|
||||||
|
|
||||||
|
export class SearchFilterInitialCollapseAction extends SearchFilterAction {
|
||||||
|
type = SearchFilterActionTypes.INITIAL_COLLAPSE;
|
||||||
|
}
|
||||||
|
|
||||||
|
export class SearchFilterInitialExpandAction extends SearchFilterAction {
|
||||||
|
type = SearchFilterActionTypes.INITIAL_EXPAND;
|
||||||
|
}
|
||||||
|
export class SearchFilterDecrementPageAction extends SearchFilterAction {
|
||||||
|
type = SearchFilterActionTypes.DECREMENT_PAGE;
|
||||||
|
}
|
||||||
|
|
||||||
|
export class SearchFilterIncrementPageAction extends SearchFilterAction {
|
||||||
|
type = SearchFilterActionTypes.INCREMENT_PAGE;
|
||||||
|
}
|
||||||
|
|
||||||
|
export class SearchFilterResetPageAction extends SearchFilterAction {
|
||||||
|
type = SearchFilterActionTypes.RESET_PAGE;
|
||||||
|
}
|
||||||
|
/* tslint:enable:max-classes-per-file */
|
@@ -0,0 +1,8 @@
|
|||||||
|
<div>
|
||||||
|
<div (click)="toggle()" class="filter-name"><h5 class="d-inline-block mb-0">{{'search.filters.filter.' + filter.name + '.head'| translate}}</h5> <span class="filter-toggle 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>
|
||||||
|
</div>
|
||||||
|
</div>
|
@@ -0,0 +1,12 @@
|
|||||||
|
@import '../../../../styles/variables.scss';
|
||||||
|
@import '../../../../styles/mixins.scss';
|
||||||
|
|
||||||
|
:host {
|
||||||
|
border: 1px solid map-get($theme-colors, light);
|
||||||
|
.search-filter-wrapper {
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
.filter-toggle {
|
||||||
|
line-height: $line-height-base;
|
||||||
|
}
|
||||||
|
}
|
@@ -0,0 +1,171 @@
|
|||||||
|
import { ChangeDetectionStrategy, NO_ERRORS_SCHEMA } from '@angular/core';
|
||||||
|
|
||||||
|
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
|
||||||
|
import { RouterTestingModule } from '@angular/router/testing';
|
||||||
|
import { TranslateModule } from '@ngx-translate/core';
|
||||||
|
import { Observable } from 'rxjs/Observable';
|
||||||
|
import { NoopAnimationsModule } from '@angular/platform-browser/animations';
|
||||||
|
import { SearchFilterService } from './search-filter.service';
|
||||||
|
import { SearchService } from '../../search-service/search.service';
|
||||||
|
import { SearchFilterComponent } from './search-filter.component';
|
||||||
|
import { SearchFilterConfig } from '../../search-service/search-filter-config.model';
|
||||||
|
import { FilterType } from '../../search-service/filter-type.model';
|
||||||
|
|
||||||
|
describe('SearchFilterComponent', () => {
|
||||||
|
let comp: SearchFilterComponent;
|
||||||
|
let fixture: ComponentFixture<SearchFilterComponent>;
|
||||||
|
const filterName1 = 'test name';
|
||||||
|
const filterName2 = 'test2';
|
||||||
|
const filterName3 = 'another name3';
|
||||||
|
const nonExistingFilter1 = 'non existing 1';
|
||||||
|
const nonExistingFilter2 = 'non existing 2';
|
||||||
|
const mockFilterConfig: SearchFilterConfig = Object.assign(new SearchFilterConfig(), {
|
||||||
|
name: filterName1,
|
||||||
|
type: FilterType.text,
|
||||||
|
hasFacets: false,
|
||||||
|
isOpenByDefault: false
|
||||||
|
});
|
||||||
|
const mockFilterService = {
|
||||||
|
/* tslint:disable:no-empty */
|
||||||
|
toggle: (filter) => {
|
||||||
|
},
|
||||||
|
collapse: (filter) => {
|
||||||
|
},
|
||||||
|
expand: (filter) => {
|
||||||
|
},
|
||||||
|
initialCollapse: (filter) => {
|
||||||
|
},
|
||||||
|
initialExpand: (filter) => {
|
||||||
|
},
|
||||||
|
getSelectedValuesForFilter: (filter) => {
|
||||||
|
return Observable.of([filterName1, filterName2, filterName3])
|
||||||
|
},
|
||||||
|
isFilterActive: (filter) => {
|
||||||
|
return Observable.of([filterName1, filterName2, filterName3].indexOf(filter) >= 0);
|
||||||
|
},
|
||||||
|
isCollapsed: (filter) => {
|
||||||
|
return Observable.of(true)
|
||||||
|
}
|
||||||
|
/* tslint:enable:no-empty */
|
||||||
|
|
||||||
|
};
|
||||||
|
let filterService;
|
||||||
|
const mockResults = Observable.of(['test', 'data']);
|
||||||
|
const searchServiceStub = {
|
||||||
|
getFacetValuesFor: (filter) => mockResults
|
||||||
|
};
|
||||||
|
|
||||||
|
beforeEach(async(() => {
|
||||||
|
TestBed.configureTestingModule({
|
||||||
|
imports: [TranslateModule.forRoot(), RouterTestingModule.withRoutes([]), NoopAnimationsModule],
|
||||||
|
declarations: [SearchFilterComponent],
|
||||||
|
providers: [
|
||||||
|
{ provide: SearchService, useValue: searchServiceStub },
|
||||||
|
{
|
||||||
|
provide: SearchFilterService,
|
||||||
|
useValue: mockFilterService
|
||||||
|
},
|
||||||
|
],
|
||||||
|
schemas: [NO_ERRORS_SCHEMA]
|
||||||
|
}).overrideComponent(SearchFilterComponent, {
|
||||||
|
set: { changeDetection: ChangeDetectionStrategy.Default }
|
||||||
|
}).compileComponents();
|
||||||
|
}));
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
fixture = TestBed.createComponent(SearchFilterComponent);
|
||||||
|
comp = fixture.componentInstance; // SearchPageComponent test instance
|
||||||
|
comp.filter = mockFilterConfig;
|
||||||
|
fixture.detectChanges();
|
||||||
|
filterService = (comp as any).filterService;
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('when the toggle method is triggered', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
spyOn(filterService, 'toggle');
|
||||||
|
comp.toggle();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should call toggle with the correct filter configuration name', () => {
|
||||||
|
expect(filterService.toggle).toHaveBeenCalledWith(mockFilterConfig.name)
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('when the initialCollapse method is triggered', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
spyOn(filterService, 'initialCollapse');
|
||||||
|
comp.initialCollapse();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should call initialCollapse with the correct filter configuration name', () => {
|
||||||
|
expect(filterService.initialCollapse).toHaveBeenCalledWith(mockFilterConfig.name)
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('when the initialExpand method is triggered', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
spyOn(filterService, 'initialExpand');
|
||||||
|
comp.initialExpand();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should call initialCollapse with the correct filter configuration name', () => {
|
||||||
|
expect(filterService.initialExpand).toHaveBeenCalledWith(mockFilterConfig.name)
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('when getSelectedValues is called', () => {
|
||||||
|
let valuesObservable: Observable<string[]>;
|
||||||
|
beforeEach(() => {
|
||||||
|
valuesObservable = comp.getSelectedValues();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return an observable containing the existing filters', () => {
|
||||||
|
const sub = valuesObservable.subscribe((values) => {
|
||||||
|
expect(values).toContain(filterName1);
|
||||||
|
expect(values).toContain(filterName2);
|
||||||
|
expect(values).toContain(filterName3);
|
||||||
|
});
|
||||||
|
sub.unsubscribe();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return an observable that does not contain the non-existing filters', () => {
|
||||||
|
const sub = valuesObservable.subscribe((values) => {
|
||||||
|
expect(values).not.toContain(nonExistingFilter1);
|
||||||
|
expect(values).not.toContain(nonExistingFilter2);
|
||||||
|
});
|
||||||
|
sub.unsubscribe();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('when isCollapsed is called and the filter is collapsed', () => {
|
||||||
|
let isActive: Observable<boolean>;
|
||||||
|
beforeEach(() => {
|
||||||
|
filterService.isCollapsed = () => Observable.of(true);
|
||||||
|
isActive = comp.isCollapsed();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return an observable containing true', () => {
|
||||||
|
const sub = isActive.subscribe((value) => {
|
||||||
|
expect(value).toBeTruthy();
|
||||||
|
});
|
||||||
|
sub.unsubscribe();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('when isCollapsed is called and the filter is not collapsed', () => {
|
||||||
|
let isActive: Observable<boolean>;
|
||||||
|
beforeEach(() => {
|
||||||
|
filterService.isCollapsed = () => Observable.of(false);
|
||||||
|
isActive = comp.isCollapsed();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return an observable containing false', () => {
|
||||||
|
const sub = isActive.subscribe((value) => {
|
||||||
|
expect(value).toBeFalsy();
|
||||||
|
});
|
||||||
|
sub.unsubscribe();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
|
});
|
@@ -0,0 +1,63 @@
|
|||||||
|
import { Component, Input, OnInit } from '@angular/core';
|
||||||
|
import { SearchFilterConfig } from '../../search-service/search-filter-config.model';
|
||||||
|
import { SearchService } from '../../search-service/search.service';
|
||||||
|
import { RemoteData } from '../../../core/data/remote-data';
|
||||||
|
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';
|
||||||
|
import { first } from 'rxjs/operator/first';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This component renders a simple item page.
|
||||||
|
* The route parameter 'id' is used to request the item it represents.
|
||||||
|
* All fields of the item that should be displayed, are defined in its template.
|
||||||
|
*/
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
selector: 'ds-search-filter',
|
||||||
|
styleUrls: ['./search-filter.component.scss'],
|
||||||
|
templateUrl: './search-filter.component.html',
|
||||||
|
animations: [slide],
|
||||||
|
})
|
||||||
|
|
||||||
|
export class SearchFilterComponent implements OnInit {
|
||||||
|
@Input() filter: SearchFilterConfig;
|
||||||
|
filterValues: Observable<RemoteData<FacetValue[]>>;
|
||||||
|
|
||||||
|
constructor(private searchService: SearchService, 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();
|
||||||
|
} else {
|
||||||
|
this.initialCollapse();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
sub.unsubscribe();
|
||||||
|
}
|
||||||
|
|
||||||
|
toggle() {
|
||||||
|
this.filterService.toggle(this.filter.name);
|
||||||
|
}
|
||||||
|
|
||||||
|
isCollapsed(): Observable<boolean> {
|
||||||
|
return this.filterService.isCollapsed(this.filter.name);
|
||||||
|
}
|
||||||
|
|
||||||
|
initialCollapse() {
|
||||||
|
this.filterService.initialCollapse(this.filter.name);
|
||||||
|
}
|
||||||
|
|
||||||
|
initialExpand() {
|
||||||
|
this.filterService.initialExpand(this.filter.name);
|
||||||
|
}
|
||||||
|
|
||||||
|
getSelectedValues(): Observable<string[]> {
|
||||||
|
return this.filterService.getSelectedValuesForFilter(this.filter);
|
||||||
|
}
|
||||||
|
}
|
@@ -0,0 +1,165 @@
|
|||||||
|
import * as deepFreeze from 'deep-freeze';
|
||||||
|
import {
|
||||||
|
SearchFilterCollapseAction, SearchFilterExpandAction, SearchFilterIncrementPageAction,
|
||||||
|
SearchFilterInitialCollapseAction,
|
||||||
|
SearchFilterInitialExpandAction,
|
||||||
|
SearchFilterToggleAction,
|
||||||
|
SearchFilterDecrementPageAction, SearchFilterResetPageAction
|
||||||
|
} from './search-filter.actions';
|
||||||
|
import { filterReducer } from './search-filter.reducer';
|
||||||
|
|
||||||
|
const filterName1 = 'author';
|
||||||
|
const filterName2 = 'scope';
|
||||||
|
|
||||||
|
class NullAction extends SearchFilterCollapseAction {
|
||||||
|
type = null;
|
||||||
|
|
||||||
|
constructor() {
|
||||||
|
super(undefined);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
describe('filterReducer', () => {
|
||||||
|
|
||||||
|
it('should return the current state when no valid actions have been made', () => {
|
||||||
|
const state = { author: { filterCollapsed: true, page: 1 } };
|
||||||
|
const action = new NullAction();
|
||||||
|
const newState = filterReducer(state, action);
|
||||||
|
|
||||||
|
expect(newState).toEqual(state);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should start with an empty object', () => {
|
||||||
|
const state = Object.create({});
|
||||||
|
const action = new NullAction();
|
||||||
|
const initialState = filterReducer(undefined, action);
|
||||||
|
|
||||||
|
// The search filter starts collapsed
|
||||||
|
expect(initialState).toEqual(state);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should set filterCollapsed to true in response to the COLLAPSE action', () => {
|
||||||
|
const state = {};
|
||||||
|
state[filterName1] = { filterCollapsed: false, page: 1 };
|
||||||
|
const action = new SearchFilterCollapseAction(filterName1);
|
||||||
|
const newState = filterReducer(state, action);
|
||||||
|
|
||||||
|
expect(newState[filterName1].filterCollapsed).toEqual(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should perform the COLLAPSE action without affecting the previous state', () => {
|
||||||
|
const state = {};
|
||||||
|
state[filterName1] = { filterCollapsed: false, page: 1 };
|
||||||
|
deepFreeze([state]);
|
||||||
|
|
||||||
|
const action = new SearchFilterCollapseAction(filterName1);
|
||||||
|
filterReducer(state, action);
|
||||||
|
|
||||||
|
// no expect required, deepFreeze will ensure an exception is thrown if the state
|
||||||
|
// is mutated, and any uncaught exception will cause the test to fail
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should set filterCollapsed to false in response to the EXPAND action', () => {
|
||||||
|
const state = {};
|
||||||
|
state[filterName1] = { filterCollapsed: true, page: 1 };
|
||||||
|
const action = new SearchFilterExpandAction(filterName1);
|
||||||
|
const newState = filterReducer(state, action);
|
||||||
|
|
||||||
|
expect(newState[filterName1].filterCollapsed).toEqual(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should perform the EXPAND action without affecting the previous state', () => {
|
||||||
|
const state = {};
|
||||||
|
state[filterName1] = { filterCollapsed: true, page: 1 };
|
||||||
|
deepFreeze([state]);
|
||||||
|
|
||||||
|
const action = new SearchFilterExpandAction(filterName1);
|
||||||
|
filterReducer(state, action);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should flip the value of filterCollapsed in response to the TOGGLE action', () => {
|
||||||
|
const state1 = {};
|
||||||
|
state1[filterName1] = { filterCollapsed: true, page: 1 };
|
||||||
|
const action = new SearchFilterToggleAction(filterName1);
|
||||||
|
|
||||||
|
const state2 = filterReducer(state1, action);
|
||||||
|
const state3 = filterReducer(state2, action);
|
||||||
|
|
||||||
|
expect(state2[filterName1].filterCollapsed).toEqual(false);
|
||||||
|
expect(state3[filterName1].filterCollapsed).toEqual(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should perform the TOGGLE action without affecting the previous state', () => {
|
||||||
|
const state = {};
|
||||||
|
state[filterName1] = { filterCollapsed: true, page: 1 };
|
||||||
|
deepFreeze([state]);
|
||||||
|
|
||||||
|
const action = new SearchFilterToggleAction(filterName1);
|
||||||
|
filterReducer(state, action);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should set filterCollapsed to true in response to the INITIAL_COLLAPSE action when no state has been set for this filter', () => {
|
||||||
|
const state = {};
|
||||||
|
state[filterName2] = { filterCollapsed: false, page: 1 };
|
||||||
|
const action = new SearchFilterInitialCollapseAction(filterName1);
|
||||||
|
const newState = filterReducer(state, action);
|
||||||
|
|
||||||
|
expect(newState[filterName1].filterCollapsed).toEqual(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should set filterCollapsed to true in response to the INITIAL_EXPAND action when no state has been set for this filter', () => {
|
||||||
|
const state = {};
|
||||||
|
state[filterName2] = { filterCollapsed: true, page: 1 };
|
||||||
|
const action = new SearchFilterInitialExpandAction(filterName1);
|
||||||
|
const newState = filterReducer(state, action);
|
||||||
|
expect(newState[filterName1].filterCollapsed).toEqual(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should not change the state in response to the INITIAL_COLLAPSE action when the state has already been set for this filter', () => {
|
||||||
|
const state = {};
|
||||||
|
state[filterName1] = { filterCollapsed: false, page: 1 };
|
||||||
|
const action = new SearchFilterInitialCollapseAction(filterName1);
|
||||||
|
const newState = filterReducer(state, action);
|
||||||
|
expect(newState).toEqual(state);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should not change the state in response to the INITIAL_EXPAND action when the state has already been set for this filter', () => {
|
||||||
|
const state = {};
|
||||||
|
state[filterName1] = { filterCollapsed: true, page: 1 };
|
||||||
|
const action = new SearchFilterInitialExpandAction(filterName1);
|
||||||
|
const newState = filterReducer(state, action);
|
||||||
|
expect(newState).toEqual(state);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should increment with 1 for the specified filter in response to the INCREMENT_PAGE action', () => {
|
||||||
|
const state = {};
|
||||||
|
state[filterName1] = { filterCollapsed: true, page: 5 };
|
||||||
|
const action = new SearchFilterIncrementPageAction(filterName1);
|
||||||
|
const newState = filterReducer(state, action);
|
||||||
|
expect(newState[filterName1].page).toEqual(6);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should decrement with 1 for the specified filter in response to the DECREMENT_PAGE action', () => {
|
||||||
|
const state = {};
|
||||||
|
state[filterName1] = { filterCollapsed: true, page: 12 };
|
||||||
|
const action = new SearchFilterDecrementPageAction(filterName1);
|
||||||
|
const newState = filterReducer(state, action);
|
||||||
|
expect(newState[filterName1].page).toEqual(11);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should not decrement when page is 1 for the specified filter in response to the DECREMENT_PAGE action', () => {
|
||||||
|
const state = {};
|
||||||
|
state[filterName1] = { filterCollapsed: true, page: 1 };
|
||||||
|
const action = new SearchFilterDecrementPageAction(filterName1);
|
||||||
|
const newState = filterReducer(state, action);
|
||||||
|
expect(newState[filterName1].page).toEqual(1);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should reset the page to 1 for the specified filter in response to the RESET_PAGE action', () => {
|
||||||
|
const state = {};
|
||||||
|
state[filterName1] = { filterCollapsed: true, page: 20 };
|
||||||
|
const action = new SearchFilterResetPageAction(filterName1);
|
||||||
|
const newState = filterReducer(state, action);
|
||||||
|
expect(newState[filterName1].page).toEqual(1);
|
||||||
|
});
|
||||||
|
});
|
@@ -0,0 +1,105 @@
|
|||||||
|
import { SearchFilterAction, SearchFilterActionTypes } from './search-filter.actions';
|
||||||
|
import { isEmpty } from '../../../shared/empty.util';
|
||||||
|
|
||||||
|
export interface SearchFilterState {
|
||||||
|
filterCollapsed: boolean,
|
||||||
|
page: number
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface SearchFiltersState {
|
||||||
|
[name: string]: SearchFilterState
|
||||||
|
}
|
||||||
|
|
||||||
|
const initialState: SearchFiltersState = Object.create(null);
|
||||||
|
|
||||||
|
export function filterReducer(state = initialState, action: SearchFilterAction): SearchFiltersState {
|
||||||
|
|
||||||
|
switch (action.type) {
|
||||||
|
|
||||||
|
case SearchFilterActionTypes.INITIAL_COLLAPSE: {
|
||||||
|
if (isEmpty(state) || isEmpty(state[action.filterName])) {
|
||||||
|
return Object.assign({}, state, {
|
||||||
|
[action.filterName]: {
|
||||||
|
filterCollapsed: true,
|
||||||
|
page: 1
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
return state;
|
||||||
|
}
|
||||||
|
|
||||||
|
case SearchFilterActionTypes.INITIAL_EXPAND: {
|
||||||
|
if (isEmpty(state) || isEmpty(state[action.filterName])) {
|
||||||
|
return Object.assign({}, state, {
|
||||||
|
[action.filterName]: {
|
||||||
|
filterCollapsed: false,
|
||||||
|
page: 1
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
return state;
|
||||||
|
}
|
||||||
|
|
||||||
|
case SearchFilterActionTypes.COLLAPSE: {
|
||||||
|
return Object.assign({}, state, {
|
||||||
|
[action.filterName]: {
|
||||||
|
filterCollapsed: true,
|
||||||
|
page: state[action.filterName].page
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
case SearchFilterActionTypes.EXPAND: {
|
||||||
|
return Object.assign({}, state, {
|
||||||
|
[action.filterName]: {
|
||||||
|
filterCollapsed: false,
|
||||||
|
page: state[action.filterName].page
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
case SearchFilterActionTypes.DECREMENT_PAGE: {
|
||||||
|
const page = state[action.filterName].page - 1;
|
||||||
|
return Object.assign({}, state, {
|
||||||
|
[action.filterName]: {
|
||||||
|
filterCollapsed: state[action.filterName].filterCollapsed,
|
||||||
|
page: (page >= 1 ? page : 1)
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
case SearchFilterActionTypes.INCREMENT_PAGE: {
|
||||||
|
return Object.assign({}, state, {
|
||||||
|
[action.filterName]: {
|
||||||
|
filterCollapsed: state[action.filterName].filterCollapsed,
|
||||||
|
page: state[action.filterName].page + 1
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
}
|
||||||
|
case SearchFilterActionTypes.RESET_PAGE: {
|
||||||
|
return Object.assign({}, state, {
|
||||||
|
[action.filterName]: {
|
||||||
|
filterCollapsed: state[action.filterName].filterCollapsed,
|
||||||
|
page: 1
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
case SearchFilterActionTypes.TOGGLE: {
|
||||||
|
return Object.assign({}, state, {
|
||||||
|
[action.filterName]: {
|
||||||
|
filterCollapsed: !state[action.filterName].filterCollapsed,
|
||||||
|
page: state[action.filterName].page
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
default: {
|
||||||
|
return state;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@@ -0,0 +1,205 @@
|
|||||||
|
import { Observable } from 'rxjs/Observable';
|
||||||
|
import { SearchFilterService } from './search-filter.service';
|
||||||
|
import { Store } from '@ngrx/store';
|
||||||
|
import {
|
||||||
|
SearchFilterCollapseAction, SearchFilterDecrementPageAction, SearchFilterExpandAction,
|
||||||
|
SearchFilterIncrementPageAction,
|
||||||
|
SearchFilterInitialCollapseAction, SearchFilterInitialExpandAction, SearchFilterResetPageAction,
|
||||||
|
SearchFilterToggleAction
|
||||||
|
} from './search-filter.actions';
|
||||||
|
import { SearchFiltersState } from './search-filter.reducer';
|
||||||
|
import { SearchFilterConfig } from '../../search-service/search-filter-config.model';
|
||||||
|
import { FilterType } from '../../search-service/filter-type.model';
|
||||||
|
|
||||||
|
describe('SearchFilterService', () => {
|
||||||
|
let service: SearchFilterService;
|
||||||
|
const filterName1 = 'test name';
|
||||||
|
const mockFilterConfig: SearchFilterConfig = Object.assign(new SearchFilterConfig(), {
|
||||||
|
name: filterName1,
|
||||||
|
type: FilterType.text,
|
||||||
|
hasFacets: false,
|
||||||
|
isOpenByDefault: false,
|
||||||
|
pageSize: 2
|
||||||
|
});
|
||||||
|
const value1 = 'random value';
|
||||||
|
// const value2 = 'another value';
|
||||||
|
const store: Store<SearchFiltersState> = jasmine.createSpyObj('store', {
|
||||||
|
/* tslint:disable:no-empty */
|
||||||
|
dispatch: {},
|
||||||
|
/* tslint:enable:no-empty */
|
||||||
|
select: Observable.of(true)
|
||||||
|
});
|
||||||
|
|
||||||
|
const routeServiceStub: any = {
|
||||||
|
/* tslint:disable:no-empty */
|
||||||
|
hasQueryParamWithValue: (param: string, value: string) => {
|
||||||
|
},
|
||||||
|
hasQueryParam: (param: string) => {
|
||||||
|
},
|
||||||
|
removeQueryParameterValue: (param: string, value: string) => {
|
||||||
|
},
|
||||||
|
addQueryParameterValue: (param: string, value: string) => {
|
||||||
|
},
|
||||||
|
getQueryParameterValues: (param: string) => {
|
||||||
|
}
|
||||||
|
/* tslint:enable:no-empty */
|
||||||
|
};
|
||||||
|
|
||||||
|
const searchServiceStub: any = {
|
||||||
|
searchLink: '/search'
|
||||||
|
};
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
service = new SearchFilterService(store, routeServiceStub, searchServiceStub);
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('when the initialCollapse method is triggered', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
service.initialCollapse(mockFilterConfig.name);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('SearchFilterInitialCollapseAction should be dispatched to the store', () => {
|
||||||
|
expect(store.dispatch).toHaveBeenCalledWith(new SearchFilterInitialCollapseAction(mockFilterConfig.name));
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('when the initialExpand method is triggered', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
service.initialExpand(mockFilterConfig.name);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('SearchFilterInitialExpandAction should be dispatched to the store', () => {
|
||||||
|
expect(store.dispatch).toHaveBeenCalledWith(new SearchFilterInitialExpandAction(mockFilterConfig.name));
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('when the collapse method is triggered', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
service.collapse(mockFilterConfig.name);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('SearchFilterCollapseAction should be dispatched to the store', () => {
|
||||||
|
expect(store.dispatch).toHaveBeenCalledWith(new SearchFilterCollapseAction(mockFilterConfig.name));
|
||||||
|
});
|
||||||
|
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('when the toggle method is triggered', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
service.toggle(mockFilterConfig.name);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('SearchFilterInitialExpandAction should be dispatched to the store', () => {
|
||||||
|
expect(store.dispatch).toHaveBeenCalledWith(new SearchFilterToggleAction(mockFilterConfig.name));
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('when the decreasePage method is triggered', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
service.decrementPage(mockFilterConfig.name);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('SearchFilterDecrementPageAction should be dispatched to the store', () => {
|
||||||
|
expect(store.dispatch).toHaveBeenCalledWith(new SearchFilterDecrementPageAction(mockFilterConfig.name));
|
||||||
|
});
|
||||||
|
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('when the increasePage method is triggered', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
service.incrementPage(mockFilterConfig.name);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('SearchFilterCollapseAction should be dispatched to the store', () => {
|
||||||
|
expect(store.dispatch).toHaveBeenCalledWith(new SearchFilterIncrementPageAction(mockFilterConfig.name));
|
||||||
|
});
|
||||||
|
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('when the resetPage method is triggered', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
service.resetPage(mockFilterConfig.name);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('SearchFilterDecrementPageAction should be dispatched to the store', () => {
|
||||||
|
expect(store.dispatch).toHaveBeenCalledWith(new SearchFilterResetPageAction(mockFilterConfig.name));
|
||||||
|
});
|
||||||
|
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('when the expand method is triggered', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
service.expand(mockFilterConfig.name);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('SearchSidebarExpandAction should be dispatched to the store', () => {
|
||||||
|
expect(store.dispatch).toHaveBeenCalledWith(new SearchFilterExpandAction(mockFilterConfig.name));
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('when the isFilterActiveWithValue method is called', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
spyOn(routeServiceStub, 'hasQueryParamWithValue');
|
||||||
|
service.isFilterActiveWithValue(mockFilterConfig.paramName, value1);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should call hasQueryParamWithValue on the route service with the same parameters', () => {
|
||||||
|
expect(routeServiceStub.hasQueryParamWithValue).toHaveBeenCalledWith(mockFilterConfig.paramName, value1);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('when the isFilterActive method is called', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
spyOn(routeServiceStub, 'hasQueryParam');
|
||||||
|
service.isFilterActive(mockFilterConfig.paramName);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should call hasQueryParam on the route service with the same parameters', () => {
|
||||||
|
expect(routeServiceStub.hasQueryParam).toHaveBeenCalledWith(mockFilterConfig.paramName);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('when the getQueryParamsWithout method is called', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
spyOn(routeServiceStub, 'removeQueryParameterValue');
|
||||||
|
service.getQueryParamsWithout(mockFilterConfig, value1);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should call removeQueryParameterValue on the route service with the same parameters', () => {
|
||||||
|
expect(routeServiceStub.removeQueryParameterValue).toHaveBeenCalledWith(mockFilterConfig.paramName, value1);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('when the getQueryParamsWith method is called', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
spyOn(routeServiceStub, 'addQueryParameterValue');
|
||||||
|
service.getQueryParamsWith(mockFilterConfig, value1);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should call addQueryParameterValue on the route service with the same parameters', () => {
|
||||||
|
expect(routeServiceStub.addQueryParameterValue).toHaveBeenCalledWith(mockFilterConfig.paramName, value1);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('when the getSelectedValuesForFilter method is called', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
spyOn(routeServiceStub, 'getQueryParameterValues');
|
||||||
|
service.getSelectedValuesForFilter(mockFilterConfig);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should call getQueryParameterValues on the route service with the same parameters', () => {
|
||||||
|
expect(routeServiceStub.getQueryParameterValues).toHaveBeenCalledWith(mockFilterConfig.paramName);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('when the searchLink method is called', () => {
|
||||||
|
let link: string;
|
||||||
|
beforeEach(() => {
|
||||||
|
link = service.searchLink;
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return the value of searchLink in the search service', () => {
|
||||||
|
expect(link).toEqual(searchServiceStub.searchLink);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
@@ -0,0 +1,119 @@
|
|||||||
|
import { Injectable } from '@angular/core';
|
||||||
|
import { SearchFiltersState, SearchFilterState } from './search-filter.reducer';
|
||||||
|
import { createSelector, MemoizedSelector, Store } from '@ngrx/store';
|
||||||
|
import { Observable } from 'rxjs/Observable';
|
||||||
|
import {
|
||||||
|
SearchFilterCollapseAction,
|
||||||
|
SearchFilterDecrementPageAction, SearchFilterExpandAction,
|
||||||
|
SearchFilterIncrementPageAction,
|
||||||
|
SearchFilterInitialCollapseAction,
|
||||||
|
SearchFilterInitialExpandAction, SearchFilterResetPageAction,
|
||||||
|
SearchFilterToggleAction
|
||||||
|
} from './search-filter.actions';
|
||||||
|
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';
|
||||||
|
|
||||||
|
const filterStateSelector = (state: SearchFiltersState) => state.searchFilter;
|
||||||
|
|
||||||
|
@Injectable()
|
||||||
|
export class SearchFilterService {
|
||||||
|
|
||||||
|
constructor(private store: Store<SearchFiltersState>,
|
||||||
|
private routeService: RouteService,
|
||||||
|
private searchService: SearchService) {
|
||||||
|
}
|
||||||
|
|
||||||
|
isFilterActiveWithValue(paramName: string, filterValue: string): Observable<boolean> {
|
||||||
|
return this.routeService.hasQueryParamWithValue(paramName, filterValue);
|
||||||
|
}
|
||||||
|
|
||||||
|
isFilterActive(paramName: string): Observable<boolean> {
|
||||||
|
return this.routeService.hasQueryParam(paramName);
|
||||||
|
}
|
||||||
|
|
||||||
|
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() {
|
||||||
|
return this.searchService.searchLink;
|
||||||
|
}
|
||||||
|
|
||||||
|
isCollapsed(filterName: string): Observable<boolean> {
|
||||||
|
return this.store.select(filterByNameSelector(filterName))
|
||||||
|
.map((object: SearchFilterState) => {
|
||||||
|
if (object) {
|
||||||
|
return object.filterCollapsed;
|
||||||
|
} else {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
getPage(filterName: string): Observable<number> {
|
||||||
|
return this.store.select(filterByNameSelector(filterName))
|
||||||
|
.map((object: SearchFilterState) => {
|
||||||
|
if (object) {
|
||||||
|
return object.page;
|
||||||
|
} else {
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
public collapse(filterName: string): void {
|
||||||
|
this.store.dispatch(new SearchFilterCollapseAction(filterName));
|
||||||
|
}
|
||||||
|
|
||||||
|
public expand(filterName: string): void {
|
||||||
|
this.store.dispatch(new SearchFilterExpandAction(filterName));
|
||||||
|
}
|
||||||
|
|
||||||
|
public toggle(filterName: string): void {
|
||||||
|
this.store.dispatch(new SearchFilterToggleAction(filterName));
|
||||||
|
}
|
||||||
|
|
||||||
|
public initialCollapse(filterName: string): void {
|
||||||
|
this.store.dispatch(new SearchFilterInitialCollapseAction(filterName));
|
||||||
|
}
|
||||||
|
|
||||||
|
public initialExpand(filterName: string): void {
|
||||||
|
this.store.dispatch(new SearchFilterInitialExpandAction(filterName));
|
||||||
|
}
|
||||||
|
|
||||||
|
public decrementPage(filterName: string): void {
|
||||||
|
this.store.dispatch(new SearchFilterDecrementPageAction(filterName));
|
||||||
|
}
|
||||||
|
|
||||||
|
public incrementPage(filterName: string): void {
|
||||||
|
this.store.dispatch(new SearchFilterIncrementPageAction(filterName));
|
||||||
|
}
|
||||||
|
|
||||||
|
public resetPage(filterName: string): void {
|
||||||
|
this.store.dispatch(new SearchFilterResetPageAction(filterName));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function filterByNameSelector(name: string): MemoizedSelector<SearchFiltersState, SearchFilterState> {
|
||||||
|
return keySelector<SearchFilterState>(name);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function keySelector<T>(key: string): MemoizedSelector<SearchFiltersState, T> {
|
||||||
|
return createSelector(filterStateSelector, (state: SearchFilterState) => {
|
||||||
|
if (hasValue(state)) {
|
||||||
|
return state[key];
|
||||||
|
} else {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
@@ -0,0 +1,7 @@
|
|||||||
|
<h3>{{"search.filters.head" | translate}}</h3>
|
||||||
|
<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>
|
||||||
|
</div>
|
||||||
|
<a class="btn btn-primary" [routerLink]="[getSearchLink()]" [queryParams]="getClearFiltersQueryParams()" role="button">{{"search.filters.reset" | translate}}</a>
|
@@ -0,0 +1,2 @@
|
|||||||
|
@import '../../../styles/variables.scss';
|
||||||
|
@import '../../../styles/mixins.scss';
|
@@ -0,0 +1,69 @@
|
|||||||
|
import { ChangeDetectionStrategy, NO_ERRORS_SCHEMA } from '@angular/core';
|
||||||
|
|
||||||
|
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
|
||||||
|
import { RouterTestingModule } from '@angular/router/testing';
|
||||||
|
import { TranslateModule } from '@ngx-translate/core';
|
||||||
|
import { NoopAnimationsModule } from '@angular/platform-browser/animations';
|
||||||
|
import { SearchFiltersComponent } from './search-filters.component';
|
||||||
|
import { SearchService } from '../search-service/search.service';
|
||||||
|
import { Observable } from 'rxjs/Observable';
|
||||||
|
|
||||||
|
describe('SearchFiltersComponent', () => {
|
||||||
|
let comp: SearchFiltersComponent;
|
||||||
|
let fixture: ComponentFixture<SearchFiltersComponent>;
|
||||||
|
let searchService: SearchService;
|
||||||
|
const searchServiceStub = {
|
||||||
|
/* tslint:disable:no-empty */
|
||||||
|
getConfig: () =>
|
||||||
|
Observable.of({ hasSucceeded: true, payload: [] }),
|
||||||
|
getClearFiltersQueryParams: () => {
|
||||||
|
},
|
||||||
|
getSearchLink: () => {
|
||||||
|
}
|
||||||
|
/* tslint:enable:no-empty */
|
||||||
|
};
|
||||||
|
|
||||||
|
beforeEach(async(() => {
|
||||||
|
TestBed.configureTestingModule({
|
||||||
|
imports: [TranslateModule.forRoot(), RouterTestingModule.withRoutes([]), NoopAnimationsModule],
|
||||||
|
declarations: [SearchFiltersComponent],
|
||||||
|
providers: [
|
||||||
|
{ provide: SearchService, useValue: searchServiceStub },
|
||||||
|
|
||||||
|
],
|
||||||
|
schemas: [NO_ERRORS_SCHEMA]
|
||||||
|
}).overrideComponent(SearchFiltersComponent, {
|
||||||
|
set: { changeDetection: ChangeDetectionStrategy.Default }
|
||||||
|
}).compileComponents();
|
||||||
|
}));
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
fixture = TestBed.createComponent(SearchFiltersComponent);
|
||||||
|
comp = fixture.componentInstance; // SearchFiltersComponent test instance
|
||||||
|
fixture.detectChanges();
|
||||||
|
searchService = (comp as any).searchService;
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('when the getClearFiltersQueryParams method is called', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
spyOn(searchService, 'getClearFiltersQueryParams');
|
||||||
|
comp.getClearFiltersQueryParams();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should call getClearFiltersQueryParams on the searchService', () => {
|
||||||
|
expect(searchService.getClearFiltersQueryParams).toHaveBeenCalled()
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('when the getSearchLink method is called', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
spyOn(searchService, 'getSearchLink');
|
||||||
|
comp.getSearchLink();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should call getSearchLink on the searchService', () => {
|
||||||
|
expect(searchService.getSearchLink).toHaveBeenCalled()
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
});
|
@@ -0,0 +1,32 @@
|
|||||||
|
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';
|
||||||
|
import { Observable } from 'rxjs/Observable';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This component renders a simple item page.
|
||||||
|
* The route parameter 'id' is used to request the item it represents.
|
||||||
|
* All fields of the item that should be displayed, are defined in its template.
|
||||||
|
*/
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
selector: 'ds-search-filters',
|
||||||
|
styleUrls: ['./search-filters.component.scss'],
|
||||||
|
templateUrl: './search-filters.component.html',
|
||||||
|
})
|
||||||
|
|
||||||
|
export class SearchFiltersComponent {
|
||||||
|
filters: Observable<RemoteData<SearchFilterConfig[]>>;
|
||||||
|
constructor(private searchService: SearchService) {
|
||||||
|
this.filters = searchService.getConfig();
|
||||||
|
}
|
||||||
|
|
||||||
|
getClearFiltersQueryParams(): any {
|
||||||
|
return this.searchService.getClearFiltersQueryParams();
|
||||||
|
}
|
||||||
|
|
||||||
|
getSearchLink() {
|
||||||
|
return this.searchService.getSearchLink();
|
||||||
|
}
|
||||||
|
}
|
@@ -1,37 +1,41 @@
|
|||||||
<div class="container">
|
<div class="container">
|
||||||
<div class="search-page">
|
<div class="search-page row">
|
||||||
<ds-search-sidebar dsStick *ngIf="!(isMobileView | async)" class="col-3 sidebar-sm-fixed"
|
<ds-search-sidebar *ngIf="!(isMobileView | async)" class="col-3 sidebar-md-sticky"
|
||||||
id="search-sidebar"
|
id="search-sidebar"
|
||||||
resultCount="{{(resultsRDObs | async)?.pageInfo?.totalElements}}"></ds-search-sidebar>
|
[resultCount]="(resultsRDObs | async)?.pageInfo?.totalElements"></ds-search-sidebar>
|
||||||
<div id="search-header" class="row">
|
<div class="col-12 col-md-9">
|
||||||
<ds-search-form id="search-form" class="col-12 col-sm-9 ml-sm-auto"
|
<ds-search-form id="search-form"
|
||||||
[query]="query"
|
[query]="query"
|
||||||
[scope]="(scopeObjectRDObs | async)?.payload"
|
[scope]="(scopeObjectRDObs | async)?.payload"
|
||||||
[currentParams]="currentParams"
|
[currentParams]="currentParams"
|
||||||
[scopes]="(scopeListRDObs | async)?.payload">
|
[scopes]="(scopeListRDObs | async)?.payload">
|
||||||
</ds-search-form>
|
</ds-search-form>
|
||||||
</div>
|
<div class="row">
|
||||||
<div class="row">
|
<div id="search-body"
|
||||||
<div id="search-body"
|
class="row-offcanvas row-offcanvas-left"
|
||||||
class="row-offcanvas row-offcanvas-left"
|
[@pushInOut]="(isSidebarCollapsed() | async) ? 'collapsed' : 'expanded'">
|
||||||
[@slideInOut]="(isSidebarCollapsed() | async) ? 'collapsed' : 'expanded'">
|
<ds-search-sidebar *ngIf="(isMobileView | async)" class="col-12"
|
||||||
<ds-search-sidebar *ngIf="(isMobileView | async)" class="col-12" id="search-sidebar-xs"
|
id="search-sidebar-sm"
|
||||||
resultCount="{{(resultsRDObs | async)?.pageInfo?.totalElements}}"
|
[resultCount]="(resultsRDObs | async)?.pageInfo?.totalElements"
|
||||||
(toggleSidebar)="closeSidebar()" [ngClass]="{'active': !(isSidebarCollapsed() | async)}"></ds-search-sidebar>
|
(toggleSidebar)="closeSidebar()"
|
||||||
<div id="search-content" class="col-12 col-sm-9 ml-sm-auto">
|
[ngClass]="{'active': !(isSidebarCollapsed() | async)}">
|
||||||
<div class="d-block d-sm-none search-controls clearfix">
|
</ds-search-sidebar>
|
||||||
|
<div id="search-content" class="col-12">
|
||||||
<ds-view-mode-switch></ds-view-mode-switch>
|
<div class="d-block d-md-none search-controls clearfix">
|
||||||
<button (click)="openSidebar()" aria-controls="#search-body"
|
<ds-view-mode-switch></ds-view-mode-switch>
|
||||||
class="btn btn-outline-primary float-right open-sidebar"><i
|
<button (click)="openSidebar()" aria-controls="#search-body"
|
||||||
class="fa fa-sliders"></i> {{"search.sidebar.open"
|
class="btn btn-outline-primary float-right open-sidebar"><i
|
||||||
| translate}}
|
class="fa fa-sliders"></i> {{"search.sidebar.open"
|
||||||
</button>
|
| translate}}
|
||||||
</div>
|
</button>
|
||||||
<ds-search-results [searchResults]="resultsRDObs | async"
|
|
||||||
[searchConfig]="searchOptions"></ds-search-results>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
|
<ds-search-results [searchResults]="resultsRDObs | async"
|
||||||
|
[searchConfig]="searchOptions"></ds-search-results>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
||||||
|
@@ -1,14 +1,10 @@
|
|||||||
@import '../../styles/variables.scss';
|
@import '../../styles/variables.scss';
|
||||||
@import '../../styles/mixins.scss';
|
@import '../../styles/mixins.scss';
|
||||||
|
|
||||||
#search-body {
|
@include media-breakpoint-down(md) {
|
||||||
position: relative;
|
.container {
|
||||||
}
|
width: 100%;
|
||||||
|
max-width: none;
|
||||||
#search-content, #search-form {
|
|
||||||
display: block;
|
|
||||||
@include media-breakpoint-down(xs) {
|
|
||||||
margin-left: 0;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -20,22 +16,22 @@
|
|||||||
&.row-offcanvas {
|
&.row-offcanvas {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
}
|
}
|
||||||
@include media-breakpoint-down(xs) {
|
@include media-breakpoint-down(sm) {
|
||||||
position: relative;
|
position: relative;
|
||||||
|
|
||||||
&.row-offcanvas {
|
&.row-offcanvas {
|
||||||
position: relative;
|
position: relative;
|
||||||
}
|
}
|
||||||
|
|
||||||
&.row-offcanvas-right #search-sidebar-xs {
|
&.row-offcanvas-right #search-sidebar-sm {
|
||||||
right: -100%;
|
right: -100%;
|
||||||
}
|
}
|
||||||
|
|
||||||
&.row-offcanvas-left #search-sidebar-xs {
|
&.row-offcanvas-left #search-sidebar-sm {
|
||||||
left: -100%;
|
left: -100%;
|
||||||
}
|
}
|
||||||
|
|
||||||
#search-sidebar-xs {
|
#search-sidebar-sm {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
top: 0;
|
top: 0;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
@@ -43,15 +39,16 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.sidebar-sm-fixed {
|
@include media-breakpoint-up(md) {
|
||||||
@include media-breakpoint-up(sm) {
|
.sidebar-md-sticky {
|
||||||
position: absolute;
|
position: sticky;
|
||||||
margin-top: -$content-spacing;
|
position: -webkit-sticky;
|
||||||
|
top: 0;
|
||||||
|
z-index: $zindex-sticky;
|
||||||
padding-top: $content-spacing;
|
padding-top: $content-spacing;
|
||||||
&.stick {
|
margin-top: -$content-spacing;
|
||||||
top: 0;
|
align-self: flex-start;
|
||||||
margin-top: 0px;
|
display: block;
|
||||||
position: fixed;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@@ -28,8 +28,14 @@ describe('SearchPageComponent', () => {
|
|||||||
/* tslint:enable:no-empty */
|
/* tslint:enable:no-empty */
|
||||||
select: Observable.of(true)
|
select: Observable.of(true)
|
||||||
});
|
});
|
||||||
|
const pagination: PaginationComponentOptions = new PaginationComponentOptions();
|
||||||
|
pagination.id = 'search-results-pagination';
|
||||||
|
pagination.currentPage = 1;
|
||||||
|
pagination.pageSize = 10;
|
||||||
|
const sort: SortOptions = new SortOptions();
|
||||||
const mockResults = Observable.of(['test', 'data']);
|
const mockResults = Observable.of(['test', 'data']);
|
||||||
const searchServiceStub = {
|
const searchServiceStub = {
|
||||||
|
searchOptions:{ pagination: pagination, sort: sort },
|
||||||
search: () => mockResults
|
search: () => mockResults
|
||||||
};
|
};
|
||||||
const queryParam = 'test query';
|
const queryParam = 'test query';
|
||||||
@@ -151,7 +157,7 @@ describe('SearchPageComponent', () => {
|
|||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
spyOn(comp, 'closeSidebar');
|
spyOn(comp, 'closeSidebar');
|
||||||
const closeSidebarButton = fixture.debugElement.query(By.css('#search-sidebar-xs'));
|
const closeSidebarButton = fixture.debugElement.query(By.css('#search-sidebar-sm'));
|
||||||
closeSidebarButton.triggerEventHandler('toggleSidebar', null);
|
closeSidebarButton.triggerEventHandler('toggleSidebar', null);
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -179,7 +185,7 @@ describe('SearchPageComponent', () => {
|
|||||||
let menu: HTMLElement;
|
let menu: HTMLElement;
|
||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
menu = fixture.debugElement.query(By.css('#search-sidebar-xs')).nativeElement;
|
menu = fixture.debugElement.query(By.css('#search-sidebar-sm')).nativeElement;
|
||||||
comp.isSidebarCollapsed = () => Observable.of(true);
|
comp.isSidebarCollapsed = () => Observable.of(true);
|
||||||
fixture.detectChanges();
|
fixture.detectChanges();
|
||||||
});
|
});
|
||||||
@@ -194,7 +200,7 @@ describe('SearchPageComponent', () => {
|
|||||||
let menu: HTMLElement;
|
let menu: HTMLElement;
|
||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
menu = fixture.debugElement.query(By.css('#search-sidebar-xs')).nativeElement;
|
menu = fixture.debugElement.query(By.css('#search-sidebar-sm')).nativeElement;
|
||||||
comp.isSidebarCollapsed = () => Observable.of(false);
|
comp.isSidebarCollapsed = () => Observable.of(false);
|
||||||
fixture.detectChanges();
|
fixture.detectChanges();
|
||||||
});
|
});
|
||||||
|
@@ -1,17 +1,15 @@
|
|||||||
import { ChangeDetectionStrategy, Component, OnDestroy, OnInit } from '@angular/core';
|
import { ChangeDetectionStrategy, Component, OnDestroy, OnInit } from '@angular/core';
|
||||||
import { ActivatedRoute } from '@angular/router';
|
import { ActivatedRoute } from '@angular/router';
|
||||||
import { Observable } from 'rxjs/Observable';
|
import { Observable } from 'rxjs/Observable';
|
||||||
import { SortOptions } from '../core/cache/models/sort-options.model';
|
|
||||||
import { CommunityDataService } from '../core/data/community-data.service';
|
import { CommunityDataService } from '../core/data/community-data.service';
|
||||||
import { RemoteData } from '../core/data/remote-data';
|
import { RemoteData } from '../core/data/remote-data';
|
||||||
import { Community } from '../core/shared/community.model';
|
import { Community } from '../core/shared/community.model';
|
||||||
import { DSpaceObject } from '../core/shared/dspace-object.model';
|
import { DSpaceObject } from '../core/shared/dspace-object.model';
|
||||||
import { isNotEmpty } from '../shared/empty.util';
|
import { isNotEmpty } from '../shared/empty.util';
|
||||||
import { PaginationComponentOptions } from '../shared/pagination/pagination-component-options.model';
|
|
||||||
import { SearchOptions } from './search-options.model';
|
import { SearchOptions } from './search-options.model';
|
||||||
import { SearchResult } from './search-result.model';
|
import { SearchResult } from './search-result.model';
|
||||||
import { SearchService } from './search-service/search.service';
|
import { SearchService } from './search-service/search.service';
|
||||||
import { slideInOut } from '../shared/animations/slide';
|
import { pushInOut } from '../shared/animations/push';
|
||||||
import { HostWindowService } from '../shared/host-window.service';
|
import { HostWindowService } from '../shared/host-window.service';
|
||||||
import { SearchSidebarService } from './search-sidebar/search-sidebar.service';
|
import { SearchSidebarService } from './search-sidebar/search-sidebar.service';
|
||||||
|
|
||||||
@@ -26,7 +24,7 @@ import { SearchSidebarService } from './search-sidebar/search-sidebar.service';
|
|||||||
styleUrls: ['./search-page.component.scss'],
|
styleUrls: ['./search-page.component.scss'],
|
||||||
templateUrl: './search-page.component.html',
|
templateUrl: './search-page.component.html',
|
||||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||||
animations: [slideInOut]
|
animations: [pushInOut]
|
||||||
})
|
})
|
||||||
export class SearchPageComponent implements OnInit, OnDestroy {
|
export class SearchPageComponent implements OnInit, OnDestroy {
|
||||||
|
|
||||||
@@ -46,15 +44,14 @@ export class SearchPageComponent implements OnInit, OnDestroy {
|
|||||||
private communityService: CommunityDataService,
|
private communityService: CommunityDataService,
|
||||||
private sidebarService: SearchSidebarService,
|
private sidebarService: SearchSidebarService,
|
||||||
private windowService: HostWindowService) {
|
private windowService: HostWindowService) {
|
||||||
this.isMobileView = this.windowService.isXs();
|
this.isMobileView = Observable.combineLatest(
|
||||||
|
this.windowService.isXs(),
|
||||||
|
this.windowService.isSm(),
|
||||||
|
((isXs, isSm) => isXs || isSm)
|
||||||
|
);
|
||||||
this.scopeListRDObs = communityService.findAll();
|
this.scopeListRDObs = communityService.findAll();
|
||||||
// Initial pagination config
|
// Initial pagination config
|
||||||
const pagination: PaginationComponentOptions = new PaginationComponentOptions();
|
this.searchOptions = this.service.searchOptions;
|
||||||
pagination.id = 'search-results-pagination';
|
|
||||||
pagination.currentPage = 1;
|
|
||||||
pagination.pageSize = 10;
|
|
||||||
const sort: SortOptions = new SortOptions();
|
|
||||||
this.searchOptions = { pagination: pagination, sort: sort };
|
|
||||||
}
|
}
|
||||||
|
|
||||||
ngOnInit(): void {
|
ngOnInit(): void {
|
||||||
@@ -90,7 +87,6 @@ export class SearchPageComponent implements OnInit, OnDestroy {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private updateSearchResults(searchOptions) {
|
private updateSearchResults(searchOptions) {
|
||||||
// Resolve search results
|
|
||||||
this.resultsRDObs = this.service.search(this.query, this.scope, searchOptions);
|
this.resultsRDObs = this.service.search(this.query, this.scope, searchOptions);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@@ -11,7 +11,12 @@ import { SearchService } from './search-service/search.service';
|
|||||||
import { SearchSidebarComponent } from './search-sidebar/search-sidebar.component';
|
import { SearchSidebarComponent } from './search-sidebar/search-sidebar.component';
|
||||||
import { SearchSidebarService } from './search-sidebar/search-sidebar.service';
|
import { SearchSidebarService } from './search-sidebar/search-sidebar.service';
|
||||||
import { SearchSidebarEffects } from './search-sidebar/search-sidebar.effects';
|
import { SearchSidebarEffects } from './search-sidebar/search-sidebar.effects';
|
||||||
|
import { SearchSettingsComponent } from './search-settings/search-settings.component';
|
||||||
import { EffectsModule } from '@ngrx/effects';
|
import { EffectsModule } from '@ngrx/effects';
|
||||||
|
import { SearchFiltersComponent } from './search-filters/search-filters.component';
|
||||||
|
import { SearchFilterComponent } from './search-filters/search-filter/search-filter.component';
|
||||||
|
import { SearchFacetFilterComponent } from './search-filters/search-filter/search-facet-filter/search-facet-filter.component';
|
||||||
|
import { SearchFilterService } from './search-filters/search-filter/search-filter.service';
|
||||||
|
|
||||||
const effects = [
|
const effects = [
|
||||||
SearchSidebarEffects
|
SearchSidebarEffects
|
||||||
@@ -28,13 +33,18 @@ const effects = [
|
|||||||
SearchPageComponent,
|
SearchPageComponent,
|
||||||
SearchResultsComponent,
|
SearchResultsComponent,
|
||||||
SearchSidebarComponent,
|
SearchSidebarComponent,
|
||||||
|
SearchSettingsComponent,
|
||||||
ItemSearchResultListElementComponent,
|
ItemSearchResultListElementComponent,
|
||||||
CollectionSearchResultListElementComponent,
|
CollectionSearchResultListElementComponent,
|
||||||
CommunitySearchResultListElementComponent
|
CommunitySearchResultListElementComponent,
|
||||||
|
SearchFiltersComponent,
|
||||||
|
SearchFilterComponent,
|
||||||
|
SearchFacetFilterComponent
|
||||||
],
|
],
|
||||||
providers: [
|
providers: [
|
||||||
SearchService,
|
SearchService,
|
||||||
SearchSidebarService
|
SearchSidebarService,
|
||||||
|
SearchFilterService
|
||||||
],
|
],
|
||||||
entryComponents: [
|
entryComponents: [
|
||||||
ItemSearchResultListElementComponent,
|
ItemSearchResultListElementComponent,
|
||||||
|
@@ -1,11 +1,10 @@
|
|||||||
<div *ngIf="searchResults?.hasSucceeded" @fadeIn>
|
<div *ngIf="searchResults?.hasSucceeded" @fadeIn>
|
||||||
<h2 *ngIf="searchResults?.payload?.length > 0">{{ 'search.results.title' | translate }}</h2>
|
<h2 *ngIf="searchResults?.payload ?.length > 0">{{ 'search.results.head' | translate }}</h2>
|
||||||
<ds-object-list
|
<ds-object-list
|
||||||
[config]="searchConfig.pagination"
|
[config]="searchConfig.pagination"
|
||||||
[sortConfig]="searchConfig.sort"
|
[sortConfig]="searchConfig.sort"
|
||||||
[objects]="searchResults"
|
[objects]="searchResults"
|
||||||
[hideGear]="false">
|
[hideGear]="true">
|
||||||
</ds-object-list>
|
</ds-object-list></div>
|
||||||
</div>
|
|
||||||
<ds-loading *ngIf="searchResults?.isLoading" message="{{'loading.search-results' | translate}}"></ds-loading>
|
<ds-loading *ngIf="searchResults?.isLoading" message="{{'loading.search-results' | translate}}"></ds-loading>
|
||||||
<ds-error *ngIf="searchResults?.hasFailed" message="{{'error.search-results' | translate}}"></ds-error>
|
<ds-error *ngIf="searchResults?.hasFailed" message="{{'error.search-results' | translate}}"></ds-error>
|
||||||
|
@@ -5,6 +5,7 @@ export class SearchFilterConfig {
|
|||||||
name: string;
|
name: string;
|
||||||
type: FilterType;
|
type: FilterType;
|
||||||
hasFacets: boolean;
|
hasFacets: boolean;
|
||||||
|
pageSize = 5;
|
||||||
isOpenByDefault: boolean;
|
isOpenByDefault: boolean;
|
||||||
/**
|
/**
|
||||||
* Name of this configuration that can be used in a url
|
* Name of this configuration that can be used in a url
|
||||||
|
@@ -3,11 +3,11 @@ import { RouterTestingModule } from '@angular/router/testing';
|
|||||||
|
|
||||||
import { CommonModule } from '@angular/common';
|
import { CommonModule } from '@angular/common';
|
||||||
import { Component } from '@angular/core';
|
import { Component } from '@angular/core';
|
||||||
import { Router } from '@angular/router';
|
|
||||||
|
|
||||||
import { SearchService } from './search.service';
|
import { SearchService } from './search.service';
|
||||||
import { ItemDataService } from './../../core/data/item-data.service';
|
import { ItemDataService } from './../../core/data/item-data.service';
|
||||||
import { ViewMode } from '../../+search-page/search-options.model';
|
import { ViewMode } from '../../+search-page/search-options.model';
|
||||||
|
import { RouteService } from '../../shared/route.service';
|
||||||
|
|
||||||
@Component({ template: '' })
|
@Component({ template: '' })
|
||||||
class DummyComponent { }
|
class DummyComponent { }
|
||||||
@@ -28,6 +28,7 @@ describe('SearchService', () => {
|
|||||||
],
|
],
|
||||||
providers: [
|
providers: [
|
||||||
{ provide: ItemDataService, useValue: {} },
|
{ provide: ItemDataService, useValue: {} },
|
||||||
|
{ provide: RouteService, useValue: {} },
|
||||||
SearchService
|
SearchService
|
||||||
],
|
],
|
||||||
});
|
});
|
||||||
|
@@ -1,4 +1,4 @@
|
|||||||
import { Injectable } from '@angular/core';
|
import { Injectable, OnDestroy } from '@angular/core';
|
||||||
import { RemoteData } from '../../core/data/remote-data';
|
import { RemoteData } from '../../core/data/remote-data';
|
||||||
import { Observable } from 'rxjs/Observable';
|
import { Observable } from 'rxjs/Observable';
|
||||||
import { SearchResult } from '../search-result.model';
|
import { SearchResult } from '../search-result.model';
|
||||||
@@ -15,6 +15,9 @@ import { FilterType } from './filter-type.model';
|
|||||||
import { FacetValue } from './facet-value.model';
|
import { FacetValue } from './facet-value.model';
|
||||||
import { ViewMode } from '../../+search-page/search-options.model';
|
import { ViewMode } from '../../+search-page/search-options.model';
|
||||||
import { Router, NavigationExtras, ActivatedRoute } from '@angular/router';
|
import { Router, NavigationExtras, ActivatedRoute } from '@angular/router';
|
||||||
|
import { RouteService } from '../../shared/route.service';
|
||||||
|
import { PaginationComponentOptions } from '../../shared/pagination/pagination-component-options.model';
|
||||||
|
import { SortOptions } from '../../core/cache/models/sort-options.model';
|
||||||
|
|
||||||
function shuffle(array: any[]) {
|
function shuffle(array: any[]) {
|
||||||
let i = 0;
|
let i = 0;
|
||||||
@@ -31,7 +34,7 @@ function shuffle(array: any[]) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class SearchService {
|
export class SearchService implements OnDestroy {
|
||||||
|
|
||||||
totalPages = 5;
|
totalPages = 5;
|
||||||
mockedHighlights: string[] = new Array(
|
mockedHighlights: string[] = new Array(
|
||||||
@@ -46,46 +49,58 @@ export class SearchService {
|
|||||||
'<em>This was blank in the actual item, no abstract</em>',
|
'<em>This was blank in the actual item, no abstract</em>',
|
||||||
'<em>The QSAR DataBank (QsarDB) repository</em>',
|
'<em>The QSAR DataBank (QsarDB) repository</em>',
|
||||||
);
|
);
|
||||||
|
private sub;
|
||||||
|
searchLink = '/search';
|
||||||
|
|
||||||
config: SearchFilterConfig[] = [
|
config: SearchFilterConfig[] = [
|
||||||
Object.assign(new SearchFilterConfig(),
|
Object.assign(new SearchFilterConfig(),
|
||||||
{
|
{
|
||||||
name: 'scope',
|
name: 'scope',
|
||||||
type: FilterType.hierarchy,
|
type: FilterType.hierarchy,
|
||||||
hasFacets: true,
|
hasFacets: true,
|
||||||
isOpenByDefault: true
|
isOpenByDefault: true
|
||||||
}),
|
}),
|
||||||
Object.assign(new SearchFilterConfig(),
|
Object.assign(new SearchFilterConfig(),
|
||||||
{
|
{
|
||||||
name: 'author',
|
name: 'author',
|
||||||
type: FilterType.text,
|
type: FilterType.text,
|
||||||
hasFacets: true,
|
hasFacets: true,
|
||||||
isOpenByDefault: false
|
isOpenByDefault: false
|
||||||
}),
|
}),
|
||||||
Object.assign(new SearchFilterConfig(),
|
Object.assign(new SearchFilterConfig(),
|
||||||
{
|
{
|
||||||
name: 'date',
|
name: 'date',
|
||||||
type: FilterType.range,
|
type: FilterType.range,
|
||||||
hasFacets: true,
|
hasFacets: true,
|
||||||
isOpenByDefault: false
|
isOpenByDefault: false
|
||||||
}),
|
}),
|
||||||
Object.assign(new SearchFilterConfig(),
|
Object.assign(new SearchFilterConfig(),
|
||||||
{
|
{
|
||||||
name: 'subject',
|
name: 'subject',
|
||||||
type: FilterType.text,
|
type: FilterType.text,
|
||||||
hasFacets: false,
|
hasFacets: false,
|
||||||
isOpenByDefault: false
|
isOpenByDefault: false
|
||||||
})
|
})
|
||||||
];
|
];
|
||||||
|
// searchOptions: BehaviorSubject<SearchOptions>;
|
||||||
|
searchOptions: SearchOptions;
|
||||||
|
|
||||||
constructor(
|
constructor(private itemDataService: ItemDataService,
|
||||||
private itemDataService: ItemDataService,
|
private routeService: RouteService,
|
||||||
private route: ActivatedRoute,
|
private route: ActivatedRoute,
|
||||||
private router: Router) {
|
private router: Router) {
|
||||||
|
|
||||||
|
const pagination: PaginationComponentOptions = new PaginationComponentOptions();
|
||||||
|
pagination.id = 'search-results-pagination';
|
||||||
|
pagination.currentPage = 1;
|
||||||
|
pagination.pageSize = 10;
|
||||||
|
const sort: SortOptions = new SortOptions();
|
||||||
|
this.searchOptions = { pagination: pagination, sort: sort };
|
||||||
|
// this.searchOptions = new BehaviorSubject<SearchOptions>(searchOptions);
|
||||||
}
|
}
|
||||||
|
|
||||||
search(query: string, scopeId?: string, searchOptions?: SearchOptions): Observable<RemoteData<Array<SearchResult<DSpaceObject>>>> {
|
search(query: string, scopeId?: string, searchOptions?: SearchOptions): Observable<RemoteData<Array<SearchResult<DSpaceObject>>>> {
|
||||||
|
this.searchOptions = this.searchOptions;
|
||||||
let self = `https://dspace7.4science.it/dspace-spring-rest/api/search?query=${query}`;
|
let self = `https://dspace7.4science.it/dspace-spring-rest/api/search?query=${query}`;
|
||||||
if (hasValue(scopeId)) {
|
if (hasValue(scopeId)) {
|
||||||
self += `&scope=${scopeId}`;
|
self += `&scope=${scopeId}`;
|
||||||
@@ -125,31 +140,31 @@ export class SearchService {
|
|||||||
.filter((rd: RemoteData<Item[]>) => rd.hasSucceeded)
|
.filter((rd: RemoteData<Item[]>) => rd.hasSucceeded)
|
||||||
.map((rd: RemoteData<Item[]>) => {
|
.map((rd: RemoteData<Item[]>) => {
|
||||||
|
|
||||||
const totalElements = rd.pageInfo.totalElements > 20 ? 20 : rd.pageInfo.totalElements;
|
const totalElements = rd.pageInfo.totalElements > 20 ? 20 : rd.pageInfo.totalElements;
|
||||||
const pageInfo = Object.assign({}, rd.pageInfo, { totalElements: totalElements });
|
const pageInfo = Object.assign({}, rd.pageInfo, { totalElements: totalElements });
|
||||||
|
|
||||||
const payload = shuffle(rd.payload)
|
const payload = shuffle(rd.payload)
|
||||||
.map((item: Item, index: number) => {
|
.map((item: Item, index: number) => {
|
||||||
const mockResult: SearchResult<DSpaceObject> = new ItemSearchResult();
|
const mockResult: SearchResult<DSpaceObject> = new ItemSearchResult();
|
||||||
mockResult.dspaceObject = item;
|
mockResult.dspaceObject = item;
|
||||||
const highlight = new Metadatum();
|
const highlight = new Metadatum();
|
||||||
highlight.key = 'dc.description.abstract';
|
highlight.key = 'dc.description.abstract';
|
||||||
highlight.value = this.mockedHighlights[index % this.mockedHighlights.length];
|
highlight.value = this.mockedHighlights[index % this.mockedHighlights.length];
|
||||||
mockResult.hitHighlights = new Array(highlight);
|
mockResult.hitHighlights = new Array(highlight);
|
||||||
return mockResult;
|
return mockResult;
|
||||||
});
|
});
|
||||||
|
|
||||||
return new RemoteData(
|
return new RemoteData(
|
||||||
self,
|
self,
|
||||||
rd.isRequestPending,
|
rd.isRequestPending,
|
||||||
rd.isResponsePending,
|
rd.isResponsePending,
|
||||||
rd.hasSucceeded,
|
rd.hasSucceeded,
|
||||||
errorMessage,
|
errorMessage,
|
||||||
statusCode,
|
statusCode,
|
||||||
pageInfo,
|
pageInfo,
|
||||||
payload
|
payload
|
||||||
)
|
)
|
||||||
}).startWith(new RemoteData(
|
}).startWith(new RemoteData(
|
||||||
'',
|
'',
|
||||||
true,
|
true,
|
||||||
false,
|
false,
|
||||||
@@ -181,31 +196,38 @@ export class SearchService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
getFacetValuesFor(searchFilterConfigName: string): Observable<RemoteData<FacetValue[]>> {
|
getFacetValuesFor(searchFilterConfigName: string): Observable<RemoteData<FacetValue[]>> {
|
||||||
const values: FacetValue[] = [];
|
const filterConfig = this.config.find((config: SearchFilterConfig) => config.name === searchFilterConfigName);
|
||||||
for (let i = 0; i < 5; i++) {
|
return this.routeService.getQueryParameterValues(filterConfig.paramName).map((selectedValues: string[]) => {
|
||||||
const value = searchFilterConfigName + ' ' + (i + 1);
|
const values: FacetValue[] = [];
|
||||||
values.push({
|
const totalFilters = 13;
|
||||||
value: value,
|
for (let i = 0; i < totalFilters; i++) {
|
||||||
count: Math.floor(Math.random() * 20) + 20 * (5 - i), // make sure first results have the highest (random) count
|
const value = searchFilterConfigName + ' ' + (i + 1);
|
||||||
search: 'https://dspace7.4science.it/dspace-spring-rest/api/search?f.' + searchFilterConfigName + '=' + encodeURI(value)
|
if (!selectedValues.includes(value)) {
|
||||||
});
|
values.push({
|
||||||
}
|
value: value,
|
||||||
const requestPending = false;
|
count: Math.floor(Math.random() * 20) + 20 * (totalFilters - i), // make sure first results have the highest (random) count
|
||||||
const responsePending = false;
|
search: decodeURI(this.router.url) + (this.router.url.includes('?') ? '&' : '?') + filterConfig.paramName + '=' + value
|
||||||
const isSuccessful = true;
|
});
|
||||||
const errorMessage = undefined;
|
}
|
||||||
const statusCode = '200';
|
}
|
||||||
const returningPageInfo = new PageInfo();
|
const requestPending = false;
|
||||||
return Observable.of(new RemoteData(
|
const responsePending = false;
|
||||||
'https://dspace7.4science.it/dspace-spring-rest/api/search',
|
const isSuccessful = true;
|
||||||
requestPending,
|
const errorMessage = undefined;
|
||||||
responsePending,
|
const statusCode = '200';
|
||||||
isSuccessful,
|
const returningPageInfo = new PageInfo();
|
||||||
errorMessage,
|
return new RemoteData(
|
||||||
statusCode,
|
'https://dspace7.4science.it/dspace-spring-rest/api/search',
|
||||||
returningPageInfo,
|
requestPending,
|
||||||
values
|
responsePending,
|
||||||
));
|
isSuccessful,
|
||||||
|
errorMessage,
|
||||||
|
statusCode,
|
||||||
|
returningPageInfo,
|
||||||
|
values
|
||||||
|
)
|
||||||
|
}
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
getViewMode(): Observable<ViewMode> {
|
getViewMode(): Observable<ViewMode> {
|
||||||
@@ -220,10 +242,34 @@ export class SearchService {
|
|||||||
|
|
||||||
setViewMode(viewMode: ViewMode) {
|
setViewMode(viewMode: ViewMode) {
|
||||||
const navigationExtras: NavigationExtras = {
|
const navigationExtras: NavigationExtras = {
|
||||||
queryParams: {view: viewMode},
|
queryParams: { view: viewMode },
|
||||||
queryParamsHandling: 'merge'
|
queryParamsHandling: 'merge'
|
||||||
};
|
};
|
||||||
|
|
||||||
this.router.navigate(['/search'], navigationExtras);
|
this.router.navigate([this.searchLink], navigationExtras);
|
||||||
|
}
|
||||||
|
|
||||||
|
getClearFiltersQueryParams(): any {
|
||||||
|
const params = {};
|
||||||
|
this.sub = this.route.queryParamMap
|
||||||
|
.subscribe((map) => {
|
||||||
|
map.keys
|
||||||
|
.filter((key) => this.config
|
||||||
|
.findIndex((conf: SearchFilterConfig) => conf.paramName === key) < 0)
|
||||||
|
.forEach((key) => {
|
||||||
|
params[key] = map.get(key);
|
||||||
|
})
|
||||||
|
});
|
||||||
|
return params;
|
||||||
|
}
|
||||||
|
|
||||||
|
getSearchLink() {
|
||||||
|
return this.searchLink;
|
||||||
|
}
|
||||||
|
|
||||||
|
ngOnDestroy(): void {
|
||||||
|
if (this.sub !== undefined) {
|
||||||
|
this.sub.unsubscribe();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@@ -0,0 +1,22 @@
|
|||||||
|
<h3>{{ 'search.sidebar.settings.title' | translate}}</h3>
|
||||||
|
<div *ngIf="[searchOptions].sort" class="setting-option result-order-settings mb-3 p-3">
|
||||||
|
<h5>{{ 'search.sidebar.settings.sort-by' | translate}}</h5>
|
||||||
|
<select class="form-control" (change)="reloadOrder($event)">
|
||||||
|
<option *ngFor="let direction of (sortDirections | dsKeys); let currentElementIndex = index"
|
||||||
|
[value]="currentElementIndex"
|
||||||
|
[selected]="direction === searchOptions.sort? 'selected': null">
|
||||||
|
{{direction.value}}
|
||||||
|
</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div *ngIf="searchOptions.pagination.pageSize" class="setting-option page-size-settings mb-3 p-3">
|
||||||
|
<h5>{{ 'search.sidebar.settings.rpp' | translate}}</h5>
|
||||||
|
|
||||||
|
<select class="form-control" (change)="reloadRPP($event)">
|
||||||
|
<option *ngFor="let item of searchOptions.pagination.pageSizeOptions" [value]="item"
|
||||||
|
[selected]="item === searchOptions.pagination.pageSize ? 'selected': null">
|
||||||
|
{{item}}
|
||||||
|
</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
@@ -0,0 +1,5 @@
|
|||||||
|
@import '../../../styles/variables.scss';
|
||||||
|
|
||||||
|
.setting-option {
|
||||||
|
border: 1px solid map-get($theme-colors, light);
|
||||||
|
}
|
@@ -0,0 +1,103 @@
|
|||||||
|
import { SearchService } from '../search-service/search.service';
|
||||||
|
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
|
||||||
|
import { SearchSettingsComponent } from './search-settings.component';
|
||||||
|
import { Observable } from 'rxjs/Observable';
|
||||||
|
import { PaginationComponentOptions } from '../../shared/pagination/pagination-component-options.model';
|
||||||
|
import { SortOptions } from '../../core/cache/models/sort-options.model';
|
||||||
|
import { TranslateModule } from '@ngx-translate/core';
|
||||||
|
import { RouterTestingModule } from '@angular/router/testing';
|
||||||
|
import { ActivatedRoute } from '@angular/router';
|
||||||
|
import { SearchSidebarService } from '../search-sidebar/search-sidebar.service';
|
||||||
|
import { NO_ERRORS_SCHEMA } from '@angular/core';
|
||||||
|
import { EnumKeysPipe } from '../../shared/utils/enum-keys-pipe';
|
||||||
|
import { By } from '@angular/platform-browser';
|
||||||
|
|
||||||
|
describe('SearchSettingsComponent', () => {
|
||||||
|
|
||||||
|
let comp: SearchSettingsComponent;
|
||||||
|
let fixture: ComponentFixture<SearchSettingsComponent>;
|
||||||
|
let searchServiceObject: SearchService;
|
||||||
|
|
||||||
|
const pagination: PaginationComponentOptions = new PaginationComponentOptions();
|
||||||
|
pagination.id = 'search-results-pagination';
|
||||||
|
pagination.currentPage = 1;
|
||||||
|
pagination.pageSize = 10;
|
||||||
|
const sort: SortOptions = new SortOptions();
|
||||||
|
const mockResults = [ 'test', 'data' ];
|
||||||
|
const searchServiceStub = {
|
||||||
|
searchOptions: { pagination: pagination, sort: sort },
|
||||||
|
search: () => mockResults
|
||||||
|
};
|
||||||
|
const queryParam = 'test query';
|
||||||
|
const scopeParam = '7669c72a-3f2a-451f-a3b9-9210e7a4c02f';
|
||||||
|
const activatedRouteStub = {
|
||||||
|
queryParams: Observable.of({
|
||||||
|
query: queryParam,
|
||||||
|
scope: scopeParam
|
||||||
|
})
|
||||||
|
};
|
||||||
|
|
||||||
|
const sidebarService = {
|
||||||
|
isCollapsed: Observable.of(true),
|
||||||
|
collapse: () => this.isCollapsed = Observable.of(true),
|
||||||
|
expand: () => this.isCollapsed = Observable.of(false)
|
||||||
|
}
|
||||||
|
|
||||||
|
beforeEach(async(() => {
|
||||||
|
TestBed.configureTestingModule({
|
||||||
|
imports: [ TranslateModule.forRoot(), RouterTestingModule.withRoutes([]) ],
|
||||||
|
declarations: [ SearchSettingsComponent, EnumKeysPipe ],
|
||||||
|
providers: [
|
||||||
|
{ provide: SearchService, useValue: searchServiceStub },
|
||||||
|
|
||||||
|
{ provide: ActivatedRoute, useValue: activatedRouteStub },
|
||||||
|
{
|
||||||
|
provide: SearchSidebarService,
|
||||||
|
useValue: sidebarService
|
||||||
|
},
|
||||||
|
],
|
||||||
|
schemas: [ NO_ERRORS_SCHEMA ]
|
||||||
|
}).compileComponents();
|
||||||
|
}));
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
fixture = TestBed.createComponent(SearchSettingsComponent);
|
||||||
|
comp = fixture.componentInstance;
|
||||||
|
|
||||||
|
// SearchPageComponent test instance
|
||||||
|
fixture.detectChanges();
|
||||||
|
searchServiceObject = (comp as any).service;
|
||||||
|
spyOn(comp, 'reloadRPP');
|
||||||
|
spyOn(comp, 'reloadOrder');
|
||||||
|
spyOn(searchServiceObject, 'search').and.callThrough();
|
||||||
|
|
||||||
|
});
|
||||||
|
|
||||||
|
it('it should show the order settings with the respective selectable options', () => {
|
||||||
|
const orderSetting = fixture.debugElement.query(By.css('div.result-order-settings'));
|
||||||
|
expect(orderSetting).toBeDefined();
|
||||||
|
const childElements = orderSetting.query(By.css('.form-control')).children;
|
||||||
|
expect(childElements.length).toEqual(2);
|
||||||
|
|
||||||
|
});
|
||||||
|
|
||||||
|
it('it should show the size settings with the respective selectable options', () => {
|
||||||
|
const pageSizeSetting = fixture.debugElement.query(By.css('div.page-size-settings'));
|
||||||
|
expect(pageSizeSetting).toBeDefined();
|
||||||
|
const childElements = pageSizeSetting.query(By.css('.form-control')).children;
|
||||||
|
expect(childElements.length).toEqual(7);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should have the proper order value selected by default', () => {
|
||||||
|
const orderSetting = fixture.debugElement.query(By.css('div.result-order-settings'));
|
||||||
|
const childElementToBeSelected = orderSetting.query(By.css('.form-control option[value="0"][selected="selected"]'))
|
||||||
|
expect(childElementToBeSelected).toBeDefined();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should have the proper rpp value selected by default', () => {
|
||||||
|
const pageSizeSetting = fixture.debugElement.query(By.css('div.page-size-settings'));
|
||||||
|
const childElementToBeSelected = pageSizeSetting.query(By.css('.form-control option[value="10"][selected="selected"]'))
|
||||||
|
expect(childElementToBeSelected).toBeDefined();
|
||||||
|
});
|
||||||
|
|
||||||
|
});
|
@@ -0,0 +1,70 @@
|
|||||||
|
import { Component, Input, OnInit } from '@angular/core';
|
||||||
|
import { SearchService } from '../search-service/search.service';
|
||||||
|
import { SearchOptions } from '../search-options.model';
|
||||||
|
import { SortDirection } from '../../core/cache/models/sort-options.model';
|
||||||
|
import { ActivatedRoute, NavigationExtras, Router } from '@angular/router';
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
selector: 'ds-search-settings',
|
||||||
|
styleUrls: ['./search-settings.component.scss'],
|
||||||
|
templateUrl: './search-settings.component.html',
|
||||||
|
})
|
||||||
|
export class SearchSettingsComponent implements OnInit {
|
||||||
|
|
||||||
|
@Input() searchOptions: SearchOptions;
|
||||||
|
/**
|
||||||
|
* Declare SortDirection enumeration to use it in the template
|
||||||
|
*/
|
||||||
|
public sortDirections = SortDirection;
|
||||||
|
/**
|
||||||
|
* Number of items per page.
|
||||||
|
*/
|
||||||
|
public pageSize;
|
||||||
|
|
||||||
|
private sub;
|
||||||
|
private scope: string;
|
||||||
|
query: string;
|
||||||
|
page: number;
|
||||||
|
direction: SortDirection;
|
||||||
|
currentParams = {};
|
||||||
|
|
||||||
|
constructor(private service: SearchService,
|
||||||
|
private route: ActivatedRoute,
|
||||||
|
private router: Router) {
|
||||||
|
}
|
||||||
|
|
||||||
|
ngOnInit(): void {
|
||||||
|
this.searchOptions = this.service.searchOptions;
|
||||||
|
this.pageSize = this.searchOptions.pagination.pageSize;
|
||||||
|
this.sub = this.route
|
||||||
|
.queryParams
|
||||||
|
.subscribe((params) => {
|
||||||
|
this.currentParams = params;
|
||||||
|
this.query = params.query || '';
|
||||||
|
this.scope = params.scope;
|
||||||
|
this.page = +params.page || this.searchOptions.pagination.currentPage;
|
||||||
|
this.pageSize = +params.pageSize || this.searchOptions.pagination.pageSize;
|
||||||
|
this.direction = +params.sortDirection || this.searchOptions.sort.direction;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
reloadRPP(event: Event) {
|
||||||
|
const value = (event.target as HTMLInputElement).value;
|
||||||
|
const navigationExtras: NavigationExtras = {
|
||||||
|
queryParams: Object.assign({}, this.currentParams, {
|
||||||
|
pageSize: value
|
||||||
|
})
|
||||||
|
};
|
||||||
|
this.router.navigate([ '/search' ], navigationExtras);
|
||||||
|
}
|
||||||
|
|
||||||
|
reloadOrder(event: Event) {
|
||||||
|
const value = (event.target as HTMLInputElement).value;
|
||||||
|
const navigationExtras: NavigationExtras = {
|
||||||
|
queryParams: Object.assign({}, this.currentParams, {
|
||||||
|
sortDirection: value
|
||||||
|
})
|
||||||
|
};
|
||||||
|
this.router.navigate([ '/search' ], navigationExtras);
|
||||||
|
}
|
||||||
|
}
|
@@ -1,13 +1,17 @@
|
|||||||
<div>
|
<div>
|
||||||
<div id="sidebar-options" class="d-block d-sm-none search-controls clearfix">
|
<div id="sidebar-options" class="d-block d-md-none search-controls clearfix">
|
||||||
<small class="results">{{resultCount}} {{"search.sidebar.results" | translate}}</small>
|
<small class="results">{{resultCount}} {{"search.sidebar.results" | translate}}</small>
|
||||||
<button (click)="toggleSidebar.emit()"
|
<button (click)="toggleSidebar.emit()"
|
||||||
aria-controls="#search-body" class="btn btn-outline-primary float-right close-sidebar"><i
|
aria-controls="#search-body"
|
||||||
|
class="btn btn-outline-primary float-right close-sidebar"><i
|
||||||
class="fa fa-arrow-right"></i> {{"search.sidebar.close" | translate}}
|
class="fa fa-arrow-right"></i> {{"search.sidebar.close" | translate}}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
<div id="search-sidebar-content">
|
<div id="search-sidebar-content">
|
||||||
<ds-view-mode-switch class="d-none d-sm-block"></ds-view-mode-switch>
|
<ds-view-mode-switch class="d-none d-md-block"></ds-view-mode-switch>
|
||||||
Filters
|
<div class="sidebar-content">
|
||||||
|
<ds-search-filters></ds-search-filters>
|
||||||
|
<ds-search-settings></ds-search-settings>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@@ -5,4 +5,11 @@
|
|||||||
.results {
|
.results {
|
||||||
line-height: $button-height;
|
line-height: $button-height;
|
||||||
}
|
}
|
||||||
|
ds-view-mode-switch {
|
||||||
|
margin-bottom: $spacer;
|
||||||
|
}
|
||||||
|
.sidebar-content > *:not(:last-child) {
|
||||||
|
margin-bottom: 4*$spacer;
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@@ -9,6 +9,7 @@ import { SearchSidebarEffects } from './search-sidebar.effects';
|
|||||||
describe('SearchSidebarEffects', () => {
|
describe('SearchSidebarEffects', () => {
|
||||||
let sidebarEffects: SearchSidebarEffects;
|
let sidebarEffects: SearchSidebarEffects;
|
||||||
let actions: Observable<any>;
|
let actions: Observable<any>;
|
||||||
|
const dummyURL = 'http://f4fb15e2-1bd3-4e63-8d0d-486ad8bc714a';
|
||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
TestBed.configureTestingModule({
|
TestBed.configureTestingModule({
|
||||||
@@ -24,13 +25,12 @@ describe('SearchSidebarEffects', () => {
|
|||||||
|
|
||||||
describe('routeChange$', () => {
|
describe('routeChange$', () => {
|
||||||
|
|
||||||
it('should return a COLLAPSE action in response to an UPDATE_LOCATION action', () => {
|
it('should return a COLLAPSE action in response to an UPDATE_LOCATION action to a new route', () => {
|
||||||
actions = hot('--a-', { a: { type: fromRouter.ROUTER_NAVIGATION } });
|
actions = hot('--a-', { a: { type: fromRouter.ROUTER_NAVIGATION, payload: {routerState: {url: dummyURL}} } });
|
||||||
|
|
||||||
const expected = cold('--b-', { b: new SearchSidebarCollapseAction() });
|
const expected = cold('--b-', { b: new SearchSidebarCollapseAction() });
|
||||||
|
|
||||||
expect(sidebarEffects.routeChange$).toBeObservable(expected);
|
expect(sidebarEffects.routeChange$).toBeObservable(expected);
|
||||||
});
|
});
|
||||||
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
@@ -2,18 +2,27 @@ import { Injectable } from '@angular/core';
|
|||||||
import { Effect, Actions } from '@ngrx/effects'
|
import { Effect, Actions } from '@ngrx/effects'
|
||||||
import * as fromRouter from '@ngrx/router-store';
|
import * as fromRouter from '@ngrx/router-store';
|
||||||
|
|
||||||
import { HostWindowActionTypes } from '../../shared/host-window.actions';
|
|
||||||
import { SearchSidebarCollapseAction } from './search-sidebar.actions';
|
import { SearchSidebarCollapseAction } from './search-sidebar.actions';
|
||||||
|
import { URLBaser } from '../../core/url-baser/url-baser';
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class SearchSidebarEffects {
|
export class SearchSidebarEffects {
|
||||||
|
private previousPath: string;
|
||||||
@Effect() routeChange$ = this.actions$
|
@Effect() routeChange$ = this.actions$
|
||||||
.ofType(fromRouter.ROUTER_NAVIGATION)
|
.ofType(fromRouter.ROUTER_NAVIGATION)
|
||||||
|
.filter((action) => this.previousPath !== this.getBaseUrl(action))
|
||||||
|
.do((action) => {this.previousPath = this.getBaseUrl(action)})
|
||||||
.map(() => new SearchSidebarCollapseAction());
|
.map(() => new SearchSidebarCollapseAction());
|
||||||
|
|
||||||
constructor(private actions$: Actions) {
|
constructor(private actions$: Actions) {
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
getBaseUrl(action: any): string {
|
||||||
|
/* tslint:disable:no-string-literal */
|
||||||
|
const url: string = action['payload'].routerState.url;
|
||||||
|
return new URLBaser(url).toString();
|
||||||
|
/* tslint:enable:no-string-literal */
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@@ -15,7 +15,12 @@ export class SearchSidebarService {
|
|||||||
private isCollapsdeInStored: Observable<boolean>;
|
private isCollapsdeInStored: Observable<boolean>;
|
||||||
|
|
||||||
constructor(private store: Store<AppState>, private windowService: HostWindowService) {
|
constructor(private store: Store<AppState>, private windowService: HostWindowService) {
|
||||||
this.isMobileView = this.windowService.isXs();
|
this.isMobileView =
|
||||||
|
Observable.combineLatest(
|
||||||
|
this.windowService.isXs(),
|
||||||
|
this.windowService.isSm(),
|
||||||
|
((isXs, isSm) => isXs || isSm)
|
||||||
|
);
|
||||||
this.isCollapsdeInStored = this.store.select(sidebarCollapsedSelector);
|
this.isCollapsdeInStored = this.store.select(sidebarCollapsedSelector);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@@ -15,6 +15,7 @@ import { PageNotFoundComponent } from './pagenotfound/pagenotfound.component';
|
|||||||
{ path: '**', pathMatch: 'full', component: PageNotFoundComponent },
|
{ path: '**', pathMatch: 'full', component: PageNotFoundComponent },
|
||||||
])
|
])
|
||||||
],
|
],
|
||||||
|
exports: [RouterModule]
|
||||||
})
|
})
|
||||||
export class AppRoutingModule {
|
export class AppRoutingModule {
|
||||||
|
|
||||||
|
@@ -1,7 +1,6 @@
|
|||||||
import { NgModule } from '@angular/core';
|
import { NgModule } from '@angular/core';
|
||||||
import { CommonModule, APP_BASE_HREF } from '@angular/common';
|
import { CommonModule, APP_BASE_HREF } from '@angular/common';
|
||||||
import { HttpModule } from '@angular/http';
|
import { HttpModule } from '@angular/http';
|
||||||
import { RouterModule } from '@angular/router';
|
|
||||||
|
|
||||||
import { EffectsModule } from '@ngrx/effects';
|
import { EffectsModule } from '@ngrx/effects';
|
||||||
import { StoreModule, MetaReducer, META_REDUCERS } from '@ngrx/store';
|
import { StoreModule, MetaReducer, META_REDUCERS } from '@ngrx/store';
|
||||||
@@ -46,14 +45,13 @@ export function getMetaReducers(config: GlobalConfig): Array<MetaReducer<AppStat
|
|||||||
const DEV_MODULES: any[] = [];
|
const DEV_MODULES: any[] = [];
|
||||||
|
|
||||||
if (!ENV_CONFIG.production) {
|
if (!ENV_CONFIG.production) {
|
||||||
DEV_MODULES.push(StoreDevtoolsModule.instrument({ maxAge: 50 }));
|
DEV_MODULES.push(StoreDevtoolsModule.instrument({ maxAge: 500 }));
|
||||||
}
|
}
|
||||||
|
|
||||||
@NgModule({
|
@NgModule({
|
||||||
imports: [
|
imports: [
|
||||||
CommonModule,
|
CommonModule,
|
||||||
HttpModule,
|
HttpModule,
|
||||||
RouterModule,
|
|
||||||
AppRoutingModule,
|
AppRoutingModule,
|
||||||
CoreModule.forRoot(),
|
CoreModule.forRoot(),
|
||||||
NgbModule.forRoot(),
|
NgbModule.forRoot(),
|
||||||
|
@@ -7,12 +7,17 @@ import {
|
|||||||
SearchSidebarState,
|
SearchSidebarState,
|
||||||
sidebarReducer
|
sidebarReducer
|
||||||
} from './+search-page/search-sidebar/search-sidebar.reducer';
|
} from './+search-page/search-sidebar/search-sidebar.reducer';
|
||||||
|
import {
|
||||||
|
filterReducer,
|
||||||
|
SearchFiltersState
|
||||||
|
} from './+search-page/search-filters/search-filter/search-filter.reducer';
|
||||||
|
|
||||||
export interface AppState {
|
export interface AppState {
|
||||||
router: fromRouter.RouterReducerState;
|
router: fromRouter.RouterReducerState;
|
||||||
hostWindow: HostWindowState;
|
hostWindow: HostWindowState;
|
||||||
header: HeaderState;
|
header: HeaderState;
|
||||||
searchSidebar: SearchSidebarState;
|
searchSidebar: SearchSidebarState;
|
||||||
|
searchFilter: SearchFiltersState;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const appReducers: ActionReducerMap<AppState> = {
|
export const appReducers: ActionReducerMap<AppState> = {
|
||||||
@@ -20,4 +25,5 @@ export const appReducers: ActionReducerMap<AppState> = {
|
|||||||
hostWindow: hostWindowReducer,
|
hostWindow: hostWindowReducer,
|
||||||
header: headerReducer,
|
header: headerReducer,
|
||||||
searchSidebar: sidebarReducer,
|
searchSidebar: sidebarReducer,
|
||||||
|
searchFilter: filterReducer
|
||||||
};
|
};
|
||||||
|
@@ -32,8 +32,10 @@ import { ServerResponseService } from '../shared/server-response.service';
|
|||||||
import { NativeWindowFactory, NativeWindowService } from '../shared/window.service';
|
import { NativeWindowFactory, NativeWindowService } from '../shared/window.service';
|
||||||
import { BrowseService } from './browse/browse.service';
|
import { BrowseService } from './browse/browse.service';
|
||||||
import { BrowseResponseParsingService } from './data/browse-response-parsing.service';
|
import { BrowseResponseParsingService } from './data/browse-response-parsing.service';
|
||||||
import { SubmissionDefinitionsConfigService } from './config/submission-definitions-config.service';
|
|
||||||
import { ConfigResponseParsingService } from './data/config-response-parsing.service';
|
import { ConfigResponseParsingService } from './data/config-response-parsing.service';
|
||||||
|
import { RouteService } from '../shared/route.service';
|
||||||
|
import { SubmissionDefinitionsConfigService } from './config/submission-definitions-config.service';
|
||||||
import { SubmissionFormsConfigService } from './config/submission-forms-config.service';
|
import { SubmissionFormsConfigService } from './config/submission-forms-config.service';
|
||||||
import { SubmissionSectionsConfigService } from './config/submission-sections-config.service';
|
import { SubmissionSectionsConfigService } from './config/submission-sections-config.service';
|
||||||
|
|
||||||
@@ -69,7 +71,9 @@ const PROVIDERS = [
|
|||||||
ServerResponseService,
|
ServerResponseService,
|
||||||
BrowseResponseParsingService,
|
BrowseResponseParsingService,
|
||||||
BrowseService,
|
BrowseService,
|
||||||
|
|
||||||
ConfigResponseParsingService,
|
ConfigResponseParsingService,
|
||||||
|
RouteService,
|
||||||
SubmissionDefinitionsConfigService,
|
SubmissionDefinitionsConfigService,
|
||||||
SubmissionFormsConfigService,
|
SubmissionFormsConfigService,
|
||||||
SubmissionSectionsConfigService,
|
SubmissionSectionsConfigService,
|
||||||
|
39
src/app/core/url-baser/url-baser.ts
Normal file
39
src/app/core/url-baser/url-baser.ts
Normal file
@@ -0,0 +1,39 @@
|
|||||||
|
import { isEmpty } from '../../shared/empty.util';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Extracts the base URL
|
||||||
|
* from a URL with query parameters
|
||||||
|
*/
|
||||||
|
export class URLBaser {
|
||||||
|
private original: string;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates a new URLBaser
|
||||||
|
*
|
||||||
|
* @param originalURL
|
||||||
|
* a string representing the original URL with possible query parameters
|
||||||
|
*/
|
||||||
|
constructor(originalURL: string) {
|
||||||
|
this.original = originalURL;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Removes the query parameters from the original URL of this URLBaser
|
||||||
|
*
|
||||||
|
* @return {string}
|
||||||
|
* The base URL
|
||||||
|
*/
|
||||||
|
toString(): string {
|
||||||
|
if (isEmpty(this.original)) {
|
||||||
|
return '';
|
||||||
|
} else {
|
||||||
|
const index = this.original.indexOf('?');
|
||||||
|
if (index < 0) {
|
||||||
|
return this.original;
|
||||||
|
} else {
|
||||||
|
return this.original.substring(0, index);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
16
src/app/shared/animations/push.ts
Normal file
16
src/app/shared/animations/push.ts
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
import { animate, state, transition, trigger, style } from '@angular/animations';
|
||||||
|
|
||||||
|
export const pushInOut = trigger('pushInOut', [
|
||||||
|
|
||||||
|
/*
|
||||||
|
state('expanded', style({ right: '100%' }));
|
||||||
|
|
||||||
|
state('collapsed', style({ right: 0 }));
|
||||||
|
*/
|
||||||
|
|
||||||
|
state('expanded', style({ left: '100%' })),
|
||||||
|
|
||||||
|
state('collapsed', style({ left: 0 })),
|
||||||
|
|
||||||
|
transition('expanded <=> collapsed', animate(250)),
|
||||||
|
]);
|
@@ -1,16 +1,10 @@
|
|||||||
import { animate, state, transition, trigger, style } from '@angular/animations';
|
import { animate, state, transition, trigger, style, stagger, query } from '@angular/animations';
|
||||||
|
|
||||||
export const slideInOut = trigger('slideInOut', [
|
export const slide = trigger('slide', [
|
||||||
|
|
||||||
/*
|
state('expanded', style({ height: '*' })),
|
||||||
state('expanded', style({ right: '100%' }));
|
|
||||||
|
|
||||||
state('collapsed', style({ right: 0 }));
|
state('collapsed', style({ height: 0 })),
|
||||||
*/
|
|
||||||
|
|
||||||
state('expanded', style({ left: '100%' })),
|
transition('expanded <=> collapsed', animate(250))
|
||||||
|
|
||||||
state('collapsed', style({ left: 0 })),
|
|
||||||
|
|
||||||
transition('expanded <=> collapsed', animate(250)),
|
|
||||||
]);
|
]);
|
||||||
|
@@ -1,12 +1,12 @@
|
|||||||
<div *ngIf="currentPageState == undefined || currentPageState == currentPage">
|
<div *ngIf="currentPageState == undefined || currentPageState == currentPage">
|
||||||
<div *ngIf="!hideGear" class="pagination-masked clearfix top">
|
<div class="pagination-masked clearfix top">
|
||||||
<div class="row">
|
<div class="row">
|
||||||
<div class="col pagination-info">
|
<div class="col pagination-info">
|
||||||
<span class="align-middle hidden-xs-down">{{ 'pagination.showing.label' | translate }}</span>
|
<span class="align-middle hidden-xs-down">{{ 'pagination.showing.label' | translate }}</span>
|
||||||
<span class="align-middle" *ngIf="collectionSize">{{ 'pagination.showing.detail' | translate:getShowingDetails(collectionSize)}}</span>
|
<span class="align-middle" *ngIf="collectionSize">{{ 'pagination.showing.detail' | translate:getShowingDetails(collectionSize)}}</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="col">
|
<div class="col">
|
||||||
<div ngbDropdown #paginationControls="ngbDropdown" placement="bottom-right" class="d-inline-block float-right">
|
<div *ngIf="!hideGear" ngbDropdown #paginationControls="ngbDropdown" placement="bottom-right" class="d-inline-block float-right">
|
||||||
<button class="btn btn-outline-primary" id="paginationControls" ngbDropdownToggle><i class="fa fa-cog" aria-hidden="true"></i></button>
|
<button class="btn btn-outline-primary" id="paginationControls" ngbDropdownToggle><i class="fa fa-cog" aria-hidden="true"></i></button>
|
||||||
<div id="paginationControlsDropdownMenu" aria-labelledby="paginationControls" ngbDropdownMenu>
|
<div id="paginationControlsDropdownMenu" aria-labelledby="paginationControls" ngbDropdownMenu>
|
||||||
<h6 class="dropdown-header">{{ 'pagination.results-per-page' | translate}}</h6>
|
<h6 class="dropdown-header">{{ 'pagination.results-per-page' | translate}}</h6>
|
||||||
|
118
src/app/shared/route.service.spec.ts
Normal file
118
src/app/shared/route.service.spec.ts
Normal file
@@ -0,0 +1,118 @@
|
|||||||
|
import { RouteService } from './route.service';
|
||||||
|
import { async, TestBed } from '@angular/core/testing';
|
||||||
|
import { ActivatedRoute, convertToParamMap, Params } from '@angular/router';
|
||||||
|
import { Observable } from 'rxjs/Observable';
|
||||||
|
|
||||||
|
describe('RouteService', () => {
|
||||||
|
let service: RouteService;
|
||||||
|
const paramName1 = 'name';
|
||||||
|
const paramValue1 = 'Test Name';
|
||||||
|
const paramName2 = 'id';
|
||||||
|
const paramValue2a = 'Test id';
|
||||||
|
const paramValue2b = 'another id';
|
||||||
|
const nonExistingParamName = 'non existing name';
|
||||||
|
const nonExistingParamValue = 'non existing value';
|
||||||
|
|
||||||
|
const paramObject: Params = {};
|
||||||
|
|
||||||
|
paramObject[paramName1] = paramValue1;
|
||||||
|
paramObject[paramName2] = [paramValue2a, paramValue2b];
|
||||||
|
|
||||||
|
beforeEach(async(() => {
|
||||||
|
return TestBed.configureTestingModule({
|
||||||
|
providers: [
|
||||||
|
{
|
||||||
|
provide: ActivatedRoute,
|
||||||
|
useValue: {
|
||||||
|
queryParams: Observable.of(paramObject),
|
||||||
|
queryParamMap: Observable.of(convertToParamMap(paramObject))
|
||||||
|
},
|
||||||
|
},
|
||||||
|
]
|
||||||
|
});
|
||||||
|
}));
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
service = new RouteService(TestBed.get(ActivatedRoute));
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('hasQueryParam', () => {
|
||||||
|
it('should return true when the parameter name exists', () => {
|
||||||
|
service.hasQueryParam(paramName1).subscribe((status) => {
|
||||||
|
expect(status).toBeTruthy();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
it('should return false when the parameter name does not exists', () => {
|
||||||
|
service.hasQueryParam(nonExistingParamName).subscribe((status) => {
|
||||||
|
expect(status).toBeFalsy();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('hasQueryParamWithValue', () => {
|
||||||
|
it('should return true when the parameter name exists and contains the specified value', () => {
|
||||||
|
service.hasQueryParamWithValue(paramName2, paramValue2a).subscribe((status) => {
|
||||||
|
expect(status).toBeTruthy();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
it('should return false when the parameter name exists and does not contain the specified value', () => {
|
||||||
|
service.hasQueryParamWithValue(paramName1, nonExistingParamValue).subscribe((status) => {
|
||||||
|
expect(status).toBeFalsy();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
it('should return false when the parameter name does not exists', () => {
|
||||||
|
service.hasQueryParamWithValue(nonExistingParamName, nonExistingParamValue).subscribe((status) => {
|
||||||
|
expect(status).toBeFalsy();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
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) => {
|
||||||
|
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) => {
|
||||||
|
const values = params[paramName1];
|
||||||
|
expect(values).toContain(paramValue1);
|
||||||
|
expect(values).toContain(nonExistingParamValue);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
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) => {
|
||||||
|
const values = params[paramName2];
|
||||||
|
expect(values).toContain(paramValue2b);
|
||||||
|
expect(values).not.toContain(paramValue2a);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
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) => {
|
||||||
|
const values = params[paramName2];
|
||||||
|
expect(values).toContain(paramValue2a);
|
||||||
|
expect(values).toContain(paramValue2b);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('removeQueryParameter', () => {
|
||||||
|
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) => {
|
||||||
|
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) => {
|
||||||
|
const values = params[nonExistingParamName];
|
||||||
|
expect(values).toEqual({});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
});
|
55
src/app/shared/route.service.ts
Normal file
55
src/app/shared/route.service.ts
Normal file
@@ -0,0 +1,55 @@
|
|||||||
|
import { Injectable } from '@angular/core';
|
||||||
|
import { Observable } from 'rxjs/Observable';
|
||||||
|
import { ActivatedRoute, convertToParamMap, Params, } from '@angular/router';
|
||||||
|
import { isNotEmpty } from './empty.util';
|
||||||
|
|
||||||
|
@Injectable()
|
||||||
|
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);});
|
||||||
|
}
|
||||||
|
|
||||||
|
hasQueryParamWithValue(paramName: string, paramValue: string): Observable<boolean> {
|
||||||
|
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);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
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);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
removeQueryParameter(paramName: string): Observable<Params> {
|
||||||
|
return this.route.queryParams.map((currentParams) => {
|
||||||
|
const newParam = {};
|
||||||
|
newParam[paramName] = {};
|
||||||
|
return Object.assign({}, currentParams, newParam);
|
||||||
|
});
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
@@ -31,7 +31,6 @@ import { SearchFormComponent } from './search-form/search-form.component';
|
|||||||
import { WrapperListElementComponent } from '../object-list/wrapper-list-element/wrapper-list-element.component';
|
import { WrapperListElementComponent } from '../object-list/wrapper-list-element/wrapper-list-element.component';
|
||||||
import { ViewModeSwitchComponent } from './view-mode-switch/view-mode-switch.component';
|
import { ViewModeSwitchComponent } from './view-mode-switch/view-mode-switch.component';
|
||||||
import { VarDirective } from './utils/var.directive';
|
import { VarDirective } from './utils/var.directive';
|
||||||
import { ScrollAndStickDirective } from './utils/scroll-and-stick.directive';
|
|
||||||
|
|
||||||
const MODULES = [
|
const MODULES = [
|
||||||
// Do NOT include UniversalModule, HttpModule, or JsonpModule here
|
// Do NOT include UniversalModule, HttpModule, or JsonpModule here
|
||||||
@@ -68,7 +67,6 @@ const COMPONENTS = [
|
|||||||
ViewModeSwitchComponent
|
ViewModeSwitchComponent
|
||||||
];
|
];
|
||||||
|
|
||||||
|
|
||||||
const ENTRY_COMPONENTS = [
|
const ENTRY_COMPONENTS = [
|
||||||
// put shared entry components (components that are created dynamically) here
|
// put shared entry components (components that are created dynamically) here
|
||||||
CollectionListElementComponent,
|
CollectionListElementComponent,
|
||||||
@@ -78,8 +76,7 @@ const ENTRY_COMPONENTS = [
|
|||||||
];
|
];
|
||||||
|
|
||||||
const DIRECTIVES = [
|
const DIRECTIVES = [
|
||||||
VarDirective,
|
VarDirective
|
||||||
ScrollAndStickDirective,
|
|
||||||
];
|
];
|
||||||
|
|
||||||
@NgModule({
|
@NgModule({
|
||||||
|
@@ -1,40 +0,0 @@
|
|||||||
|
|
||||||
import { NativeWindowRef, NativeWindowService } from '../window.service';
|
|
||||||
import { Observable } from 'rxjs/Observable';
|
|
||||||
import { AfterViewInit, Directive, ElementRef, Inject } from '@angular/core';
|
|
||||||
|
|
||||||
@Directive({
|
|
||||||
selector: '[dsStick]'
|
|
||||||
})
|
|
||||||
export class ScrollAndStickDirective implements AfterViewInit {
|
|
||||||
|
|
||||||
private initialY: number;
|
|
||||||
|
|
||||||
constructor(private _element: ElementRef, @Inject(NativeWindowService) private _window: NativeWindowRef) {
|
|
||||||
this.subscribeForScrollEvent();
|
|
||||||
}
|
|
||||||
|
|
||||||
ngAfterViewInit(): void {
|
|
||||||
this.initialY = this._element.nativeElement.getBoundingClientRect().top;
|
|
||||||
}
|
|
||||||
|
|
||||||
subscribeForScrollEvent() {
|
|
||||||
|
|
||||||
const obs = Observable.fromEvent(window, 'scroll');
|
|
||||||
|
|
||||||
obs.subscribe((e) => this.handleScrollEvent(e));
|
|
||||||
}
|
|
||||||
|
|
||||||
handleScrollEvent(e) {
|
|
||||||
|
|
||||||
if (this._window.nativeWindow.pageYOffset >= this.initialY) {
|
|
||||||
|
|
||||||
this._element.nativeElement.classList.add('stick');
|
|
||||||
|
|
||||||
} else {
|
|
||||||
|
|
||||||
this._element.nativeElement.classList.remove('stick');
|
|
||||||
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@@ -1,4 +1,3 @@
|
|||||||
import { DebugElement } from '@angular/core';
|
|
||||||
import { TranslateModule, TranslateLoader } from '@ngx-translate/core';
|
import { TranslateModule, TranslateLoader } from '@ngx-translate/core';
|
||||||
import { async, ComponentFixture, TestBed, fakeAsync, tick } from '@angular/core/testing';
|
import { async, ComponentFixture, TestBed, fakeAsync, tick } from '@angular/core/testing';
|
||||||
import { By } from '@angular/platform-browser';
|
import { By } from '@angular/platform-browser';
|
||||||
@@ -6,12 +5,12 @@ import { MockTranslateLoader } from '../mocks/mock-translate-loader';
|
|||||||
import { RouterTestingModule } from '@angular/router/testing';
|
import { RouterTestingModule } from '@angular/router/testing';
|
||||||
|
|
||||||
import { Component } from '@angular/core';
|
import { Component } from '@angular/core';
|
||||||
import { Observable } from 'rxjs/Observable';
|
|
||||||
|
|
||||||
import { SearchService } from '../../+search-page/search-service/search.service';
|
import { SearchService } from '../../+search-page/search-service/search.service';
|
||||||
import { ItemDataService } from './../../core/data/item-data.service';
|
import { ItemDataService } from './../../core/data/item-data.service';
|
||||||
import { ViewModeSwitchComponent } from './view-mode-switch.component';
|
import { ViewModeSwitchComponent } from './view-mode-switch.component';
|
||||||
import { ViewMode } from '../../+search-page/search-options.model';
|
import { ViewMode } from '../../+search-page/search-options.model';
|
||||||
|
import { RouteService } from '../route.service';
|
||||||
|
|
||||||
@Component({ template: '' })
|
@Component({ template: '' })
|
||||||
class DummyComponent { }
|
class DummyComponent { }
|
||||||
@@ -42,6 +41,7 @@ describe('ViewModeSwitchComponent', () => {
|
|||||||
],
|
],
|
||||||
providers: [
|
providers: [
|
||||||
{ provide: ItemDataService, useValue: {} },
|
{ provide: ItemDataService, useValue: {} },
|
||||||
|
{ provide: RouteService, useValue: {} },
|
||||||
SearchService
|
SearchService
|
||||||
],
|
],
|
||||||
}).compileComponents();
|
}).compileComponents();
|
||||||
|
Reference in New Issue
Block a user