+
{{'search.filters.filter.' + filter.name + '.head'| translate}}
-
+ [ngClass]="(collapsed$ | async) ? 'fa-plus' : 'fa-minus'">
+
\ No newline at end of file
diff --git a/src/app/+search-page/search-filters/search-filter/search-filter.component.scss b/src/app/+search-page/search-filters/search-filter/search-filter.component.scss
index 4e45f49468e..1db5e9a1b23 100644
--- a/src/app/+search-page/search-filters/search-filter/search-filter.component.scss
+++ b/src/app/+search-page/search-filters/search-filter/search-filter.component.scss
@@ -1,7 +1,7 @@
@import '../../../../styles/variables.scss';
@import '../../../../styles/mixins.scss';
-:host {
+:host .facet-filter {
border: 1px solid map-get($theme-colors, light);
cursor: pointer;
.search-filter-wrapper.closed {
diff --git a/src/app/+search-page/search-filters/search-filter/search-filter.component.spec.ts b/src/app/+search-page/search-filters/search-filter/search-filter.component.spec.ts
index caa5a6febc2..30ef349675a 100644
--- a/src/app/+search-page/search-filters/search-filter/search-filter.component.spec.ts
+++ b/src/app/+search-page/search-filters/search-filter/search-filter.component.spec.ts
@@ -10,6 +10,7 @@ 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';
+import { SearchConfigurationService } from '../../search-service/search-configuration.service';
describe('SearchFilterComponent', () => {
let comp: SearchFilterComponent;
@@ -33,9 +34,7 @@ describe('SearchFilterComponent', () => {
},
expand: (filter) => {
},
- initialCollapse: (filter) => {
- },
- initialExpand: (filter) => {
+ initializeFilter: (filter) => {
},
getSelectedValuesForFilter: (filter) => {
return observableOf([filterName1, filterName2, filterName3])
@@ -55,6 +54,8 @@ describe('SearchFilterComponent', () => {
getFacetValuesFor: (filter) => mockResults
};
+ const searchConfigServiceStub = {};
+
beforeEach(async(() => {
TestBed.configureTestingModule({
imports: [TranslateModule.forRoot(), RouterTestingModule.withRoutes([]), NoopAnimationsModule],
@@ -65,6 +66,7 @@ describe('SearchFilterComponent', () => {
provide: SearchFilterService,
useValue: mockFilterService
},
+ { provide: SearchConfigurationService, useValue: searchConfigServiceStub },
],
schemas: [NO_ERRORS_SCHEMA]
}).overrideComponent(SearchFilterComponent, {
@@ -91,32 +93,21 @@ describe('SearchFilterComponent', () => {
});
});
- 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', () => {
+ describe('when the initializeFilter method is triggered', () => {
beforeEach(() => {
- spyOn(filterService, 'initialExpand');
- comp.initialExpand();
+ spyOn(filterService, 'initializeFilter');
+ comp.initializeFilter();
});
it('should call initialCollapse with the correct filter configuration name', () => {
- expect(filterService.initialExpand).toHaveBeenCalledWith(mockFilterConfig.name)
+ expect(filterService.initializeFilter).toHaveBeenCalledWith(mockFilterConfig)
});
});
describe('when getSelectedValues is called', () => {
let valuesObservable: Observable
;
beforeEach(() => {
- valuesObservable = comp.getSelectedValues();
+ valuesObservable = (comp as any).getSelectedValues();
});
it('should return an observable containing the existing filters', () => {
@@ -141,7 +132,7 @@ describe('SearchFilterComponent', () => {
let isActive: Observable;
beforeEach(() => {
filterService.isCollapsed = () => observableOf(true);
- isActive = comp.isCollapsed();
+ isActive = (comp as any).isCollapsed();
});
it('should return an observable containing true', () => {
@@ -156,7 +147,7 @@ describe('SearchFilterComponent', () => {
let isActive: Observable;
beforeEach(() => {
filterService.isCollapsed = () => observableOf(false);
- isActive = comp.isCollapsed();
+ isActive = (comp as any).isCollapsed();
});
it('should return an observable containing false', () => {
diff --git a/src/app/+search-page/search-filters/search-filter/search-filter.component.ts b/src/app/+search-page/search-filters/search-filter/search-filter.component.ts
index dcc01f2b46b..14ba8f0b76c 100644
--- a/src/app/+search-page/search-filters/search-filter/search-filter.component.ts
+++ b/src/app/+search-page/search-filters/search-filter/search-filter.component.ts
@@ -1,11 +1,12 @@
-
-import { take } from 'rxjs/operators';
+import { filter, first, map, startWith, switchMap, take } from 'rxjs/operators';
import { Component, Input, OnInit } from '@angular/core';
import { SearchFilterConfig } from '../../search-service/search-filter-config.model';
import { SearchFilterService } from './search-filter.service';
-import { Observable } from 'rxjs';
+import { BehaviorSubject, Observable, of as observableOf } from 'rxjs';
import { slide } from '../../../shared/animations/slide';
import { isNotEmpty } from '../../../shared/empty.util';
+import { SearchService } from '../../search-service/search.service';
+import { SearchConfigurationService } from '../../search-service/search-configuration.service';
@Component({
selector: 'ds-search-filter',
@@ -26,9 +27,24 @@ export class SearchFilterComponent implements OnInit {
/**
* True when the filter is 100% collapsed in the UI
*/
- collapsed;
+ closed = true;
+
+ /**
+ * Emits true when the filter is currently collapsed in the store
+ */
+ collapsed$: Observable;
+
+ /**
+ * Emits all currently selected values for this filter
+ */
+ selectedValues$: Observable;
+
+ /**
+ * Emits true when the current filter is supposed to be shown
+ */
+ active$: Observable;
- constructor(private filterService: SearchFilterService) {
+ constructor(private filterService: SearchFilterService, private searchService: SearchService, private searchConfigService: SearchConfigurationService) {
}
/**
@@ -37,11 +53,13 @@ export class SearchFilterComponent implements OnInit {
* Else, the filter should initially be collapsed
*/
ngOnInit() {
- this.getSelectedValues().pipe(take(1)).subscribe((isActive) => {
- if (this.filter.isOpenByDefault || isNotEmpty(isActive)) {
- this.initialExpand();
- } else {
- this.initialCollapse();
+ this.selectedValues$ = this.getSelectedValues();
+ this.active$ = this.isActive();
+ this.collapsed$ = this.isCollapsed();
+ this.initializeFilter();
+ this.selectedValues$.pipe(take(1)).subscribe((selectedValues) => {
+ if (isNotEmpty(selectedValues)) {
+ this.filterService.expand(this.filter.name);
}
});
}
@@ -57,30 +75,21 @@ export class SearchFilterComponent implements OnInit {
* Checks if the filter is currently collapsed
* @returns {Observable} Emits true when the current state of the filter is collapsed, false when it's expanded
*/
- isCollapsed(): Observable {
+ private isCollapsed(): Observable {
return this.filterService.isCollapsed(this.filter.name);
}
/**
- * Changes the initial state to collapsed
- */
- initialCollapse() {
- this.filterService.initialCollapse(this.filter.name);
- this.collapsed = true;
- }
-
- /**
- * Changes the initial state to expanded
+ * Sets the initial state of the filter
*/
- initialExpand() {
- this.filterService.initialExpand(this.filter.name);
- this.collapsed = false;
+ initializeFilter() {
+ this.filterService.initializeFilter(this.filter);
}
/**
* @returns {Observable} Emits a list of all values that are currently active for this filter
*/
- getSelectedValues(): Observable {
+ private getSelectedValues(): Observable {
return this.filterService.getSelectedValuesForFilter(this.filter);
}
@@ -90,7 +99,7 @@ export class SearchFilterComponent implements OnInit {
*/
finishSlide(event: any): void {
if (event.fromState === 'collapsed') {
- this.collapsed = false;
+ this.closed = false;
}
}
@@ -100,7 +109,31 @@ export class SearchFilterComponent implements OnInit {
*/
startSlide(event: any): void {
if (event.toState === 'collapsed') {
- this.collapsed = true;
+ this.closed = true;
}
}
+
+ /**
+ * Check if a given filter is supposed to be shown or not
+ * @returns {Observable} Emits true whenever a given filter config should be shown
+ */
+ private isActive(): Observable {
+ return this.selectedValues$.pipe(
+ switchMap((isActive) => {
+ if (isNotEmpty(isActive)) {
+ return observableOf(true);
+ } else {
+ return this.searchConfigService.searchOptions.pipe(
+ switchMap((options) => {
+ return this.searchService.getFacetValuesFor(this.filter, 1, options).pipe(
+ filter((RD) => !RD.isLoading),
+ map((valuesRD) => {
+ return valuesRD.payload.totalElements > 0
+ }),)
+ }
+ ))
+ }
+ }),
+ startWith(true));
+ }
}
diff --git a/src/app/+search-page/search-filters/search-filter/search-filter.reducer.spec.ts b/src/app/+search-page/search-filters/search-filter/search-filter.reducer.spec.ts
index 8fbfbf2e651..2f3268fba59 100644
--- a/src/app/+search-page/search-filters/search-filter/search-filter.reducer.spec.ts
+++ b/src/app/+search-page/search-filters/search-filter/search-filter.reducer.spec.ts
@@ -1,10 +1,8 @@
import * as deepFreeze from 'deep-freeze';
import {
SearchFilterCollapseAction, SearchFilterExpandAction, SearchFilterIncrementPageAction,
- SearchFilterInitialCollapseAction,
- SearchFilterInitialExpandAction,
SearchFilterToggleAction,
- SearchFilterDecrementPageAction, SearchFilterResetPageAction
+ SearchFilterDecrementPageAction, SearchFilterResetPageAction, SearchFilterInitializeAction
} from './search-filter.actions';
import { filterReducer } from './search-filter.reducer';
@@ -98,35 +96,39 @@ describe('filterReducer', () => {
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', () => {
+ it('should set filterCollapsed to true in response to the INITIALIZE action with isOpenByDefault to false when no state has been set for this filter', () => {
const state = {};
state[filterName2] = { filterCollapsed: false, page: 1 };
- const action = new SearchFilterInitialCollapseAction(filterName1);
+ const filterConfig = {isOpenByDefault: false, name: filterName1} as any;
+ const action = new SearchFilterInitializeAction(filterConfig);
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', () => {
+ it('should set filterCollapsed to false in response to the INITIALIZE action with isOpenByDefault to true when no state has been set for this filter', () => {
const state = {};
state[filterName2] = { filterCollapsed: true, page: 1 };
- const action = new SearchFilterInitialExpandAction(filterName1);
+ const filterConfig = {isOpenByDefault: true, name: filterName1} as any;
+ const action = new SearchFilterInitializeAction(filterConfig);
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', () => {
+ it('should not change the state in response to the INITIALIZE action with isOpenByDefault to false when the state has already been set for this filter', () => {
const state = {};
state[filterName1] = { filterCollapsed: false, page: 1 };
- const action = new SearchFilterInitialCollapseAction(filterName1);
+ const filterConfig = { isOpenByDefault: true, name: filterName1 } as any;
+ const action = new SearchFilterInitializeAction(filterConfig);
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', () => {
+ it('should not change the state in response to the INITIALIZE action with isOpenByDefault to true when the state has already been set for this filter', () => {
const state = {};
state[filterName1] = { filterCollapsed: true, page: 1 };
- const action = new SearchFilterInitialExpandAction(filterName1);
+ const filterConfig = { isOpenByDefault: false, name: filterName1 } as any;
+ const action = new SearchFilterInitializeAction(filterConfig);
const newState = filterReducer(state, action);
expect(newState).toEqual(state);
});
diff --git a/src/app/+search-page/search-filters/search-filter/search-filter.reducer.ts b/src/app/+search-page/search-filters/search-filter/search-filter.reducer.ts
index f7e064fcc7a..187bcd50d05 100644
--- a/src/app/+search-page/search-filters/search-filter/search-filter.reducer.ts
+++ b/src/app/+search-page/search-filters/search-filter/search-filter.reducer.ts
@@ -1,5 +1,9 @@
-import { SearchFilterAction, SearchFilterActionTypes } from './search-filter.actions';
-import { isEmpty } from '../../../shared/empty.util';
+import {
+ SearchFilterAction,
+ SearchFilterActionTypes,
+ SearchFilterInitializeAction
+} from './search-filter.actions';
+import { isEmpty, isNotUndefined } from '../../../shared/empty.util';
/**
* Interface that represents the state for a single filters
@@ -28,27 +32,14 @@ export function filterReducer(state = initialState, action: SearchFilterAction):
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
- }
- });
- }
+ case SearchFilterActionTypes.INITIALIZE: {
+ const initAction = (action as SearchFilterInitializeAction);
+ return Object.assign({}, state, {
+ [action.filterName]: {
+ filterCollapsed: !initAction.initiallyExpanded,
+ page: 1
+ }
+ });
return state;
}
diff --git a/src/app/+search-page/search-filters/search-filter/search-filter.service.spec.ts b/src/app/+search-page/search-filters/search-filter/search-filter.service.spec.ts
index 156e8d47ea0..19239d899cf 100644
--- a/src/app/+search-page/search-filters/search-filter/search-filter.service.spec.ts
+++ b/src/app/+search-page/search-filters/search-filter/search-filter.service.spec.ts
@@ -5,8 +5,7 @@ import {
SearchFilterDecrementPageAction,
SearchFilterExpandAction,
SearchFilterIncrementPageAction,
- SearchFilterInitialCollapseAction,
- SearchFilterInitialExpandAction,
+ SearchFilterInitializeAction,
SearchFilterResetPageAction,
SearchFilterToggleAction
} from './search-filter.actions';
@@ -62,23 +61,13 @@ describe('SearchFilterService', () => {
service = new SearchFilterService(store, routeServiceStub);
});
- describe('when the initialCollapse method is triggered', () => {
+ describe('when the initializeFilter method is triggered', () => {
beforeEach(() => {
- service.initialCollapse(mockFilterConfig.name);
+ service.initializeFilter(mockFilterConfig);
});
- 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));
+ it('SearchFilterInitializeAction should be dispatched to the store', () => {
+ expect(store.dispatch).toHaveBeenCalledWith(new SearchFilterInitializeAction(mockFilterConfig));
});
});
diff --git a/src/app/+search-page/search-filters/search-filter/search-filter.service.ts b/src/app/+search-page/search-filters/search-filter/search-filter.service.ts
index bf21eab3675..bed4b1777f6 100644
--- a/src/app/+search-page/search-filters/search-filter/search-filter.service.ts
+++ b/src/app/+search-page/search-filters/search-filter/search-filter.service.ts
@@ -1,6 +1,6 @@
import { combineLatest as observableCombineLatest, Observable } from 'rxjs';
import { Injectable, InjectionToken } from '@angular/core';
-import { map } from 'rxjs/operators';
+import { distinctUntilChanged, map } from 'rxjs/operators';
import { SearchFiltersState, SearchFilterState } from './search-filter.reducer';
import { createSelector, MemoizedSelector, select, Store } from '@ngrx/store';
import {
@@ -8,8 +8,7 @@ import {
SearchFilterDecrementPageAction,
SearchFilterExpandAction,
SearchFilterIncrementPageAction,
- SearchFilterInitialCollapseAction,
- SearchFilterInitialExpandAction,
+ SearchFilterInitializeAction,
SearchFilterResetPageAction,
SearchFilterToggleAction
} from './search-filter.actions';
@@ -17,7 +16,8 @@ import { hasValue, isNotEmpty, } from '../../../shared/empty.util';
import { SearchFilterConfig } from '../../search-service/search-filter-config.model';
import { RouteService } from '../../../shared/services/route.service';
import { Params } from '@angular/router';
-
+import { SearchOptions } from '../../search-options.model';
+// const spy = create();
const filterStateSelector = (state: SearchFiltersState) => state.searchFilter;
export const FILTER_CONFIG: InjectionToken = new InjectionToken('filterConfig');
@@ -60,7 +60,7 @@ export class SearchFilterService {
getSelectedValuesForFilter(filterConfig: SearchFilterConfig): Observable {
const values$ = this.routeService.getQueryParameterValues(filterConfig.paramName);
const prefixValues$ = this.routeService.getQueryParamsWithPrefix(filterConfig.paramName + '.').pipe(
- map((params: Params) => [].concat(...Object.values(params)))
+ map((params: Params) => [].concat(...Object.values(params))),
);
return observableCombineLatest(values$, prefixValues$).pipe(
@@ -88,13 +88,14 @@ export class SearchFilterService {
} else {
return false;
}
- })
+ }),
+ distinctUntilChanged()
);
}
/**
* Request the current page of a given filter
- * @param {string} filterName The filtername for which the page state is checked
+ * @param {string} filterName The filter name for which the page state is checked
* @returns {Observable} Emits the current page state of the given filter, if it's unavailable, return 1
*/
getPage(filterName: string): Observable {
@@ -106,7 +107,8 @@ export class SearchFilterService {
} else {
return 1;
}
- }));
+ }),
+ distinctUntilChanged());
}
/**
@@ -134,19 +136,11 @@ export class SearchFilterService {
}
/**
- * Dispatches an initial collapse action to the store for a given filter
- * @param {string} filterName The filter for which the action is dispatched
- */
- public initialCollapse(filterName: string): void {
- this.store.dispatch(new SearchFilterInitialCollapseAction(filterName));
- }
-
- /**
- * Dispatches an initial expand action to the store for a given filter
- * @param {string} filterName The filter for which the action is dispatched
+ * Dispatches an initialize action to the store for a given filter
+ * @param {SearchFilterConfig} filter The filter for which the action is dispatched
*/
- public initialExpand(filterName: string): void {
- this.store.dispatch(new SearchFilterInitialExpandAction(filterName));
+ public initializeFilter(filter: SearchFilterConfig): void {
+ this.store.dispatch(new SearchFilterInitializeAction(filter));
}
/**
diff --git a/src/app/+search-page/search-filters/search-filter/search-hierarchy-filter/search-hierarchy-filter.component.html b/src/app/+search-page/search-filters/search-filter/search-hierarchy-filter/search-hierarchy-filter.component.html
index 812f5437162..b6ae0ada639 100644
--- a/src/app/+search-page/search-filters/search-filter/search-hierarchy-filter/search-hierarchy-filter.component.html
+++ b/src/app/+search-page/search-filters/search-filter/search-hierarchy-filter/search-hierarchy-filter.component.html
@@ -1,24 +1,9 @@
-
-
- {{value}}
-
+
diff --git a/src/app/+search-page/search-filters/search-filter/search-range-filter/search-range-filter.component.html b/src/app/+search-page/search-filters/search-filter/search-range-filter/search-range-filter.component.html
index 352c1710c05..9d35cc518af 100644
--- a/src/app/+search-page/search-filters/search-filter/search-range-filter/search-range-filter.component.html
+++ b/src/app/+search-page/search-filters/search-filter/search-range-filter/search-range-filter.component.html
@@ -24,16 +24,7 @@
diff --git a/src/app/+search-page/search-filters/search-filter/search-range-filter/search-range-filter.component.spec.ts b/src/app/+search-page/search-filters/search-filter/search-range-filter/search-range-filter.component.spec.ts
index 6f3450e18e1..930ea8c9fb2 100644
--- a/src/app/+search-page/search-filters/search-filter/search-range-filter/search-range-filter.component.spec.ts
+++ b/src/app/+search-page/search-filters/search-filter/search-range-filter/search-range-filter.component.spec.ts
@@ -106,16 +106,6 @@ describe('SearchRangeFilterComponent', () => {
fixture.detectChanges();
});
- describe('when the getChangeParams method is called wih a value', () => {
- it('should return the selectedValue list with the new parameter value', () => {
- const result$ = comp.getChangeParams(value3);
- result$.subscribe((result) => {
- expect(result[mockFilterConfig.paramName + minSuffix]).toEqual(['1990']);
- expect(result[mockFilterConfig.paramName + maxSuffix]).toEqual(['1992']);
- });
- });
- });
-
describe('when the onSubmit method is called with data', () => {
const searchUrl = '/search/path';
// const data = { [mockFilterConfig.paramName + minSuffix]: '1900', [mockFilterConfig.paramName + maxSuffix]: '1950' };
diff --git a/src/app/+search-page/search-filters/search-filter/search-range-filter/search-range-filter.component.ts b/src/app/+search-page/search-filters/search-filter/search-range-filter/search-range-filter.component.ts
index 6cb04c6c1f3..ebdb7975003 100644
--- a/src/app/+search-page/search-filters/search-filter/search-range-filter/search-range-filter.component.ts
+++ b/src/app/+search-page/search-filters/search-filter/search-range-filter/search-range-filter.component.ts
@@ -1,9 +1,4 @@
-import {
- of as observableOf,
- combineLatest as observableCombineLatest,
- Observable,
- Subscription
-} from 'rxjs';
+import { combineLatest as observableCombineLatest, Subscription } from 'rxjs';
import { map, startWith } from 'rxjs/operators';
import { isPlatformBrowser } from '@angular/common';
import { Component, Inject, OnDestroy, OnInit, PLATFORM_ID } from '@angular/core';
@@ -23,16 +18,26 @@ import { RouteService } from '../../../../shared/services/route.service';
import { hasValue } from '../../../../shared/empty.util';
import { SearchConfigurationService } from '../../../search-service/search-configuration.service';
+/**
+ * The suffix for a range filters' minimum in the frontend URL
+ */
+export const RANGE_FILTER_MIN_SUFFIX = '.min';
+
+/**
+ * The suffix for a range filters' maximum in the frontend URL
+ */
+export const RANGE_FILTER_MAX_SUFFIX = '.max';
+
+/**
+ * The date formats that are possible to appear in a date filter
+ */
+const dateFormats = ['YYYY', 'YYYY-MM', 'YYYY-MM-DD'];
+
/**
* 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.
*/
-const minSuffix = '.min';
-const maxSuffix = '.max';
-const dateFormats = ['YYYY', 'YYYY-MM', 'YYYY-MM-DD'];
-const rangeDelimiter = '-';
-
@Component({
selector: 'ds-search-range-filter',
styleUrls: ['./search-range-filter.component.scss'],
@@ -85,8 +90,8 @@ export class SearchRangeFilterComponent extends SearchFacetFilterComponent imple
super.ngOnInit();
this.min = moment(this.filterConfig.minValue, dateFormats).year() || this.min;
this.max = moment(this.filterConfig.maxValue, dateFormats).year() || this.max;
- const iniMin = this.route.getQueryParameterValue(this.filterConfig.paramName + minSuffix).pipe(startWith(undefined));
- const iniMax = this.route.getQueryParameterValue(this.filterConfig.paramName + maxSuffix).pipe(startWith(undefined));
+ const iniMin = this.route.getQueryParameterValue(this.filterConfig.paramName + RANGE_FILTER_MIN_SUFFIX).pipe(startWith(undefined));
+ const iniMax = this.route.getQueryParameterValue(this.filterConfig.paramName + RANGE_FILTER_MAX_SUFFIX).pipe(startWith(undefined));
this.sub = observableCombineLatest(iniMin, iniMax).pipe(
map(([min, max]) => {
const minimum = hasValue(min) ? min : this.min;
@@ -96,23 +101,6 @@ export class SearchRangeFilterComponent extends SearchFacetFilterComponent imple
).subscribe((minmax) => this.range = minmax);
}
- /**
- * Calculates the parameters that should change if a given values for this range filter would be changed
- * @param {string} value The values that are changed for this filter
- * @returns {Observable
} The changed filter parameters
- */
- getChangeParams(value: string) {
- const parts = value.split(rangeDelimiter);
- const min = parts.length > 1 ? parts[0].trim() : value;
- const max = parts.length > 1 ? parts[1].trim() : value;
- return observableOf(
- {
- [this.filterConfig.paramName + minSuffix]: [min],
- [this.filterConfig.paramName + maxSuffix]: [max],
- page: 1
- });
- }
-
/**
* Submits new custom range values to the range filter from the widget
*/
@@ -122,8 +110,8 @@ export class SearchRangeFilterComponent extends SearchFacetFilterComponent imple
this.router.navigate([this.getSearchLink()], {
queryParams:
{
- [this.filterConfig.paramName + minSuffix]: newMin,
- [this.filterConfig.paramName + maxSuffix]: newMax
+ [this.filterConfig.paramName + RANGE_FILTER_MIN_SUFFIX]: newMin,
+ [this.filterConfig.paramName + RANGE_FILTER_MAX_SUFFIX]: newMax
},
queryParamsHandling: 'merge'
});
@@ -148,8 +136,4 @@ export class SearchRangeFilterComponent extends SearchFacetFilterComponent imple
this.sub.unsubscribe();
}
}
-
- out(call) {
- console.log(call);
- }
}
diff --git a/src/app/+search-page/search-filters/search-filter/search-text-filter/search-text-filter.component.html b/src/app/+search-page/search-filters/search-filter/search-text-filter/search-text-filter.component.html
index fcc2393b935..25ff8e46d34 100644
--- a/src/app/+search-page/search-filters/search-filter/search-text-filter/search-text-filter.component.html
+++ b/src/app/+search-page/search-filters/search-filter/search-text-filter/search-text-filter.component.html
@@ -1,26 +1,9 @@
-
-
- {{value}}
-
-
+
+
@@ -40,6 +23,5 @@
(submitSuggestion)="onSubmit($event)"
(clickSuggestion)="onClick($event)"
(findSuggestions)="findSuggestions($event)"
- ngDefaultControl
- >
+ ngDefaultControl>
diff --git a/src/app/+search-page/search-filters/search-filters.component.html b/src/app/+search-page/search-filters/search-filters.component.html
index 0522c1fba0e..895765f6ac5 100644
--- a/src/app/+search-page/search-filters/search-filters.component.html
+++ b/src/app/+search-page/search-filters/search-filters.component.html
@@ -1,7 +1,7 @@
{{"search.filters.head" | translate}}
-
{{"search.filters.reset" | translate}}
\ No newline at end of file
diff --git a/src/app/+search-page/search-filters/search-filters.component.ts b/src/app/+search-page/search-filters/search-filters.component.ts
index f16faff1f35..1dd747e908d 100644
--- a/src/app/+search-page/search-filters/search-filters.component.ts
+++ b/src/app/+search-page/search-filters/search-filters.component.ts
@@ -1,12 +1,11 @@
-import { Observable, of as observableOf } from 'rxjs';
+import { Observable } from 'rxjs';
-import { filter, map, mergeMap, startWith, switchMap } from 'rxjs/operators';
+import { map } from 'rxjs/operators';
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 { SearchConfigurationService } from '../search-service/search-configuration.service';
-import { isNotEmpty } from '../../shared/empty.util';
import { SearchFilterService } from './search-filter/search-filter.service';
import { getSucceededRemoteData } from '../../core/shared/operators';
@@ -53,26 +52,9 @@ export class SearchFiltersComponent {
}
/**
- * Check if a given filter is supposed to be shown or not
- * @param {SearchFilterConfig} filter The filter to check for
- * @returns {Observable
} Emits true whenever a given filter config should be shown
+ * Prevent unnecessary rerendering
*/
- isActive(filterConfig: SearchFilterConfig): Observable {
- return this.filterService.getSelectedValuesForFilter(filterConfig).pipe(
- mergeMap((isActive) => {
- if (isNotEmpty(isActive)) {
- return observableOf(true);
- } else {
- return this.searchConfigService.searchOptions.pipe(
- switchMap((options) => {
- return this.searchService.getFacetValuesFor(filterConfig, 1, options).pipe(
- filter((RD) => !RD.isLoading),
- map((valuesRD) => {
- return valuesRD.payload.totalElements > 0
- }),)
- }
- ))
- }
- }),startWith(true),);
+ trackUpdate(index, config: SearchFilterConfig) {
+ return config ? config.name : undefined;
}
}
diff --git a/src/app/+search-page/search-page.component.ts b/src/app/+search-page/search-page.component.ts
index 816e3d67bf6..0c572a3a843 100644
--- a/src/app/+search-page/search-page.component.ts
+++ b/src/app/+search-page/search-page.component.ts
@@ -62,7 +62,6 @@ export class SearchPageComponent implements OnInit {
constructor(private service: SearchService,
private sidebarService: SearchSidebarService,
private windowService: HostWindowService,
- private filterService: SearchFilterService,
private searchConfigService: SearchConfigurationService) {
this.isXsOrSm$ = this.windowService.isXsOrSm();
}
diff --git a/src/app/+search-page/search-page.module.ts b/src/app/+search-page/search-page.module.ts
index 505065e44c8..dc895e5d703 100644
--- a/src/app/+search-page/search-page.module.ts
+++ b/src/app/+search-page/search-page.module.ts
@@ -27,6 +27,9 @@ import { SearchFacetFilterWrapperComponent } from './search-filters/search-filte
import { SearchBooleanFilterComponent } from './search-filters/search-filter/search-boolean-filter/search-boolean-filter.component';
import { SearchHierarchyFilterComponent } from './search-filters/search-filter/search-hierarchy-filter/search-hierarchy-filter.component';
import { SearchConfigurationService } from './search-service/search-configuration.service';
+import { SearchFacetOptionComponent } from './search-filters/search-filter/search-facet-filter-options/search-facet-option/search-facet-option.component';
+import { SearchFacetSelectedOptionComponent } from './search-filters/search-filter/search-facet-filter-options/search-facet-selected-option/search-facet-selected-option.component';
+import { SearchFacetRangeOptionComponent } from './search-filters/search-filter/search-facet-filter-options/search-facet-range-option/search-facet-range-option.component';
const effects = [
SearchSidebarEffects
@@ -58,6 +61,9 @@ const effects = [
SearchTextFilterComponent,
SearchHierarchyFilterComponent,
SearchBooleanFilterComponent,
+ SearchFacetOptionComponent,
+ SearchFacetSelectedOptionComponent,
+ SearchFacetRangeOptionComponent
],
providers: [
SearchSidebarService,
@@ -76,6 +82,9 @@ const effects = [
SearchTextFilterComponent,
SearchHierarchyFilterComponent,
SearchBooleanFilterComponent,
+ SearchFacetOptionComponent,
+ SearchFacetSelectedOptionComponent,
+ SearchFacetRangeOptionComponent
]
})
diff --git a/src/app/+search-page/search-service/search-configuration.service.spec.ts b/src/app/+search-page/search-service/search-configuration.service.spec.ts
index af8897c93b7..f1f4ef8bdce 100644
--- a/src/app/+search-page/search-service/search-configuration.service.spec.ts
+++ b/src/app/+search-page/search-service/search-configuration.service.spec.ts
@@ -117,7 +117,7 @@ describe('SearchConfigurationService', () => {
describe('when subscribeToSearchOptions is called', () => {
beforeEach(() => {
- service.subscribeToSearchOptions(defaults)
+ (service as any).subscribeToSearchOptions(defaults)
});
it('should call all getters it needs, but not call any others', () => {
expect(service.getCurrentPagination).not.toHaveBeenCalled();
@@ -131,7 +131,7 @@ describe('SearchConfigurationService', () => {
describe('when subscribeToPaginatedSearchOptions is called', () => {
beforeEach(() => {
- service.subscribeToPaginatedSearchOptions(defaults);
+ (service as any).subscribeToPaginatedSearchOptions(defaults);
});
it('should call all getters it needs', () => {
expect(service.getCurrentPagination).toHaveBeenCalled();
diff --git a/src/app/+search-page/search-service/search-configuration.service.ts b/src/app/+search-page/search-service/search-configuration.service.ts
index 292f26724db..7ba1ebd75f9 100644
--- a/src/app/+search-page/search-service/search-configuration.service.ts
+++ b/src/app/+search-page/search-service/search-configuration.service.ts
@@ -186,7 +186,7 @@ export class SearchConfigurationService implements OnDestroy {
* @param {SearchOptions} defaults Default values for when no parameters are available
* @returns {Subscription} The subscription to unsubscribe from
*/
- subscribeToSearchOptions(defaults: SearchOptions): Subscription {
+ private subscribeToSearchOptions(defaults: SearchOptions): Subscription {
return observableMerge(
this.getScopePart(defaults.scope),
this.getQueryPart(defaults.query),
@@ -204,7 +204,7 @@ export class SearchConfigurationService implements OnDestroy {
* @param {PaginatedSearchOptions} defaults Default values for when no parameters are available
* @returns {Subscription} The subscription to unsubscribe from
*/
- subscribeToPaginatedSearchOptions(defaults: PaginatedSearchOptions): Subscription {
+ private subscribeToPaginatedSearchOptions(defaults: PaginatedSearchOptions): Subscription {
return observableMerge(
this.getPaginationPart(defaults.pagination),
this.getSortPart(defaults.sort),
diff --git a/src/app/core/cache/object-cache.service.ts b/src/app/core/cache/object-cache.service.ts
index b4c37df7362..483de65b980 100644
--- a/src/app/core/cache/object-cache.service.ts
+++ b/src/app/core/cache/object-cache.service.ts
@@ -1,34 +1,44 @@
+import { Injectable } from '@angular/core';
+import { createSelector, MemoizedSelector, select, Store } from '@ngrx/store';
+import { applyPatch, Operation } from 'fast-json-patch';
import { combineLatest as observableCombineLatest, Observable } from 'rxjs';
import { distinctUntilChanged, filter, map, mergeMap, take, } from 'rxjs/operators';
-import { Injectable } from '@angular/core';
-import { MemoizedSelector, select, Store } from '@ngrx/store';
-import { IndexName } from '../index/index.reducer';
-
-import { CacheableObject, ObjectCacheEntry } from './object-cache.reducer';
+import { hasNoValue, hasValue, isNotEmpty } from '../../shared/empty.util';
+import { CoreState } from '../core.reducers';
+import { coreSelector } from '../core.selectors';
+import { RestRequestMethod } from '../data/rest-request-method';
+import { selfLinkFromUuidSelector } from '../index/index.selectors';
+import { GenericConstructor } from '../shared/generic-constructor';
+import { NormalizedObjectFactory } from './models/normalized-object-factory';
+import { NormalizedObject } from './models/normalized-object.model';
import {
AddPatchObjectCacheAction,
AddToObjectCacheAction,
ApplyPatchObjectCacheAction,
RemoveFromObjectCacheAction
} from './object-cache.actions';
-import { hasNoValue, isNotEmpty } from '../../shared/empty.util';
-import { GenericConstructor } from '../shared/generic-constructor';
-import { coreSelector, CoreState } from '../core.reducers';
-import { pathSelector } from '../shared/selectors';
-import { NormalizedObjectFactory } from './models/normalized-object-factory';
-import { NormalizedObject } from './models/normalized-object.model';
-import { applyPatch, Operation } from 'fast-json-patch';
+
+import { CacheableObject, ObjectCacheEntry, ObjectCacheState } from './object-cache.reducer';
import { AddToSSBAction } from './server-sync-buffer.actions';
-import { RestRequestMethod } from '../data/rest-request-method';
-function selfLinkFromUuidSelector(uuid: string): MemoizedSelector {
- return pathSelector(coreSelector, 'index', IndexName.OBJECT, uuid);
-}
+/**
+ * The base selector function to select the object cache in the store
+ */
+const objectCacheSelector = createSelector(
+ coreSelector,
+ (state: CoreState) => state['cache/object']
+);
-function entryFromSelfLinkSelector(selfLink: string): MemoizedSelector {
- return pathSelector(coreSelector, 'cache/object', selfLink);
-}
+/**
+ * Selector function to select an object entry by self link from the cache
+ * @param selfLink The self link of the object
+ */
+const entryFromSelfLinkSelector =
+ (selfLink: string): MemoizedSelector => createSelector(
+ objectCacheSelector,
+ (state: ObjectCacheState) => state[selfLink],
+ );
/**
* A service to interact with the object cache
diff --git a/src/app/core/cache/server-sync-buffer.effects.ts b/src/app/core/cache/server-sync-buffer.effects.ts
index 2e11f155403..3aa6ad312fb 100644
--- a/src/app/core/cache/server-sync-buffer.effects.ts
+++ b/src/app/core/cache/server-sync-buffer.effects.ts
@@ -1,6 +1,7 @@
import { delay, exhaustMap, map, switchMap, take } from 'rxjs/operators';
import { Inject, Injectable } from '@angular/core';
import { Actions, Effect, ofType } from '@ngrx/effects';
+import { coreSelector } from '../core.selectors';
import {
AddToSSBAction,
CommitSSBAction,
@@ -9,7 +10,7 @@ import {
} from './server-sync-buffer.actions';
import { GLOBAL_CONFIG } from '../../../config';
import { GlobalConfig } from '../../../config/global-config.interface';
-import { coreSelector, CoreState } from '../core.reducers';
+import { CoreState } from '../core.reducers';
import { Action, createSelector, MemoizedSelector, select, Store } from '@ngrx/store';
import { ServerSyncBufferEntry, ServerSyncBufferState } from './server-sync-buffer.reducer';
import { combineLatest as observableCombineLatest, of as observableOf } from 'rxjs';
diff --git a/src/app/core/core.reducers.ts b/src/app/core/core.reducers.ts
index e0ddb4a9de1..abfc3b69cc8 100644
--- a/src/app/core/core.reducers.ts
+++ b/src/app/core/core.reducers.ts
@@ -4,7 +4,7 @@ import {
} from '@ngrx/store';
import { objectCacheReducer, ObjectCacheState } from './cache/object-cache.reducer';
-import { indexReducer, IndexState } from './index/index.reducer';
+import { indexReducer, MetaIndexState } from './index/index.reducer';
import { requestReducer, RequestState } from './data/request.reducer';
import { authReducer, AuthState } from './auth/auth.reducer';
import { serverSyncBufferReducer, ServerSyncBufferState } from './cache/server-sync-buffer.reducer';
@@ -18,7 +18,7 @@ export interface CoreState {
'cache/syncbuffer': ServerSyncBufferState,
'cache/object-updates': ObjectUpdatesState
'data/request': RequestState,
- 'index': IndexState,
+ 'index': MetaIndexState,
'auth': AuthState,
}
@@ -30,5 +30,3 @@ export const coreReducers: ActionReducerMap = {
'index': indexReducer,
'auth': authReducer,
};
-
-export const coreSelector = createFeatureSelector('core');
diff --git a/src/app/core/core.selectors.ts b/src/app/core/core.selectors.ts
new file mode 100644
index 00000000000..60365be7c26
--- /dev/null
+++ b/src/app/core/core.selectors.ts
@@ -0,0 +1,7 @@
+import { createFeatureSelector } from '@ngrx/store';
+import { CoreState } from './core.reducers';
+
+/**
+ * Base selector to select the core state from the store
+ */
+export const coreSelector = createFeatureSelector('core');
diff --git a/src/app/core/data/object-updates/object-updates.service.ts b/src/app/core/data/object-updates/object-updates.service.ts
index 85e17b5b2f2..a13fb9487b2 100644
--- a/src/app/core/data/object-updates/object-updates.service.ts
+++ b/src/app/core/data/object-updates/object-updates.service.ts
@@ -1,6 +1,7 @@
import { Injectable } from '@angular/core';
import { createSelector, MemoizedSelector, select, Store } from '@ngrx/store';
-import { coreSelector, CoreState } from '../../core.reducers';
+import { CoreState } from '../../core.reducers';
+import { coreSelector } from '../../core.selectors';
import {
FieldState,
FieldUpdates,
diff --git a/src/app/core/data/request.service.spec.ts b/src/app/core/data/request.service.spec.ts
index b3d9436d30e..c1b90289fcd 100644
--- a/src/app/core/data/request.service.spec.ts
+++ b/src/app/core/data/request.service.spec.ts
@@ -21,6 +21,8 @@ import {
import { RequestService } from './request.service';
import { TestScheduler } from 'rxjs/testing';
import { BehaviorSubject } from 'rxjs/internal/BehaviorSubject';
+import { MockStore } from '../../shared/testing/mock-store';
+import { MetaIndexState } from '../index/index.reducer';
describe('RequestService', () => {
let scheduler: TestScheduler;
diff --git a/src/app/core/data/request.service.ts b/src/app/core/data/request.service.ts
index f129f8becd8..97bc4ea1a2d 100644
--- a/src/app/core/data/request.service.ts
+++ b/src/app/core/data/request.service.ts
@@ -3,77 +3,88 @@ import { filter, mergeMap, take } from 'rxjs/operators';
import { Injectable } from '@angular/core';
import { createSelector, MemoizedSelector, select, Store } from '@ngrx/store';
+import { AppState } from '../../app.reducer';
import { hasValue, isNotEmpty } from '../../shared/empty.util';
import { CacheableObject } from '../cache/object-cache.reducer';
import { ObjectCacheService } from '../cache/object-cache.service';
-import { coreSelector, CoreState } from '../core.reducers';
-import { IndexName, IndexState } from '../index/index.reducer';
-import { pathSelector } from '../shared/selectors';
+import { CoreState } from '../core.reducers';
+import { IndexName, IndexState, MetaIndexState } from '../index/index.reducer';
+import {
+ originalRequestUUIDFromRequestUUIDSelector,
+ requestIndexSelector,
+ uuidFromHrefSelector
+} from '../index/index.selectors';
import { UUIDService } from '../shared/uuid.service';
import {
RequestConfigureAction,
RequestExecuteAction,
RequestRemoveAction
} from './request.actions';
-import { EndpointMapRequest, GetRequest, RestRequest } from './request.models';
+import { GetRequest, RestRequest } from './request.models';
-import { RequestEntry } from './request.reducer';
+import { RequestEntry, RequestState } from './request.reducer';
import { CommitSSBAction } from '../cache/server-sync-buffer.actions';
import { RestRequestMethod } from './rest-request-method';
import { AddToIndexAction, RemoveFromIndexBySubstringAction } from '../index/index.actions';
+import { coreSelector } from '../core.selectors';
-@Injectable()
-export class RequestService {
- private requestsOnTheirWayToTheStore: string[] = [];
+/**
+ * The base selector function to select the request state in the store
+ */
+const requestCacheSelector = createSelector(
+ coreSelector,
+ (state: CoreState) => state['data/request']
+);
- constructor(private objectCache: ObjectCacheService,
- private uuidService: UUIDService,
- private store: Store,
- private indexStore: Store) {
+/**
+ * Selector function to select a request entry by uuid from the cache
+ * @param uuid The uuid of the request
+ */
+const entryFromUUIDSelector = (uuid: string): MemoizedSelector => createSelector(
+ requestCacheSelector,
+ (state: RequestState) => {
+ return hasValue(state) ? state[uuid] : undefined;
}
+);
- private entryFromUUIDSelector(uuid: string): MemoizedSelector {
- return pathSelector(coreSelector, 'data/request', uuid);
- }
-
- private uuidFromHrefSelector(href: string): MemoizedSelector {
- return pathSelector(coreSelector, 'index', IndexName.REQUEST, href);
- }
+/**
+ * Create a selector that fetches a list of request UUIDs from a given index substate of which the request href
+ * contains a given substring
+ * @param selector MemoizedSelector to start from
+ * @param name The name of the index substate we're fetching request UUIDs from
+ * @param href Substring that the request's href should contain
+ */
+const uuidsFromHrefSubstringSelector =
+ (selector: MemoizedSelector, href: string): MemoizedSelector => createSelector(
+ selector,
+ (state: IndexState) => getUuidsFromHrefSubstring(state, href)
+ );
- private originalUUIDFromUUIDSelector(uuid: string): MemoizedSelector {
- return pathSelector(coreSelector, 'index', IndexName.UUID_MAPPING, uuid);
+/**
+ * Fetch a list of request UUIDs from a given index substate of which the request href contains a given substring
+ * @param state The IndexState
+ * @param href Substring that the request's href should contain
+ */
+const getUuidsFromHrefSubstring = (state: IndexState, href: string): string[] => {
+ let result = [];
+ if (isNotEmpty(state)) {
+ result = Object.values(state)
+ .filter((value: string) => value.startsWith(href));
}
+ return result;
+};
- /**
- * Create a selector that fetches a list of request UUIDs from a given index substate of which the request href
- * contains a given substring
- * @param selector MemoizedSelector to start from
- * @param name The name of the index substate we're fetching request UUIDs from
- * @param href Substring that the request's href should contain
- */
- private uuidsFromHrefSubstringSelector(selector: MemoizedSelector, name: string, href: string): MemoizedSelector {
- return createSelector(selector, (state: IndexState) => this.getUuidsFromHrefSubstring(state, name, href));
- }
+/**
+ * A service to interact with the request state in the store
+ */
+@Injectable()
+export class RequestService {
+ private requestsOnTheirWayToTheStore: string[] = [];
- /**
- * Fetch a list of request UUIDs from a given index substate of which the request href contains a given substring
- * @param state The IndexState
- * @param name The name of the index substate we're fetching request UUIDs from
- * @param href Substring that the request's href should contain
- */
- private getUuidsFromHrefSubstring(state: IndexState, name: string, href: string): string[] {
- let result = [];
- if (isNotEmpty(state)) {
- const subState = state[name];
- if (isNotEmpty(subState)) {
- for (const value in subState) {
- if (value.indexOf(href) > -1) {
- result = [...result, subState[value]];
- }
- }
- }
- }
- return result;
+ constructor(private objectCache: ObjectCacheService,
+ private uuidService: UUIDService,
+ private store: Store,
+ private indexStore: Store) {
}
generateRequestId(): string {
@@ -104,11 +115,11 @@ export class RequestService {
*/
getByUUID(uuid: string): Observable {
return observableRace(
- this.store.pipe(select(this.entryFromUUIDSelector(uuid))),
+ this.store.pipe(select(entryFromUUIDSelector(uuid))),
this.store.pipe(
- select(this.originalUUIDFromUUIDSelector(uuid)),
+ select(originalRequestUUIDFromRequestUUIDSelector(uuid)),
mergeMap((originalUUID) => {
- return this.store.pipe(select(this.entryFromUUIDSelector(originalUUID)))
+ return this.store.pipe(select(entryFromUUIDSelector(originalUUID)))
},
))
);
@@ -119,7 +130,7 @@ export class RequestService {
*/
getByHref(href: string): Observable {
return this.store.pipe(
- select(this.uuidFromHrefSelector(href)),
+ select(uuidFromHrefSelector(href)),
mergeMap((uuid: string) => this.getByUUID(uuid))
);
}
@@ -156,7 +167,7 @@ export class RequestService {
*/
removeByHrefSubstring(href: string) {
this.store.pipe(
- select(this.uuidsFromHrefSubstringSelector(pathSelector(coreSelector, 'index'), IndexName.REQUEST, href)),
+ select(uuidsFromHrefSubstringSelector(requestIndexSelector, href)),
take(1)
).subscribe((uuids: string[]) => {
for (const uuid of uuids) {
diff --git a/src/app/core/index/index.reducer.spec.ts b/src/app/core/index/index.reducer.spec.ts
index d1403ac5bf0..ef46c760c6e 100644
--- a/src/app/core/index/index.reducer.spec.ts
+++ b/src/app/core/index/index.reducer.spec.ts
@@ -1,6 +1,6 @@
import * as deepFreeze from 'deep-freeze';
-import { IndexName, indexReducer, IndexState } from './index.reducer';
+import { IndexName, indexReducer, MetaIndexState } from './index.reducer';
import { AddToIndexAction, RemoveFromIndexBySubstringAction, RemoveFromIndexByValueAction } from './index.actions';
class NullAction extends AddToIndexAction {
@@ -17,7 +17,7 @@ describe('requestReducer', () => {
const key2 = '1911e8a4-6939-490c-b58b-a5d70f8d91fb';
const val1 = 'https://dspace7.4science.it/dspace-spring-rest/api/core/items/567a639f-f5ff-4126-807c-b7d0910808c8';
const val2 = 'https://dspace7.4science.it/dspace-spring-rest/api/core/items/1911e8a4-6939-490c-b58b-a5d70f8d91fb';
- const testState: IndexState = {
+ const testState: MetaIndexState = {
[IndexName.OBJECT]: {
[key1]: val1
},[IndexName.REQUEST]: {
diff --git a/src/app/core/index/index.reducer.ts b/src/app/core/index/index.reducer.ts
index 3597c786d8d..b4cd8aa84b7 100644
--- a/src/app/core/index/index.reducer.ts
+++ b/src/app/core/index/index.reducer.ts
@@ -1,26 +1,57 @@
import {
+ AddToIndexAction,
IndexAction,
IndexActionTypes,
- AddToIndexAction,
- RemoveFromIndexByValueAction, RemoveFromIndexBySubstringAction
+ RemoveFromIndexBySubstringAction,
+ RemoveFromIndexByValueAction
} from './index.actions';
+/**
+ * An enum containing all index names
+ */
export enum IndexName {
+ // Contains all objects in the object cache indexed by UUID
OBJECT = 'object/uuid-to-self-link',
+
+ // contains all requests in the request cache indexed by UUID
REQUEST = 'get-request/href-to-uuid',
+
+ /**
+ * Contains the UUIDs of requests that were sent to the server and
+ * have their responses cached, indexed by the UUIDs of requests that
+ * weren't sent because the response they requested was already cached
+ */
UUID_MAPPING = 'get-request/configured-to-cache-uuid'
}
-export type IndexState = {
- [name in IndexName]: {
- [key: string]: string
- }
+/**
+ * The state of a single index
+ */
+export interface IndexState {
+ [key: string]: string
+}
+
+/**
+ * The state that contains all indices
+ */
+export type MetaIndexState = {
+ [name in IndexName]: IndexState
}
// Object.create(null) ensures the object has no default js properties (e.g. `__proto__`)
-const initialState: IndexState = Object.create(null);
+const initialState: MetaIndexState = Object.create(null);
-export function indexReducer(state = initialState, action: IndexAction): IndexState {
+/**
+ * The Index Reducer
+ *
+ * @param state
+ * the current state
+ * @param action
+ * the action to perform on the state
+ * @return MetaIndexState
+ * the new state
+ */
+export function indexReducer(state = initialState, action: IndexAction): MetaIndexState {
switch (action.type) {
case IndexActionTypes.ADD: {
@@ -41,7 +72,17 @@ export function indexReducer(state = initialState, action: IndexAction): IndexSt
}
}
-function addToIndex(state: IndexState, action: AddToIndexAction): IndexState {
+/**
+ * Add an entry to a given index
+ *
+ * @param state
+ * The MetaIndexState that contains all indices
+ * @param action
+ * The AddToIndexAction containing the value to add, and the index to add it to
+ * @return MetaIndexState
+ * the new state
+ */
+function addToIndex(state: MetaIndexState, action: AddToIndexAction): MetaIndexState {
const subState = state[action.payload.name];
const newSubState = Object.assign({}, subState, {
[action.payload.key]: action.payload.value
@@ -52,7 +93,17 @@ function addToIndex(state: IndexState, action: AddToIndexAction): IndexState {
return obs;
}
-function removeFromIndexByValue(state: IndexState, action: RemoveFromIndexByValueAction): IndexState {
+/**
+ * Remove a entries that contain a given value from a given index
+ *
+ * @param state
+ * The MetaIndexState that contains all indices
+ * @param action
+ * The RemoveFromIndexByValueAction containing the value to remove, and the index to remove it from
+ * @return MetaIndexState
+ * the new state
+ */
+function removeFromIndexByValue(state: MetaIndexState, action: RemoveFromIndexByValueAction): MetaIndexState {
const subState = state[action.payload.name];
const newSubState = Object.create(null);
for (const value in subState) {
@@ -66,11 +117,16 @@ function removeFromIndexByValue(state: IndexState, action: RemoveFromIndexByValu
}
/**
- * Remove values from the IndexState's substate that contain a given substring
- * @param state The IndexState to remove values from
- * @param action The RemoveFromIndexByValueAction containing the necessary information to remove the values
+ * Remove entries that contain a given substring from a given index
+ *
+ * @param state
+ * The MetaIndexState that contains all indices
+ * @param action
+ * The RemoveFromIndexByValueAction the substring to remove, and the index to remove it from
+ * @return MetaIndexState
+ * the new state
*/
-function removeFromIndexBySubstring(state: IndexState, action: RemoveFromIndexByValueAction): IndexState {
+function removeFromIndexBySubstring(state: MetaIndexState, action: RemoveFromIndexByValueAction): MetaIndexState {
const subState = state[action.payload.name];
const newSubState = Object.create(null);
for (const value in subState) {
diff --git a/src/app/core/index/index.selectors.ts b/src/app/core/index/index.selectors.ts
new file mode 100644
index 00000000000..3c7b331a926
--- /dev/null
+++ b/src/app/core/index/index.selectors.ts
@@ -0,0 +1,94 @@
+import { createSelector, MemoizedSelector } from '@ngrx/store';
+import { AppState } from '../../app.reducer';
+import { hasValue } from '../../shared/empty.util';
+import { CoreState } from '../core.reducers';
+import { coreSelector } from '../core.selectors';
+import { IndexName, IndexState, MetaIndexState } from './index.reducer';
+
+/**
+ * Return the MetaIndexState based on the CoreSate
+ *
+ * @returns
+ * a MemoizedSelector to select the MetaIndexState
+ */
+export const metaIndexSelector: MemoizedSelector = createSelector(
+ coreSelector,
+ (state: CoreState) => state.index
+);
+
+/**
+ * Return the object index based on the MetaIndexState
+ * It contains all objects in the object cache indexed by UUID
+ *
+ * @returns
+ * a MemoizedSelector to select the object index
+ */
+export const objectIndexSelector: MemoizedSelector = createSelector(
+ metaIndexSelector,
+ (state: MetaIndexState) => state[IndexName.OBJECT]
+);
+
+/**
+ * Return the request index based on the MetaIndexState
+ *
+ * @returns
+ * a MemoizedSelector to select the request index
+ */
+export const requestIndexSelector: MemoizedSelector = createSelector(
+ metaIndexSelector,
+ (state: MetaIndexState) => state[IndexName.REQUEST]
+);
+
+/**
+ * Return the request UUID mapping index based on the MetaIndexState
+ *
+ * @returns
+ * a MemoizedSelector to select the request UUID mapping
+ */
+export const requestUUIDIndexSelector: MemoizedSelector = createSelector(
+ metaIndexSelector,
+ (state: MetaIndexState) => state[IndexName.UUID_MAPPING]
+);
+
+/**
+ * Return the self link of an object in the object-cache based on its UUID
+ *
+ * @param uuid
+ * the UUID for which you want to find the matching self link
+ * @returns
+ * a MemoizedSelector to select the self link
+ */
+export const selfLinkFromUuidSelector =
+ (uuid: string): MemoizedSelector => createSelector(
+ objectIndexSelector,
+ (state: IndexState) => hasValue(state) ? state[uuid] : undefined
+ );
+
+/**
+ * Return the UUID of a GET request based on its href
+ *
+ * @param href
+ * the href of the GET request
+ * @returns
+ * a MemoizedSelector to select the UUID
+ */
+export const uuidFromHrefSelector =
+ (href: string): MemoizedSelector => createSelector(
+ requestIndexSelector,
+ (state: IndexState) => hasValue(state) ? state[href] : undefined
+ );
+
+/**
+ * Return the UUID of a cached request based on the UUID of a request
+ * that wasn't sent because the response was already cached
+ *
+ * @param uuid
+ * The UUID of the new request
+ * @returns
+ * a MemoizedSelector to select the UUID of the cached request
+ */
+export const originalRequestUUIDFromRequestUUIDSelector =
+ (uuid: string): MemoizedSelector => createSelector(
+ requestUUIDIndexSelector,
+ (state: IndexState) => hasValue(state) ? state[uuid] : undefined
+ );
diff --git a/src/app/core/shared/selectors.ts b/src/app/core/shared/selectors.ts
deleted file mode 100644
index 7bd35d39c16..00000000000
--- a/src/app/core/shared/selectors.ts
+++ /dev/null
@@ -1,17 +0,0 @@
-import { createSelector, MemoizedSelector } from '@ngrx/store';
-import { hasNoValue, isEmpty } from '../../shared/empty.util';
-
-export function pathSelector(selector: MemoizedSelector, ...path: string[]): MemoizedSelector {
- return createSelector(selector, (state: any) => getSubState(state, path));
-}
-
-function getSubState(state: any, path: string[]) {
- const current = path[0];
- const remainingPath = path.slice(1);
- const subState = state[current];
- if (hasNoValue(subState) || isEmpty(remainingPath)) {
- return subState;
- } else {
- return getSubState(subState, remainingPath);
- }
-}
diff --git a/src/app/shared/error/error.component.ts b/src/app/shared/error/error.component.ts
index 6900869183a..bcb30c5fdc2 100644
--- a/src/app/shared/error/error.component.ts
+++ b/src/app/shared/error/error.component.ts
@@ -32,5 +32,4 @@ export class ErrorComponent {
this.subscription.unsubscribe();
}
}
-
}
diff --git a/src/app/shared/services/route.service.ts b/src/app/shared/services/route.service.ts
index 9dd9a0f1641..3e62064d916 100644
--- a/src/app/shared/services/route.service.ts
+++ b/src/app/shared/services/route.service.ts
@@ -1,4 +1,4 @@
-import { distinctUntilChanged, map } from 'rxjs/operators';
+import { distinctUntilChanged, map, tap } from 'rxjs/operators';
import { Injectable } from '@angular/core';
import { Observable } from 'rxjs';
import {
@@ -6,20 +6,32 @@ import {
Router,
} from '@angular/router';
import { isNotEmpty } from '../empty.util';
+import { detect } from 'rxjs-spy';
+/**
+ * Service to keep track of the current query parameters
+ */
@Injectable()
export class RouteService {
constructor(private route: ActivatedRoute) {
}
+ /**
+ * Retrieves all query parameter values based on a parameter name
+ * @param paramName The name of the parameter to look for
+ */
getQueryParameterValues(paramName: string): Observable {
return this.route.queryParamMap.pipe(
map((params) => [...params.getAll(paramName)]),
- distinctUntilChanged()
+ distinctUntilChanged((a, b) => JSON.stringify(a) === JSON.stringify(b))
);
}
+ /**
+ * Retrieves a single query parameter values based on a parameter name
+ * @param paramName The name of the parameter to look for
+ */
getQueryParameterValue(paramName: string): Observable {
return this.route.queryParamMap.pipe(
map((params) => params.get(paramName)),
@@ -27,6 +39,10 @@ export class RouteService {
);
}
+ /**
+ * Checks if the query parameter currently exists in the route
+ * @param paramName The name of the parameter to look for
+ */
hasQueryParam(paramName: string): Observable {
return this.route.queryParamMap.pipe(
map((params) => params.has(paramName)),
@@ -34,6 +50,11 @@ export class RouteService {
);
}
+ /**
+ * Checks if the query parameter with a specific value currently exists in the route
+ * @param paramName The name of the parameter to look for
+ * @param paramValue The value of the parameter to look for
+ */
hasQueryParamWithValue(paramName: string, paramValue: string): Observable {
return this.route.queryParamMap.pipe(
map((params) => params.getAll(paramName).indexOf(paramValue) > -1),
@@ -41,6 +62,10 @@ export class RouteService {
);
}
+ /**
+ * Retrieves all query parameters of which the parameter name starts with the given prefix
+ * @param prefix The prefix of the parameter name to look for
+ */
getQueryParamsWithPrefix(prefix: string): Observable {
return this.route.queryParamMap.pipe(
map((qparams) => {
@@ -52,6 +77,7 @@ export class RouteService {
});
return params;
}),
- distinctUntilChanged((a, b) => JSON.stringify(a) === JSON.stringify(b)));
+ distinctUntilChanged((a, b) => JSON.stringify(a) === JSON.stringify(b)),
+ );
}
}
diff --git a/yarn.lock b/yarn.lock
index 7399f373e6a..8a923202f67 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -243,6 +243,10 @@
"@types/connect" "*"
"@types/node" "*"
+"@types/circular-json@^0.4.0":
+ version "0.4.0"
+ resolved "https://registry.yarnpkg.com/@types/circular-json/-/circular-json-0.4.0.tgz#7401f7e218cfe87ad4c43690da5658b9acaf51be"
+
"@types/connect@*":
version "3.4.32"
resolved "https://registry.yarnpkg.com/@types/connect/-/connect-3.4.32.tgz#aa0e9616b9435ccad02bc52b5b454ffc2c70ba28"
@@ -375,6 +379,10 @@
dependencies:
"@types/node" "*"
+"@types/stacktrace-js@^0.0.32":
+ version "0.0.32"
+ resolved "https://registry.yarnpkg.com/@types/stacktrace-js/-/stacktrace-js-0.0.32.tgz#d23e4a36a5073d39487fbea8234cc6186862d389"
+
"@types/strip-bom@^3.0.0":
version "3.0.0"
resolved "https://registry.yarnpkg.com/@types/strip-bom/-/strip-bom-3.0.0.tgz#14a8ec3956c2e81edb7520790aecf21c290aebd2"
@@ -1662,6 +1670,10 @@ cipher-base@^1.0.0, cipher-base@^1.0.1, cipher-base@^1.0.3:
inherits "^2.0.1"
safe-buffer "^5.0.1"
+circular-json@^0.5.0:
+ version "0.5.9"
+ resolved "https://registry.yarnpkg.com/circular-json/-/circular-json-0.5.9.tgz#932763ae88f4f7dead7a0d09c8a51a4743a53b1d"
+
circular-json@^0.5.5:
version "0.5.5"
resolved "https://registry.yarnpkg.com/circular-json/-/circular-json-0.5.5.tgz#64182ef359042d37cd8e767fc9de878b1e9447d3"
@@ -2842,6 +2854,12 @@ error-ex@^1.2.0, error-ex@^1.3.1:
dependencies:
is-arrayish "^0.2.1"
+error-stack-parser@^2.0.1:
+ version "2.0.2"
+ resolved "https://registry.yarnpkg.com/error-stack-parser/-/error-stack-parser-2.0.2.tgz#4ae8dbaa2bf90a8b450707b9149dcabca135520d"
+ dependencies:
+ stackframe "^1.0.4"
+
es-abstract@^1.12.0:
version "1.13.0"
resolved "https://registry.yarnpkg.com/es-abstract/-/es-abstract-1.13.0.tgz#ac86145fdd5099d8dd49558ccba2eaf9b88e24e9"
@@ -7912,6 +7930,16 @@ rx@^4.1.0:
version "4.1.0"
resolved "https://registry.yarnpkg.com/rx/-/rx-4.1.0.tgz#a5f13ff79ef3b740fe30aa803fb09f98805d4782"
+rxjs-spy@^7.5.1:
+ version "7.5.1"
+ resolved "https://registry.yarnpkg.com/rxjs-spy/-/rxjs-spy-7.5.1.tgz#1a9ef50bc8d7dd00d9ecf3c54c00929231eaf319"
+ dependencies:
+ "@types/circular-json" "^0.4.0"
+ "@types/stacktrace-js" "^0.0.32"
+ circular-json "^0.5.0"
+ error-stack-parser "^2.0.1"
+ stacktrace-gps "^3.0.2"
+
rxjs@6.2.2, rxjs@^6.0.0, rxjs@^6.1.0:
version "6.2.2"
resolved "https://registry.yarnpkg.com/rxjs/-/rxjs-6.2.2.tgz#eb75fa3c186ff5289907d06483a77884586e1cf9"
@@ -8356,6 +8384,10 @@ source-map@0.5.0:
version "0.5.0"
resolved "https://registry.yarnpkg.com/source-map/-/source-map-0.5.0.tgz#0fe96503ac86a5adb5de63f4e412ae4872cdbe86"
+source-map@0.5.6:
+ version "0.5.6"
+ resolved "https://registry.yarnpkg.com/source-map/-/source-map-0.5.6.tgz#75ce38f52bf0733c5a7f0c118d81334a2bb5f412"
+
source-map@0.7.3:
version "0.7.3"
resolved "https://registry.yarnpkg.com/source-map/-/source-map-0.7.3.tgz#5302f8169031735226544092e64981f751750383"
@@ -8481,6 +8513,17 @@ stable@^0.1.8:
resolved "https://registry.yarnpkg.com/stable/-/stable-0.1.8.tgz#836eb3c8382fe2936feaf544631017ce7d47a3cf"
integrity sha512-ji9qxRnOVfcuLDySj9qzhGSEFVobyt1kIOSkj1qZzYLzq7Tos/oUUWvotUPQLlrsidqsK6tBH89Bc9kL5zHA6w==
+stackframe@^1.0.4:
+ version "1.0.4"
+ resolved "https://registry.yarnpkg.com/stackframe/-/stackframe-1.0.4.tgz#357b24a992f9427cba6b545d96a14ed2cbca187b"
+
+stacktrace-gps@^3.0.2:
+ version "3.0.2"
+ resolved "https://registry.yarnpkg.com/stacktrace-gps/-/stacktrace-gps-3.0.2.tgz#33f8baa4467323ab2bd1816efa279942ba431ccc"
+ dependencies:
+ source-map "0.5.6"
+ stackframe "^1.0.4"
+
static-extend@^0.1.1:
version "0.1.2"
resolved "https://registry.yarnpkg.com/static-extend/-/static-extend-0.1.2.tgz#60809c39cbff55337226fd5e0b520f341f1fb5c6"