optimizations links in facets

This commit is contained in:
lotte
2019-03-20 16:57:55 +01:00
parent db9861b495
commit fc73fc1d3e
28 changed files with 446 additions and 206 deletions

View File

@@ -119,6 +119,7 @@
"pem": "1.12.3",
"reflect-metadata": "0.1.12",
"rxjs": "6.2.2",
"rxjs-spy": "^7.5.1",
"sortablejs": "1.7.0",
"text-mask-core": "5.0.1",
"ts-loader": "^5.2.1",

View File

@@ -1,24 +1,10 @@
<div>
<div class="filters py-2">
<a *ngFor="let value of (selectedValues | async)" class="d-flex flex-row"
[routerLink]="[getSearchLink()]"
[queryParams]="getRemoveParams(value) | async" queryParamsHandling="merge">
<input type="checkbox" [checked]="true" class="my-1 align-self-stretch"/>
<span class="filter-value pl-1">{{value}}</span>
</a>
<ds-search-facet-selected-option *ngFor="let value of (selectedValues | async)" [selectedValue]="value" [filterConfig]="filterConfig"></ds-search-facet-selected-option>
<ng-container *ngFor="let page of (filterValues$ | async)?.payload">
<div [@facetLoad]="animationState">
<ng-container *ngFor="let value of page.page; trackBy: trackUpdate">
<a *ngIf="!(selectedValues | async).includes(value.value)" class="d-flex flex-row"
[routerLink]="[getSearchLink()]"
[queryParams]="getAddParams(value.value) | async" queryParamsHandling="merge">
<input type="checkbox" [checked]="false" class="my-1 align-self-stretch"/>
<span class="filter-value px-1">{{value.value}}</span>
<span class="float-right filter-value-count ml-auto">
<span class="badge badge-secondary badge-pill">{{value.count}}</span>
</span>
</a>
</ng-container>
<ds-search-facet-option *ngFor="let value of page.page; trackBy: trackUpdate" [filterConfig]="filterConfig" [filterValue]="value">
</ds-search-facet-option>
</div>
</ng-container>
<div class="clearfix toggle-more-filters">

View File

@@ -0,0 +1,9 @@
<a *ngIf="isVisible" class="d-flex flex-row"
[routerLink]="[getSearchLink()]"
[queryParams]="addQueryParams | async" queryParamsHandling="merge">
<input type="checkbox" [checked]="false" class="my-1 align-self-stretch"/>
<span class="filter-value px-1">{{filterValue.value}}</span>
<span class="float-right filter-value-count ml-auto">
<span class="badge badge-secondary badge-pill">{{filterValue.count}}</span>
</span>
</a>

View File

@@ -0,0 +1,78 @@
import { Observable, of as observableOf } from 'rxjs';
import { map } from 'rxjs/operators';
import { Component, Input, OnInit } from '@angular/core';
import { Router } from '@angular/router';
import { FacetValue } from '../../../../search-service/facet-value.model';
import { SearchFilterConfig } from '../../../../search-service/search-filter-config.model';
import { SearchService } from '../../../../search-service/search.service';
import { SearchFilterService } from '../../search-filter.service';
@Component({
selector: 'ds-search-facet-option',
templateUrl: './search-facet-option.component.html',
})
/**
* Represents a single option in a filter facet
*/
export class SearchFacetOptionComponent implements OnInit {
/**
* A single value for this component
*/
@Input() filterValue: FacetValue;
@Input() filterConfig: SearchFilterConfig;
/**
* Emits the active values for this filter
*/
selectedValues$: Observable<string[]>;
isVisible: Observable<boolean>;
addQueryParams;
constructor(protected searchService: SearchService,
protected filterService: SearchFilterService,
protected router: Router
) {
}
/**
* Initializes all observable instance variables and starts listening to them
*/
ngOnInit(): void {
this.selectedValues$ = this.filterService.getSelectedValuesForFilter(this.filterConfig);
this.isVisible = this.isChecked().pipe(map((checked: boolean) => !checked));
this.addQueryParams = this.getAddParams();
}
/**
* Checks if a value for this filter is currently active
*/
private isChecked(): Observable<boolean> {
return this.filterService.isFilterActiveWithValue(this.filterConfig.paramName, this.filterValue.value);
}
/**
* @returns {string} The base path to the search page
*/
getSearchLink() {
return this.searchService.getSearchLink();
}
/**
* Calculates the parameters that should change if a given value for this filter would be added to the active filters
* @param {string} value The value that is added for this filter
* @returns {Observable<any>} The changed filter parameters
*/
private getAddParams(): Observable<any> {
return this.selectedValues$.pipe(map((selectedValues) => {
return {
[this.filterConfig.paramName]: [...selectedValues, this.filterValue.value],
page: 1
};
}));
}
}

View File

@@ -0,0 +1,8 @@
<a *ngIf="isVisible" class="d-flex flex-row"
[routerLink]="[getSearchLink()]"
[queryParams]="changeQueryParams | async" queryParamsHandling="merge">
<span class="filter-value px-1">{{filterValue.value}}</span>
<span class="float-right filter-value-count ml-auto">
<span class="badge badge-secondary badge-pill">{{filterValue.count}}</span>
</span>
</a>

View File

@@ -0,0 +1,88 @@
import { Observable, of as observableOf } from 'rxjs';
import { map } from 'rxjs/operators';
import { Component, Input, OnInit } from '@angular/core';
import { Router } from '@angular/router';
import { FacetValue } from '../../../../search-service/facet-value.model';
import { SearchFilterConfig } from '../../../../search-service/search-filter-config.model';
import { SearchService } from '../../../../search-service/search.service';
import { SearchFilterService } from '../../search-filter.service';
import {
RANGE_FILTER_MAX_SUFFIX,
RANGE_FILTER_MIN_SUFFIX
} from '../../search-range-filter/search-range-filter.component';
const rangeDelimiter = '-';
@Component({
selector: 'ds-search-facet-range-option',
templateUrl: './search-facet-range-option.component.html',
})
/**
* Represents a single option in a filter facet
*/
export class SearchFacetRangeOptionComponent implements OnInit {
/**
* A single value for this component
*/
@Input() filterValue: FacetValue;
@Input() filterConfig: SearchFilterConfig;
/**
* Emits the active values for this filter
*/
selectedValues$: Observable<string[]>;
isVisible: Observable<boolean>;
changeQueryParams;
constructor(protected searchService: SearchService,
protected filterService: SearchFilterService,
protected router: Router
) {
}
/**
* Initializes all observable instance variables and starts listening to them
*/
ngOnInit(): void {
this.selectedValues$ = this.filterService.getSelectedValuesForFilter(this.filterConfig);
this.isVisible = this.isChecked().pipe(map((checked: boolean) => !checked));
this.changeQueryParams = this.getChangeParams();
}
/**
* Checks if a value for this filter is currently active
*/
private isChecked(): Observable<boolean> {
return this.filterService.isFilterActiveWithValue(this.filterConfig.paramName, this.filterValue.value);
}
/**
* @returns {string} The base path to the search page
*/
getSearchLink() {
return this.searchService.getSearchLink();
}
/**
* 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<any>} The changed filter parameters
*/
getChangeParams() {
const parts = this.filterValue.value.split(rangeDelimiter);
const min = parts.length > 1 ? parts[0].trim() : this.filterValue.value;
const max = parts.length > 1 ? parts[1].trim() : this.filterValue.value;
return observableOf(
{
[this.filterConfig.paramName + RANGE_FILTER_MIN_SUFFIX]: [min],
[this.filterConfig.paramName + RANGE_FILTER_MAX_SUFFIX]: [max],
page: 1
});
}
}

View File

@@ -0,0 +1,6 @@
<a class="d-flex flex-row"
[routerLink]="[getSearchLink()]"
[queryParams]="removeQueryParams | async" queryParamsHandling="merge">
<input type="checkbox" [checked]="true" class="my-1 align-self-stretch"/>
<span class="filter-value pl-1">{{selectedValue}}</span>
</a>

View File

@@ -0,0 +1,67 @@
import { Observable, of as observableOf } from 'rxjs';
import { map } from 'rxjs/operators';
import { Component, Input, OnInit } from '@angular/core';
import { Router } from '@angular/router';
import { FacetValue } from '../../../../search-service/facet-value.model';
import { SearchFilterConfig } from '../../../../search-service/search-filter-config.model';
import { SearchService } from '../../../../search-service/search.service';
import { SearchFilterService } from '../../search-filter.service';
@Component({
selector: 'ds-search-facet-selected-option',
templateUrl: './search-facet-selected-option.component.html',
})
/**
* Represents a single option in a filter facet
*/
export class SearchFacetSelectedOptionComponent implements OnInit {
/**
* A single value for this component
*/
@Input() selectedValue: string;
@Input() filterConfig: SearchFilterConfig;
/**
* Emits the active values for this filter
*/
selectedValues$: Observable<string[]>;
removeQueryParams;
constructor(protected searchService: SearchService,
protected filterService: SearchFilterService,
protected router: Router
) {
}
/**
* Initializes all observable instance variables and starts listening to them
*/
ngOnInit(): void {
this.selectedValues$ = this.filterService.getSelectedValuesForFilter(this.filterConfig);
this.removeQueryParams = this.getRemoveParams();
}
/**
* @returns {string} The base path to the search page
*/
getSearchLink() {
return this.searchService.getSearchLink();
}
/**
* Calculates the parameters that should change if a given value for this filter would be removed from the active filters
* @param {string} value The value that is removed for this filter
* @returns {Observable<any>} The changed filter parameters
*/
private getRemoveParams(): Observable<any> {
return this.selectedValues$.pipe(map((selectedValues) => {
return {
[this.filterConfig.paramName]: selectedValues.filter((v) => v !== this.selectedValue),
page: 1
};
}));
}
}

View File

@@ -1 +1 @@
<ng-container *ngComponentOutlet="getSearchFilter(); injector: objectInjector;"></ng-container>
<ng-container *ngComponentOutlet="searchFilter injector: objectInjector;"></ng-container>

View File

@@ -3,6 +3,8 @@ import { renderFilterType } from '../search-filter-type-decorator';
import { FilterType } from '../../../search-service/filter-type.model';
import { SearchFilterConfig } from '../../../search-service/search-filter-config.model';
import { FILTER_CONFIG } from '../search-filter.service';
import { GenericConstructor } from '../../../../core/shared/generic-constructor';
import { SearchFacetFilterComponent } from '../search-facet-filter/search-facet-filter.component';
@Component({
selector: 'ds-search-facet-filter-wrapper',
@@ -18,6 +20,7 @@ export class SearchFacetFilterWrapperComponent implements OnInit {
*/
@Input() filterConfig: SearchFilterConfig;
searchFilter: GenericConstructor<SearchFacetFilterComponent>;
/**
* Injector to inject a child component with the @Input parameters
*/
@@ -30,6 +33,7 @@ export class SearchFacetFilterWrapperComponent implements OnInit {
* Initialize and add the filter config to the injector
*/
ngOnInit(): void {
this.searchFilter = this.getSearchFilter();
this.objectInjector = Injector.create({
providers: [
{ provide: FILTER_CONFIG, useFactory: () => (this.filterConfig), deps: [] }
@@ -41,7 +45,7 @@ export class SearchFacetFilterWrapperComponent implements OnInit {
/**
* Find the correct component based on the filter config's type
*/
getSearchFilter() {
private getSearchFilter() {
const type: FilterType = this.filterConfig.type;
return renderFilterType(type);
}

View File

@@ -85,8 +85,11 @@ export class SearchFacetFilterComponent implements OnInit, OnDestroy {
* Initializes all observable instance variables and starts listening to them
*/
ngOnInit(): void {
console.log('renderSearchFacetFilterComponent')
this.filterValues$ = new BehaviorSubject(new RemoteData(true, false, undefined, undefined, undefined));
this.currentPage = this.getCurrentPage().pipe(distinctUntilChanged());
this.selectedValues = this.filterService.getSelectedValuesForFilter(this.filterConfig);
const searchOptions = this.searchConfigService.searchOptions;
this.subs.push(this.searchConfigService.searchOptions.subscribe(() => this.updateFilterValueList()));
@@ -190,6 +193,7 @@ export class SearchFacetFilterComponent implements OnInit, OnDestroy {
* @param data The string from the input field
*/
onSubmit(data: any) {
console.log('onsubmit');
this.selectedValues.pipe(take(1)).subscribe((selectedValues) => {
if (isNotEmpty(data)) {
this.router.navigate([this.getSearchLink()], {

View File

@@ -1,6 +1,7 @@
import { Action } from '@ngrx/store';
import { type } from '../../../shared/ngrx/type';
import { SearchFilterConfig } from '../../search-service/search-filter-config.model';
/**
* For each action type in an action group, make a simple
@@ -12,9 +13,8 @@ import { type } from '../../../shared/ngrx/type';
*/
export const SearchFilterActionTypes = {
COLLAPSE: type('dspace/search-filter/COLLAPSE'),
INITIAL_COLLAPSE: type('dspace/search-filter/INITIAL_COLLAPSE'),
INITIALIZE: type('dspace/search-filter/INITIALIZE'),
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'),
@@ -66,15 +66,13 @@ export class SearchFilterToggleAction extends SearchFilterAction {
/**
* Used to set the initial state of a filter to collapsed
*/
export class SearchFilterInitialCollapseAction extends SearchFilterAction {
type = SearchFilterActionTypes.INITIAL_COLLAPSE;
}
/**
* Used to set the initial state of a filter to expanded
*/
export class SearchFilterInitialExpandAction extends SearchFilterAction {
type = SearchFilterActionTypes.INITIAL_EXPAND;
export class SearchFilterInitializeAction extends SearchFilterAction {
type = SearchFilterActionTypes.INITIALIZE;
initiallyExpanded;
constructor(filter: SearchFilterConfig) {
super(filter.name);
this.initiallyExpanded = filter.isOpenByDefault;
}
}
/**

View File

@@ -1,7 +1,7 @@
<div>
<div *ngIf="active$ | async">
<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 fas float-right"
[ngClass]="(isCollapsed() | async) ? 'fa-plus' : 'fa-minus'"></span></div>
<div [@slide]="(isCollapsed() | async) ? 'collapsed' : 'expanded'" (@slide.start)="startSlide($event)" (@slide.done)="finishSlide($event)" class="search-filter-wrapper" [ngClass]="{'closed' : collapsed}">
[ngClass]="(collapsed$ | async) ? 'fa-plus' : 'fa-minus'"></span></div>
<div [@slide]="(collapsed$ | async) ? 'collapsed' : 'expanded'" (@slide.start)="startSlide($event)" (@slide.done)="finishSlide($event)" class="search-filter-wrapper" [ngClass]="{'closed' : closed}">
<ds-search-facet-filter-wrapper [filterConfig]="filter"></ds-search-facet-filter-wrapper>
</div>
</div>

View File

@@ -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,13 @@ export class SearchFilterComponent implements OnInit {
/**
* True when the filter is 100% collapsed in the UI
*/
collapsed;
closed = true;
collapsed$: Observable<boolean>;
constructor(private filterService: SearchFilterService) {
selectedValues$: Observable<string[]>;
active$: Observable<boolean>;
constructor(private filterService: SearchFilterService, private searchService: SearchService, private searchConfigService: SearchConfigurationService) {
}
/**
@@ -37,11 +42,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 +64,21 @@ export class SearchFilterComponent implements OnInit {
* Checks if the filter is currently collapsed
* @returns {Observable<boolean>} Emits true when the current state of the filter is collapsed, false when it's expanded
*/
isCollapsed(): Observable<boolean> {
private isCollapsed(): Observable<boolean> {
return this.filterService.isCollapsed(this.filter.name);
}
/**
* Changes the initial state to collapsed
* Sets the initial state of the filter
*/
initialCollapse() {
this.filterService.initialCollapse(this.filter.name);
this.collapsed = true;
}
/**
* Changes the initial state to expanded
*/
initialExpand() {
this.filterService.initialExpand(this.filter.name);
this.collapsed = false;
initializeFilter() {
this.filterService.initializeFilter(this.filter);
}
/**
* @returns {Observable<string[]>} Emits a list of all values that are currently active for this filter
*/
getSelectedValues(): Observable<string[]> {
private getSelectedValues(): Observable<string[]> {
return this.filterService.getSelectedValuesForFilter(this.filter);
}
@@ -90,7 +88,7 @@ export class SearchFilterComponent implements OnInit {
*/
finishSlide(event: any): void {
if (event.fromState === 'collapsed') {
this.collapsed = false;
this.closed = false;
}
}
@@ -100,7 +98,32 @@ 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<boolean>} Emits true whenever a given filter config should be shown
*/
private isActive(): Observable<boolean> {
return this.selectedValues$.pipe(
switchMap((isActive) => {
if (isNotEmpty(isActive)) {
return observableOf(true);
} else {
return this.searchConfigService.searchOptions.pipe(
first(),
switchMap((options) => {
return this.searchService.getFacetValuesFor(this.filter, 1, options).pipe(
filter((RD) => !RD.isLoading),
map((valuesRD) => {
return valuesRD.payload.totalElements > 0
}),)
}
))
}
}),
startWith(true));
}
}

View File

@@ -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;
}

View File

@@ -5,8 +5,7 @@ import {
SearchFilterDecrementPageAction,
SearchFilterExpandAction,
SearchFilterIncrementPageAction,
SearchFilterInitialCollapseAction,
SearchFilterInitialExpandAction,
SearchFilterInitializeAction,
SearchFilterResetPageAction,
SearchFilterToggleAction
} from './search-filter.actions';
@@ -62,25 +61,16 @@ 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));
it('SearchFilterInitializeAction should be dispatched to the store', () => {
expect(store.dispatch).toHaveBeenCalledWith(new SearchFilterInitializeAction(mockFilterConfig));
});
});
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(() => {

View File

@@ -1,6 +1,6 @@
import { combineLatest as observableCombineLatest, Observable } from 'rxjs';
import { Injectable, InjectionToken } from '@angular/core';
import { distinctUntilChanged, map } from 'rxjs/operators';
import { distinctUntilChanged, map, tap } 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,9 @@ 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 { tag } from 'rxjs-spy/operators';
import { create, detect } from "rxjs-spy";
const spy = create();
const filterStateSelector = (state: SearchFiltersState) => state.searchFilter;
export const FILTER_CONFIG: InjectionToken<SearchFilterConfig> = new InjectionToken<SearchFilterConfig>('filterConfig');
@@ -58,16 +59,21 @@ export class SearchFilterService {
* @returns {Observable<string[]>} Emits the active filters for the given filter configuration
*/
getSelectedValuesForFilter(filterConfig: SearchFilterConfig): Observable<string[]> {
const values$ = this.routeService.getQueryParameterValues(filterConfig.paramName);
const prefixValues$ = this.routeService.getQueryParamsWithPrefix(filterConfig.paramName + '.').pipe(
map((params: Params) => [].concat(...Object.values(params)))
const values$ = this.routeService.getQueryParameterValues(filterConfig.paramName).pipe(
tag("parameter")
);
const prefixValues$ = this.routeService.getQueryParamsWithPrefix(filterConfig.paramName + '.').pipe(
map((params: Params) => [].concat(...Object.values(params))),
tag("prefix-tag")
);
spy.log();
detect('prefix-tag');
return observableCombineLatest(values$, prefixValues$).pipe(
map(([values, prefixValues]) => {
console.log('getSelectedValuesForFilter ', values, prefixValues);
if (isNotEmpty(values)) {
if (isNotEmpty(values)) {
return values;
}
return prefixValues;
@@ -138,19 +144,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
* Dispatches an initialize action to the store for a given filter
* @param {SearchFilterConfig} filter 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
*/
public initialExpand(filterName: string): void {
this.store.dispatch(new SearchFilterInitialExpandAction(filterName));
public initializeFilter(filter: SearchFilterConfig): void {
this.store.dispatch(new SearchFilterInitializeAction(filter));
}
/**

View File

@@ -8,17 +8,8 @@
</a>
<ng-container *ngFor="let page of (filterValues$ | async)?.payload">
<div [@facetLoad]="animationState">
<ng-container *ngFor="let value of page.page; trackBy: trackUpdate">
<a *ngIf="!(selectedValues | async).includes(value.value)" class="d-flex flex-row"
[routerLink]="[getSearchLink()]"
[queryParams]="getAddParams(value.value) | async" queryParamsHandling="merge" >
<input type="checkbox" [checked]="false" class="my-1 align-self-stretch"/>
<span class="filter-value px-1">{{value.value}}</span>
<span class="float-right filter-value-count ml-auto">
<span class="badge badge-secondary badge-pill">{{value.count}}</span>
</span>
</a>
</ng-container>
<ds-search-facet-option *ngFor="let value of page.page; trackBy: trackUpdate" [filterConfig]="filterConfig" [filterValue]="value">
</ds-search-facet-option>
</div>
</ng-container>
<div class="clearfix toggle-more-filters">

View File

@@ -25,14 +25,7 @@
<ng-container *ngFor="let page of (filterValues$ | async)?.payload">
<div [@facetLoad]="animationState">
<ng-container *ngFor="let value of page.page; trackBy: trackUpdate">
<a *ngIf="!(selectedValues | async).includes(value.value)" class="d-flex flex-row"
[routerLink]="[getSearchLink()]"
[queryParams]="getChangeParams(value.value) | async" queryParamsHandling="merge">
<span class="filter-value px-1">{{value.value}}</span>
<span class="float-right filter-value-count ml-auto">
<span class="badge badge-secondary badge-pill">{{value.count}}</span>
</span>
</a>
</ng-container>
</div>
</ng-container>

View File

@@ -28,10 +28,9 @@ import { SearchConfigurationService } from '../../../search-service/search-confi
* 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';
export const RANGE_FILTER_MIN_SUFFIX = '.min';
export const RANGE_FILTER_MAX_SUFFIX = '.max';
const dateFormats = ['YYYY', 'YYYY-MM', 'YYYY-MM-DD'];
const rangeDelimiter = '-';
@Component({
selector: 'ds-search-range-filter',
@@ -85,8 +84,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,22 +95,7 @@ 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<any>} 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 +106,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'
});

View File

@@ -10,15 +10,8 @@
<div [@facetLoad]="animationState">
<ng-container *ngFor="let page of filterValuesRD?.payload">
<ng-container *ngFor="let value of page.page; trackBy: trackUpdate">
<a *ngIf="!(selectedValues | async).includes(value.value)" class="d-flex flex-row"
[routerLink]="[getSearchLink()]"
[queryParams]="getAddParams(value.value) | async" queryParamsHandling="merge" >
<input type="checkbox" [checked]="false" class="my-1 align-self-stretch"/>
<span class="filter-value px-1">{{value.value}}</span>
<span class="float-right filter-value-count ml-auto">
<span class="badge badge-secondary badge-pill">{{value.count}}</span>
</span>
</a>
<ds-search-facet-option *ngFor="let value of page.page; trackBy: trackUpdate" [filterConfig]="filterConfig" [filterValue]="value">
</ds-search-facet-option>
</ng-container>
</ng-container>
</div>
@@ -40,6 +33,5 @@
(submitSuggestion)="onSubmit($event)"
(clickSuggestion)="onClick($event)"
(findSuggestions)="findSuggestions($event)"
ngDefaultControl
></ds-input-suggestions>
ngDefaultControl></ds-input-suggestions>
</div>

View File

@@ -1,7 +1,7 @@
<h3>{{"search.filters.head" | translate}}</h3>
<div *ngIf="(filters | async)?.hasSucceeded">
<div *ngFor="let filter of (filters | async)?.payload; trackBy: trackUpdate">
<ds-search-filter *ngIf="isActive(filter) | async" class="d-block mb-3 p-3" [filter]="filter"></ds-search-filter>
<ds-search-filter class="d-block mb-3 p-3" [filter]="filter"></ds-search-filter>
</div>
</div>
<a class="btn btn-primary" [routerLink]="[getSearchLink()]" [queryParams]="clearParams | async" queryParamsHandling="merge" role="button">{{"search.filters.reset" | translate}}</a>

View File

@@ -1,15 +1,13 @@
import { Observable, of as observableOf } from 'rxjs';
import { Observable } from 'rxjs';
import { filter, first, map, mergeMap, startWith, switchMap, tap } 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';
import { FieldUpdate } from '../../core/data/object-updates/object-updates.reducer';
@Component({
selector: 'ds-search-filters',
@@ -53,32 +51,6 @@ export class SearchFiltersComponent {
return this.searchService.getSearchLink();
}
/**
* Check if a given filter is supposed to be shown or not
* @param {SearchFilterConfig} filter The filter to check for
* @returns {Observable<boolean>} Emits true whenever a given filter config should be shown
*/
isActive(filterConfig: SearchFilterConfig): Observable<boolean> {
return this.filterService.getSelectedValuesForFilter(filterConfig).pipe(
switchMap((isActive) => {
console.log('selected fires');
if (isNotEmpty(isActive)) {
return observableOf(true);
} else {
return this.searchConfigService.searchOptions.pipe(
first(),
switchMap((options) => {
return this.searchService.getFacetValuesFor(filterConfig, 1, options).pipe(
filter((RD) => !RD.isLoading),
map((valuesRD) => {
return valuesRD.payload.totalElements > 0
}),)
}
))
}
}), tap(t => console.log(t)), startWith(true));
}
/**
* Prevent unnecessary rerendering
*/

View File

@@ -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();
}

View File

@@ -28,6 +28,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
@@ -63,6 +66,9 @@ const effects = [
SearchTextFilterComponent,
SearchHierarchyFilterComponent,
SearchBooleanFilterComponent,
SearchFacetOptionComponent,
SearchFacetSelectedOptionComponent,
SearchFacetRangeOptionComponent
],
providers: [
SearchService,
@@ -82,6 +88,9 @@ const effects = [
SearchTextFilterComponent,
SearchHierarchyFilterComponent,
SearchBooleanFilterComponent,
SearchFacetOptionComponent,
SearchFacetSelectedOptionComponent,
SearchFacetRangeOptionComponent
]
})

View File

@@ -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,6 +6,7 @@ import {
Router,
} from '@angular/router';
import { isNotEmpty } from '../empty.util';
import { detect } from 'rxjs-spy';
@Injectable()
export class RouteService {
@@ -14,6 +15,7 @@ export class RouteService {
}
getQueryParameterValues(paramName: string): Observable<string[]> {
console.log('called');
return this.route.queryParamMap.pipe(
map((params) => [...params.getAll(paramName)]),
distinctUntilChanged((a, b) => JSON.stringify(a) === JSON.stringify(b))
@@ -44,6 +46,7 @@ export class RouteService {
getQueryParamsWithPrefix(prefix: string): Observable<Params> {
return this.route.queryParamMap.pipe(
map((qparams) => {
console.log('map');
const params = {};
qparams.keys
.filter((key) => key.startsWith(prefix))
@@ -52,6 +55,8 @@ export class RouteService {
});
return params;
}),
distinctUntilChanged((a, b) => JSON.stringify(a) === JSON.stringify(b)));
distinctUntilChanged((a, b) => { console.log('changed?', a, b, JSON.stringify(a) === JSON.stringify(b)); return JSON.stringify(a) === JSON.stringify(b)}),
tap((t) => console.log('changed'))
);
}
}

View File

@@ -12,6 +12,7 @@ import { BrowserAppModule } from './modules/app/browser-app.module';
import { ENV_CONFIG } from './config';
if (ENV_CONFIG.production) {
enableProdMode();
}

View File

@@ -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"
@@ -370,6 +374,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"
@@ -1600,6 +1608,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"
@@ -2605,6 +2617,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.4.3, es-abstract@^1.5.1:
version "1.12.0"
resolved "https://registry.yarnpkg.com/es-abstract/-/es-abstract-1.12.0.tgz#9dbbdd27c6856f0001421ca18782d786bf8a6165"
@@ -7237,6 +7255,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"
@@ -7681,6 +7709,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"
@@ -7801,6 +7833,17 @@ ssri@^5.2.4:
dependencies:
safe-buffer "^5.1.1"
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"