[CST-6494] Update filter values when a workflow action is dispatched

This commit is contained in:
Giuseppe Digilio
2022-09-21 09:54:10 +02:00
parent 6d3f3cad2f
commit e072cdf75b
26 changed files with 187 additions and 114 deletions

View File

@@ -1,4 +1,4 @@
import { combineLatest as observableCombineLatest, Observable } from 'rxjs'; import { BehaviorSubject, combineLatest as observableCombineLatest, Observable } from 'rxjs';
import { distinctUntilChanged, map } from 'rxjs/operators'; import { distinctUntilChanged, map } from 'rxjs/operators';
import { Injectable, InjectionToken } from '@angular/core'; import { Injectable, InjectionToken } from '@angular/core';
import { import {
@@ -26,6 +26,7 @@ const filterStateSelector = (state: SearchFiltersState) => state.searchFilter;
export const FILTER_CONFIG: InjectionToken<SearchFilterConfig> = new InjectionToken<SearchFilterConfig>('filterConfig'); export const FILTER_CONFIG: InjectionToken<SearchFilterConfig> = new InjectionToken<SearchFilterConfig>('filterConfig');
export const IN_PLACE_SEARCH: InjectionToken<boolean> = new InjectionToken<boolean>('inPlaceSearch'); export const IN_PLACE_SEARCH: InjectionToken<boolean> = new InjectionToken<boolean>('inPlaceSearch');
export const REFRESH_FILTER: InjectionToken<BehaviorSubject<any>> = new InjectionToken<boolean>('refreshFilters');
/** /**
* Service that performs all actions that have to do with search filters and facets * Service that performs all actions that have to do with search filters and facets

View File

@@ -262,9 +262,11 @@ export class SearchService implements OnDestroy {
* @param {number} valuePage The page number of the filter values * @param {number} valuePage The page number of the filter values
* @param {SearchOptions} searchOptions The search configuration for the current search * @param {SearchOptions} searchOptions The search configuration for the current search
* @param {string} filterQuery The optional query used to filter out filter values * @param {string} filterQuery The optional query used to filter out filter values
* @param useCachedVersionIfAvailable If this is true, the request will only be sent if there's
* no valid cached version. Defaults to true
* @returns {Observable<RemoteData<PaginatedList<FacetValue>>>} Emits the given page of facet values * @returns {Observable<RemoteData<PaginatedList<FacetValue>>>} Emits the given page of facet values
*/ */
getFacetValuesFor(filterConfig: SearchFilterConfig, valuePage: number, searchOptions?: SearchOptions, filterQuery?: string): Observable<RemoteData<FacetValues>> { getFacetValuesFor(filterConfig: SearchFilterConfig, valuePage: number, searchOptions?: SearchOptions, filterQuery?: string, useCachedVersionIfAvailable = true): Observable<RemoteData<FacetValues>> {
let href; let href;
const args: string[] = [`page=${valuePage - 1}`, `size=${filterConfig.pageSize}`]; const args: string[] = [`page=${valuePage - 1}`, `size=${filterConfig.pageSize}`];
if (hasValue(filterQuery)) { if (hasValue(filterQuery)) {
@@ -282,7 +284,7 @@ export class SearchService implements OnDestroy {
return FacetValueResponseParsingService; return FacetValueResponseParsingService;
} }
}); });
this.requestService.send(request, true); this.requestService.send(request, useCachedVersionIfAvailable);
return this.rdb.buildFromHref(href); return this.rdb.buildFromHref(href);
} }

View File

@@ -19,7 +19,7 @@
[importable]="importable" [importable]="importable"
[importConfig]="importConfig" [importConfig]="importConfig"
(importObject)="importObject.emit($event)" (importObject)="importObject.emit($event)"
(contentChange)="contentChange.emit()" (contentChange)="contentChange.emit($event)"
(prev)="goPrev()" (prev)="goPrev()"
(next)="goNext()" (next)="goNext()"
*ngIf="(currentMode$ | async) === viewModeEnum.ListElement"> *ngIf="(currentMode$ | async) === viewModeEnum.ListElement">
@@ -49,6 +49,7 @@
[context]="context" [context]="context"
[hidePaginationDetail]="hidePaginationDetail" [hidePaginationDetail]="hidePaginationDetail"
[showPaginator]="showPaginator" [showPaginator]="showPaginator"
(contentChange)="contentChange.emit($event)"
*ngIf="(currentMode$ | async) === viewModeEnum.DetailedListElement"> *ngIf="(currentMode$ | async) === viewModeEnum.DetailedListElement">
</ds-object-detail> </ds-object-detail>

View File

@@ -50,6 +50,11 @@ export class ObjectCollectionComponent implements OnInit {
@Input() hideGear = false; @Input() hideGear = false;
@Input() selectable = false; @Input() selectable = false;
@Input() selectionConfig: {repeatable: boolean, listId: string}; @Input() selectionConfig: {repeatable: boolean, listId: string};
/**
* Emit custom event for listable object custom actions.
*/
@Output() customEvent = new EventEmitter<any>();
@Output() deselectObject: EventEmitter<ListableObject> = new EventEmitter<ListableObject>(); @Output() deselectObject: EventEmitter<ListableObject> = new EventEmitter<ListableObject>();
@Output() selectObject: EventEmitter<ListableObject> = new EventEmitter<ListableObject>(); @Output() selectObject: EventEmitter<ListableObject> = new EventEmitter<ListableObject>();

View File

@@ -5,7 +5,9 @@ import { ListableObject } from '../listable-object.model';
import { GenericConstructor } from '../../../../core/shared/generic-constructor'; import { GenericConstructor } from '../../../../core/shared/generic-constructor';
import { Context } from '../../../../core/shared/context.model'; import { Context } from '../../../../core/shared/context.model';
import { ViewMode } from '../../../../core/shared/view-mode.model'; import { ViewMode } from '../../../../core/shared/view-mode.model';
import { ItemListElementComponent } from '../../../object-list/item-list-element/item-types/item/item-list-element.component'; import {
ItemListElementComponent
} from '../../../object-list/item-list-element/item-types/item/item-list-element.component';
import { ListableObjectDirective } from './listable-object.directive'; import { ListableObjectDirective } from './listable-object.directive';
import { TranslateModule } from '@ngx-translate/core'; import { TranslateModule } from '@ngx-translate/core';
import { By } from '@angular/platform-browser'; import { By } from '@angular/platform-browser';
@@ -146,7 +148,7 @@ describe('ListableObjectComponentLoaderComponent', () => {
expect((comp as any).instantiateComponent).not.toHaveBeenCalled(); expect((comp as any).instantiateComponent).not.toHaveBeenCalled();
(listableComponent as any).reloadedObject.emit(reloadedObject); (listableComponent as any).reloadedObject.emit(reloadedObject);
tick(); tick(200);
expect((comp as any).instantiateComponent).toHaveBeenCalledWith(reloadedObject); expect((comp as any).instantiateComponent).toHaveBeenCalledWith(reloadedObject);
})); }));
@@ -155,7 +157,7 @@ describe('ListableObjectComponentLoaderComponent', () => {
expect((comp as any).contentChange.emit).not.toHaveBeenCalled(); expect((comp as any).contentChange.emit).not.toHaveBeenCalled();
(listableComponent as any).reloadedObject.emit(reloadedObject); (listableComponent as any).reloadedObject.emit(reloadedObject);
tick(); tick(200);
expect((comp as any).contentChange.emit).toHaveBeenCalledWith(reloadedObject); expect((comp as any).contentChange.emit).toHaveBeenCalledWith(reloadedObject);
})); }));

View File

@@ -1,16 +1,16 @@
import { import {
Component, Component,
ComponentFactoryResolver, ComponentFactoryResolver,
ComponentRef,
ElementRef, ElementRef,
EventEmitter,
Input, Input,
OnChanges,
OnDestroy, OnDestroy,
OnInit, OnInit,
Output, Output,
ViewChild,
EventEmitter,
SimpleChanges, SimpleChanges,
OnChanges, ViewChild
ComponentRef
} from '@angular/core'; } from '@angular/core';
import { ListableObject } from '../listable-object.model'; import { ListableObject } from '../listable-object.model';
import { ViewMode } from '../../../../core/shared/view-mode.model'; import { ViewMode } from '../../../../core/shared/view-mode.model';
@@ -187,7 +187,10 @@ export class ListableObjectComponentLoaderComponent implements OnInit, OnChanges
this.compRef.destroy(); this.compRef.destroy();
this.object = reloadedObject; this.object = reloadedObject;
this.instantiateComponent(reloadedObject); this.instantiateComponent(reloadedObject);
// Add delay before emitting event to allow the new object is instantiated
setTimeout(() => {
this.contentChange.emit(reloadedObject); this.contentChange.emit(reloadedObject);
}, 100);
} }
}); });
} }

View File

@@ -14,12 +14,14 @@
(sortFieldChange)="onSortFieldChange($event)" (sortFieldChange)="onSortFieldChange($event)"
(paginationChange)="onPaginationChange($event)" (paginationChange)="onPaginationChange($event)"
(prev)="goPrev()" (prev)="goPrev()"
(next)="goNext()" (next)="goNext()">
>
<div class="row mt-2" *ngIf="objects?.hasSucceeded" @fadeIn> <div class="row mt-2" *ngIf="objects?.hasSucceeded" @fadeIn>
<div class="col" <div class="col"
*ngFor="let object of objects?.payload?.page"> *ngFor="let object of objects?.payload?.page">
<ds-listable-object-component-loader [object]="object" [viewMode]="viewMode" [context]="context"></ds-listable-object-component-loader> <ds-listable-object-component-loader [object]="object"
[viewMode]="viewMode"
[context]="context"
(contentChange)="contentChange.emit($event)"></ds-listable-object-component-loader>
</div> </div>
</div> </div>
<ds-error *ngIf="objects.hasFailed" message="{{'error.objects' | translate}}"></ds-error> <ds-error *ngIf="objects.hasFailed" message="{{'error.objects' | translate}}"></ds-error>

View File

@@ -1,11 +1,4 @@
import { import { ChangeDetectionStrategy, Component, EventEmitter, Input, Output, ViewEncapsulation } from '@angular/core';
ChangeDetectionStrategy,
Component,
EventEmitter,
Input,
Output,
ViewEncapsulation
} from '@angular/core';
import { SortDirection, SortOptions } from '../../core/cache/models/sort-options.model'; import { SortDirection, SortOptions } from '../../core/cache/models/sort-options.model';
import { PaginatedList } from '../../core/data/paginated-list.model'; import { PaginatedList } from '../../core/data/paginated-list.model';
@@ -71,6 +64,11 @@ export class ObjectDetailComponent {
*/ */
@Input() showPaginator = true; @Input() showPaginator = true;
/**
* Emit when one of the listed object has changed.
*/
@Output() contentChange = new EventEmitter<any>();
/** /**
* If showPaginator is set to true, emit when the previous button is clicked * If showPaginator is set to true, emit when the previous button is clicked
*/ */

View File

@@ -14,8 +14,7 @@
(sortFieldChange)="onSortFieldChange($event)" (sortFieldChange)="onSortFieldChange($event)"
(paginationChange)="onPaginationChange($event)" (paginationChange)="onPaginationChange($event)"
(prev)="goPrev()" (prev)="goPrev()"
(next)="goNext()" (next)="goNext()">
>
<ul *ngIf="objects?.hasSucceeded" class="list-unstyled" [ngClass]="{'ml-4': selectable}"> <ul *ngIf="objects?.hasSucceeded" class="list-unstyled" [ngClass]="{'ml-4': selectable}">
<li *ngFor="let object of objects?.payload?.page; let i = index; let last = last" class="mt-4 mb-4 d-flex" [class.border-bottom]="hasBorder && !last" [attr.data-test]="'list-object' | dsBrowserOnly"> <li *ngFor="let object of objects?.payload?.page; let i = index; let last = last" class="mt-4 mb-4 d-flex" [class.border-bottom]="hasBorder && !last" [attr.data-test]="'list-object' | dsBrowserOnly">
<ds-selectable-list-item-control *ngIf="selectable" [index]="i" <ds-selectable-list-item-control *ngIf="selectable" [index]="i"
@@ -28,8 +27,7 @@
(importObject)="importObject.emit($event)"></ds-importable-list-item-control> (importObject)="importObject.emit($event)"></ds-importable-list-item-control>
<ds-listable-object-component-loader [object]="object" [viewMode]="viewMode" [index]="i" [context]="context" [linkType]="linkType" <ds-listable-object-component-loader [object]="object" [viewMode]="viewMode" [index]="i" [context]="context" [linkType]="linkType"
[listID]="selectionConfig?.listId" [listID]="selectionConfig?.listId"
(contentChange)="contentChange.emit()" (contentChange)="contentChange.emit($event)"></ds-listable-object-component-loader>
></ds-listable-object-component-loader>
</li> </li>
</ul> </ul>
</ds-pagination> </ds-pagination>

View File

@@ -74,7 +74,7 @@ export class ObjectListComponent {
/** /**
* Config used for the import button * Config used for the import button
*/ */
@Input() importConfig: { importLabel: string }; @Input() importConfig: { buttonLabel: string };
/** /**
* Whether or not the pagination should be rendered as simple previous and next buttons instead of the normal pagination * Whether or not the pagination should be rendered as simple previous and next buttons instead of the normal pagination
@@ -221,4 +221,5 @@ export class ObjectListComponent {
goNext() { goNext() {
this.next.emit(true); this.next.emit(true);
} }
} }

View File

@@ -2,9 +2,14 @@ import { Component, Injector, Input, OnInit } from '@angular/core';
import { renderFilterType } from '../search-filter-type-decorator'; import { renderFilterType } from '../search-filter-type-decorator';
import { FilterType } from '../../../models/filter-type.model'; import { FilterType } from '../../../models/filter-type.model';
import { SearchFilterConfig } from '../../../models/search-filter-config.model'; import { SearchFilterConfig } from '../../../models/search-filter-config.model';
import { FILTER_CONFIG, IN_PLACE_SEARCH } from '../../../../../core/shared/search/search-filter.service'; import {
FILTER_CONFIG,
IN_PLACE_SEARCH,
REFRESH_FILTER
} from '../../../../../core/shared/search/search-filter.service';
import { GenericConstructor } from '../../../../../core/shared/generic-constructor'; import { GenericConstructor } from '../../../../../core/shared/generic-constructor';
import { SearchFacetFilterComponent } from '../search-facet-filter/search-facet-filter.component'; import { SearchFacetFilterComponent } from '../search-facet-filter/search-facet-filter.component';
import { BehaviorSubject } from 'rxjs';
@Component({ @Component({
selector: 'ds-search-facet-filter-wrapper', selector: 'ds-search-facet-filter-wrapper',
@@ -25,6 +30,11 @@ export class SearchFacetFilterWrapperComponent implements OnInit {
*/ */
@Input() inPlaceSearch; @Input() inPlaceSearch;
/**
* Emits when the search filters values may be stale, and so they must be refreshed.
*/
@Input() refreshFilters: BehaviorSubject<boolean>;
/** /**
* The constructor of the search facet filter that should be rendered, based on the filter config's type * The constructor of the search facet filter that should be rendered, based on the filter config's type
*/ */
@@ -45,7 +55,8 @@ export class SearchFacetFilterWrapperComponent implements OnInit {
this.objectInjector = Injector.create({ this.objectInjector = Injector.create({
providers: [ providers: [
{ provide: FILTER_CONFIG, useFactory: () => (this.filterConfig), deps: [] }, { provide: FILTER_CONFIG, useFactory: () => (this.filterConfig), deps: [] },
{ provide: IN_PLACE_SEARCH, useFactory: () => (this.inPlaceSearch), deps: [] } { provide: IN_PLACE_SEARCH, useFactory: () => (this.inPlaceSearch), deps: [] },
{ provide: REFRESH_FILTER, useFactory: () => (this.refreshFilters), deps: [] }
], ],
parent: this.injector parent: this.injector
}); });

View File

@@ -5,13 +5,14 @@ import { NoopAnimationsModule } from '@angular/platform-browser/animations';
import { import {
FILTER_CONFIG, FILTER_CONFIG,
IN_PLACE_SEARCH, IN_PLACE_SEARCH,
REFRESH_FILTER,
SearchFilterService SearchFilterService
} from '../../../../../core/shared/search/search-filter.service'; } from '../../../../../core/shared/search/search-filter.service';
import { SearchFilterConfig } from '../../../models/search-filter-config.model'; import { SearchFilterConfig } from '../../../models/search-filter-config.model';
import { FilterType } from '../../../models/filter-type.model'; import { FilterType } from '../../../models/filter-type.model';
import { FacetValue } from '../../../models/facet-value.model'; import { FacetValue } from '../../../models/facet-value.model';
import { FormsModule } from '@angular/forms'; import { FormsModule } from '@angular/forms';
import { of as observableOf } from 'rxjs'; import { BehaviorSubject, of as observableOf } from 'rxjs';
import { SearchService } from '../../../../../core/shared/search/search.service'; import { SearchService } from '../../../../../core/shared/search/search.service';
import { SearchServiceStub } from '../../../../testing/search-service.stub'; import { SearchServiceStub } from '../../../../testing/search-service.stub';
import { buildPaginatedList } from '../../../../../core/data/paginated-list.model'; import { buildPaginatedList } from '../../../../../core/data/paginated-list.model';
@@ -97,6 +98,7 @@ describe('SearchFacetFilterComponent', () => {
{ provide: RemoteDataBuildService, useValue: { aggregate: () => observableOf({}) } }, { provide: RemoteDataBuildService, useValue: { aggregate: () => observableOf({}) } },
{ provide: SEARCH_CONFIG_SERVICE, useValue: new SearchConfigurationServiceStub() }, { provide: SEARCH_CONFIG_SERVICE, useValue: new SearchConfigurationServiceStub() },
{ provide: IN_PLACE_SEARCH, useValue: false }, { provide: IN_PLACE_SEARCH, useValue: false },
{ provide: REFRESH_FILTER, useValue: new BehaviorSubject<boolean>(false) },
{ {
provide: SearchFilterService, useValue: { provide: SearchFilterService, useValue: {
getSelectedValuesForFilter: () => observableOf(selectedValues), getSelectedValuesForFilter: () => observableOf(selectedValues),

View File

@@ -6,7 +6,7 @@ import {
Subject, Subject,
Subscription Subscription
} from 'rxjs'; } from 'rxjs';
import { distinctUntilChanged, map, switchMap, take, tap } from 'rxjs/operators'; import { distinctUntilChanged, filter, map, switchMap, take, tap } from 'rxjs/operators';
import { animate, state, style, transition, trigger } from '@angular/animations'; import { animate, state, style, transition, trigger } from '@angular/animations';
import { Component, Inject, OnDestroy, OnInit } from '@angular/core'; import { Component, Inject, OnDestroy, OnInit } from '@angular/core';
import { Router } from '@angular/router'; import { Router } from '@angular/router';
@@ -21,6 +21,7 @@ import { SearchService } from '../../../../../core/shared/search/search.service'
import { import {
FILTER_CONFIG, FILTER_CONFIG,
IN_PLACE_SEARCH, IN_PLACE_SEARCH,
REFRESH_FILTER,
SearchFilterService SearchFilterService
} from '../../../../../core/shared/search/search-filter.service'; } from '../../../../../core/shared/search/search-filter.service';
import { SearchConfigurationService } from '../../../../../core/shared/search/search-configuration.service'; import { SearchConfigurationService } from '../../../../../core/shared/search/search-configuration.service';
@@ -98,7 +99,8 @@ export class SearchFacetFilterComponent implements OnInit, OnDestroy {
protected router: Router, protected router: Router,
@Inject(SEARCH_CONFIG_SERVICE) public searchConfigService: SearchConfigurationService, @Inject(SEARCH_CONFIG_SERVICE) public searchConfigService: SearchConfigurationService,
@Inject(IN_PLACE_SEARCH) public inPlaceSearch: boolean, @Inject(IN_PLACE_SEARCH) public inPlaceSearch: boolean,
@Inject(FILTER_CONFIG) public filterConfig: SearchFilterConfig) { @Inject(FILTER_CONFIG) public filterConfig: SearchFilterConfig,
@Inject(REFRESH_FILTER) public refreshFilters: BehaviorSubject<boolean>) {
} }
/** /**
@@ -110,66 +112,15 @@ export class SearchFacetFilterComponent implements OnInit, OnDestroy {
this.currentPage = this.getCurrentPage().pipe(distinctUntilChanged()); this.currentPage = this.getCurrentPage().pipe(distinctUntilChanged());
this.searchOptions$ = this.searchConfigService.searchOptions; this.searchOptions$ = this.searchConfigService.searchOptions;
this.subs.push(this.searchOptions$.subscribe(() => this.updateFilterValueList())); this.subs.push(
const facetValues$ = observableCombineLatest(this.searchOptions$, this.currentPage).pipe( this.searchOptions$.subscribe(() => this.updateFilterValueList()),
map(([options, page]) => { this.refreshFilters.asObservable().pipe(
return { options, page }; filter((toRefresh: boolean) => toRefresh),
}), ).subscribe(() => {
switchMap(({ options, page }) => { this.retrieveFilterValues(false);
return this.searchService.getFacetValuesFor(this.filterConfig, page, options)
.pipe(
getFirstSucceededRemoteData(),
map((results) => {
return {
values: observableOf(results),
page: page
};
}
)
);
}) })
); );
this.retrieveFilterValues();
let filterValues = [];
this.subs.push(facetValues$.subscribe((facetOutcome) => {
const newValues$ = facetOutcome.values;
if (this.collapseNextUpdate) {
this.showFirstPageOnly();
facetOutcome.page = 1;
this.collapseNextUpdate = false;
}
if (facetOutcome.page === 1) {
filterValues = [];
}
filterValues = [...filterValues, newValues$];
this.subs.push(this.rdbs.aggregate(filterValues).pipe(
tap((rd: RemoteData<PaginatedList<FacetValue>[]>) => {
this.selectedValues$ = this.filterService.getSelectedValuesForFilter(this.filterConfig).pipe(
map((selectedValues) => {
return selectedValues.map((value: string) => {
const fValue = [].concat(...rd.payload.map((page) => page.page)).find((facetValue: FacetValue) => this.getFacetValue(facetValue) === value);
if (hasValue(fValue)) {
return fValue;
}
const filterValue = stripOperatorFromFilterValue(value);
return Object.assign(new FacetValue(), { label: filterValue, value: filterValue });
});
})
);
})
).subscribe((rd: RemoteData<PaginatedList<FacetValue>[]>) => {
this.animationState = 'ready';
this.filterValues$.next(rd);
}));
this.subs.push(newValues$.pipe(take(1)).subscribe((rd) => {
this.isLastPage$.next(hasNoValue(rd.payload.next));
}));
}));
} }
/** /**
@@ -324,6 +275,67 @@ export class SearchFacetFilterComponent implements OnInit, OnDestroy {
return getFacetValueForType(facet, this.filterConfig); return getFacetValueForType(facet, this.filterConfig);
} }
protected retrieveFilterValues(useCachedVersionIfAvailable = true) {
const facetValues$ = observableCombineLatest([this.searchOptions$, this.currentPage]).pipe(
map(([options, page]) => {
return { options, page };
}),
switchMap(({ options, page }) => {
return this.searchService.getFacetValuesFor(this.filterConfig, page, options, null, useCachedVersionIfAvailable)
.pipe(
getFirstSucceededRemoteData(),
map((results) => {
return {
values: observableOf(results),
page: page
};
}
)
);
})
);
let filterValues = [];
this.subs.push(facetValues$.subscribe((facetOutcome) => {
const newValues$ = facetOutcome.values;
if (this.collapseNextUpdate) {
this.showFirstPageOnly();
facetOutcome.page = 1;
this.collapseNextUpdate = false;
}
if (facetOutcome.page === 1) {
filterValues = [];
}
filterValues = [...filterValues, newValues$];
this.subs.push(this.rdbs.aggregate(filterValues).pipe(
tap((rd: RemoteData<PaginatedList<FacetValue>[]>) => {
this.selectedValues$ = this.filterService.getSelectedValuesForFilter(this.filterConfig).pipe(
map((selectedValues) => {
return selectedValues.map((value: string) => {
const fValue = [].concat(...rd.payload.map((page) => page.page)).find((facetValue: FacetValue) => this.getFacetValue(facetValue) === value);
if (hasValue(fValue)) {
return fValue;
}
const filterValue = stripOperatorFromFilterValue(value);
return Object.assign(new FacetValue(), { label: filterValue, value: filterValue });
});
})
);
})
).subscribe((rd: RemoteData<PaginatedList<FacetValue>[]>) => {
this.animationState = 'ready';
this.filterValues$.next(rd);
}));
this.subs.push(newValues$.pipe(take(1)).subscribe((rd) => {
this.isLastPage$.next(hasNoValue(rd.payload.next));
}));
}));
}
/** /**
* Transforms the facet value string, so if the query matches part of the value, it's emphasized in the value * Transforms the facet value string, so if the query matches part of the value, it's emphasized in the value
* @param {FacetValue} facet The value of the facet as returned by the server * @param {FacetValue} facet The value of the facet as returned by the server

View File

@@ -19,7 +19,8 @@
class="search-filter-wrapper" [ngClass]="{ 'closed' : closed, 'notab': notab }"> class="search-filter-wrapper" [ngClass]="{ 'closed' : closed, 'notab': notab }">
<ds-search-facet-filter-wrapper <ds-search-facet-filter-wrapper
[filterConfig]="filter" [filterConfig]="filter"
[inPlaceSearch]="inPlaceSearch"> [inPlaceSearch]="inPlaceSearch"
[refreshFilters]="refreshFilters" >
</ds-search-facet-filter-wrapper> </ds-search-facet-filter-wrapper>
</div> </div>
</div> </div>

View File

@@ -1,6 +1,6 @@
import { Component, Inject, Input, OnInit } from '@angular/core'; import { Component, Inject, Input, OnInit } from '@angular/core';
import { Observable, of as observableOf } from 'rxjs'; import { BehaviorSubject, Observable, of as observableOf } from 'rxjs';
import { filter, map, startWith, switchMap, take } from 'rxjs/operators'; import { filter, map, startWith, switchMap, take } from 'rxjs/operators';
import { SearchFilterConfig } from '../../models/search-filter-config.model'; import { SearchFilterConfig } from '../../models/search-filter-config.model';
@@ -33,6 +33,11 @@ export class SearchFilterComponent implements OnInit {
*/ */
@Input() inPlaceSearch; @Input() inPlaceSearch;
/**
* Emits when the search filters values may be stale, and so they must be refreshed.
*/
@Input() refreshFilters: BehaviorSubject<boolean>;
/** /**
* True when the filter is 100% collapsed in the UI * True when the filter is 100% collapsed in the UI
*/ */

View File

@@ -1,17 +1,18 @@
import { TestBed, ComponentFixture } from '@angular/core/testing'; import { ComponentFixture, TestBed } from '@angular/core/testing';
import { SearchHierarchyFilterComponent } from './search-hierarchy-filter.component'; import { SearchHierarchyFilterComponent } from './search-hierarchy-filter.component';
import { SearchService } from '../../../../../core/shared/search/search.service'; import { SearchService } from '../../../../../core/shared/search/search.service';
import { import {
SearchFilterService,
FILTER_CONFIG, FILTER_CONFIG,
IN_PLACE_SEARCH IN_PLACE_SEARCH,
REFRESH_FILTER,
SearchFilterService
} from '../../../../../core/shared/search/search-filter.service'; } from '../../../../../core/shared/search/search-filter.service';
import { RemoteDataBuildService } from '../../../../../core/cache/builders/remote-data-build.service'; import { RemoteDataBuildService } from '../../../../../core/cache/builders/remote-data-build.service';
import { SearchFiltersComponent } from '../../search-filters.component'; import { SearchFiltersComponent } from '../../search-filters.component';
import { Router } from '@angular/router'; import { Router } from '@angular/router';
import { RouterStub } from '../../../../testing/router.stub'; import { RouterStub } from '../../../../testing/router.stub';
import { SearchServiceStub } from '../../../../testing/search-service.stub'; import { SearchServiceStub } from '../../../../testing/search-service.stub';
import { of as observableOf, Observable } from 'rxjs'; import { BehaviorSubject, Observable, of as observableOf } from 'rxjs';
import { SEARCH_CONFIG_SERVICE } from '../../../../../my-dspace-page/my-dspace-page.component'; import { SEARCH_CONFIG_SERVICE } from '../../../../../my-dspace-page/my-dspace-page.component';
import { SearchConfigurationServiceStub } from '../../../../testing/search-configuration-service.stub'; import { SearchConfigurationServiceStub } from '../../../../testing/search-configuration-service.stub';
import { SearchFilterConfig } from '../../../models/search-filter-config.model'; import { SearchFilterConfig } from '../../../models/search-filter-config.model';
@@ -21,7 +22,7 @@ import {
} from '../../../../input-suggestions/filter-suggestions/filter-input-suggestions.component'; } from '../../../../input-suggestions/filter-suggestions/filter-input-suggestions.component';
import { FormsModule } from '@angular/forms'; import { FormsModule } from '@angular/forms';
import { NoopAnimationsModule } from '@angular/platform-browser/animations'; import { NoopAnimationsModule } from '@angular/platform-browser/animations';
import { NO_ERRORS_SCHEMA, ChangeDetectionStrategy } from '@angular/core'; import { ChangeDetectionStrategy, NO_ERRORS_SCHEMA } from '@angular/core';
import { createSuccessfulRemoteDataObject$ } from '../../../../remote-data.utils'; import { createSuccessfulRemoteDataObject$ } from '../../../../remote-data.utils';
import { FacetValue } from '../../../models/facet-value.model'; import { FacetValue } from '../../../models/facet-value.model';
import { FilterType } from '../../../models/filter-type.model'; import { FilterType } from '../../../models/filter-type.model';
@@ -112,7 +113,8 @@ describe('SearchHierarchyFilterComponent', () => {
{ provide: Router, useValue: new RouterStub() }, { provide: Router, useValue: new RouterStub() },
{ provide: SEARCH_CONFIG_SERVICE, useValue: new SearchConfigurationServiceStub() }, { provide: SEARCH_CONFIG_SERVICE, useValue: new SearchConfigurationServiceStub() },
{ provide: IN_PLACE_SEARCH, useValue: false }, { provide: IN_PLACE_SEARCH, useValue: false },
{ provide: FILTER_CONFIG, useValue: new SearchFilterConfig() } { provide: FILTER_CONFIG, useValue: new SearchFilterConfig() },
{ provide: REFRESH_FILTER, useValue: new BehaviorSubject<boolean>(false) }
], ],
schemas: [NO_ERRORS_SCHEMA] schemas: [NO_ERRORS_SCHEMA]
}).overrideComponent(SearchHierarchyFilterComponent, { }).overrideComponent(SearchHierarchyFilterComponent, {
@@ -140,7 +142,7 @@ describe('SearchHierarchyFilterComponent', () => {
}); });
it('should navigate to the correct filter with the query operator', () => { it('should navigate to the correct filter with the query operator', () => {
expect((comp as any).searchService.getFacetValuesFor).toHaveBeenCalledWith(comp.filterConfig, 0, {}); expect((comp as any).searchService.getFacetValuesFor).toHaveBeenCalledWith(comp.filterConfig, 0, {}, null, true);
const searchQuery = 'MARVEL'; const searchQuery = 'MARVEL';
comp.onSubmit(searchQuery); comp.onSubmit(searchQuery);

View File

@@ -5,13 +5,14 @@ import { NoopAnimationsModule } from '@angular/platform-browser/animations';
import { import {
FILTER_CONFIG, FILTER_CONFIG,
IN_PLACE_SEARCH, IN_PLACE_SEARCH,
REFRESH_FILTER,
SearchFilterService SearchFilterService
} from '../../../../../core/shared/search/search-filter.service'; } from '../../../../../core/shared/search/search-filter.service';
import { SearchFilterConfig } from '../../../models/search-filter-config.model'; import { SearchFilterConfig } from '../../../models/search-filter-config.model';
import { FilterType } from '../../../models/filter-type.model'; import { FilterType } from '../../../models/filter-type.model';
import { FacetValue } from '../../../models/facet-value.model'; import { FacetValue } from '../../../models/facet-value.model';
import { FormsModule } from '@angular/forms'; import { FormsModule } from '@angular/forms';
import { of as observableOf } from 'rxjs'; import { BehaviorSubject, of as observableOf } from 'rxjs';
import { SearchService } from '../../../../../core/shared/search/search.service'; import { SearchService } from '../../../../../core/shared/search/search.service';
import { SearchServiceStub } from '../../../../testing/search-service.stub'; import { SearchServiceStub } from '../../../../testing/search-service.stub';
import { buildPaginatedList } from '../../../../../core/data/paginated-list.model'; import { buildPaginatedList } from '../../../../../core/data/paginated-list.model';
@@ -104,6 +105,7 @@ describe('SearchRangeFilterComponent', () => {
{ provide: RouteService, useValue: { getQueryParameterValue: () => observableOf({}) } }, { provide: RouteService, useValue: { getQueryParameterValue: () => observableOf({}) } },
{ provide: SEARCH_CONFIG_SERVICE, useValue: new SearchConfigurationServiceStub() }, { provide: SEARCH_CONFIG_SERVICE, useValue: new SearchConfigurationServiceStub() },
{ provide: IN_PLACE_SEARCH, useValue: false }, { provide: IN_PLACE_SEARCH, useValue: false },
{ provide: REFRESH_FILTER, useValue: new BehaviorSubject<boolean>(false) },
{ {
provide: SearchFilterService, useValue: { provide: SearchFilterService, useValue: {
getSelectedValuesForFilter: () => selectedValues, getSelectedValuesForFilter: () => selectedValues,

View File

@@ -1,4 +1,4 @@
import { combineLatest as observableCombineLatest, Subscription } from 'rxjs'; import { BehaviorSubject, combineLatest as observableCombineLatest, Subscription } from 'rxjs';
import { map, startWith } from 'rxjs/operators'; import { map, startWith } from 'rxjs/operators';
import { isPlatformBrowser } from '@angular/common'; import { isPlatformBrowser } from '@angular/common';
import { Component, Inject, OnDestroy, OnInit, PLATFORM_ID } from '@angular/core'; import { Component, Inject, OnDestroy, OnInit, PLATFORM_ID } from '@angular/core';
@@ -10,6 +10,7 @@ import { SearchFilterConfig } from '../../../models/search-filter-config.model';
import { import {
FILTER_CONFIG, FILTER_CONFIG,
IN_PLACE_SEARCH, IN_PLACE_SEARCH,
REFRESH_FILTER,
SearchFilterService SearchFilterService
} from '../../../../../core/shared/search/search-filter.service'; } from '../../../../../core/shared/search/search-filter.service';
import { SearchService } from '../../../../../core/shared/search/search.service'; import { SearchService } from '../../../../../core/shared/search/search.service';
@@ -86,8 +87,9 @@ export class SearchRangeFilterComponent extends SearchFacetFilterComponent imple
@Inject(IN_PLACE_SEARCH) public inPlaceSearch: boolean, @Inject(IN_PLACE_SEARCH) public inPlaceSearch: boolean,
@Inject(FILTER_CONFIG) public filterConfig: SearchFilterConfig, @Inject(FILTER_CONFIG) public filterConfig: SearchFilterConfig,
@Inject(PLATFORM_ID) private platformId: any, @Inject(PLATFORM_ID) private platformId: any,
@Inject(REFRESH_FILTER) public refreshFilters: BehaviorSubject<boolean>,
private route: RouteService) { private route: RouteService) {
super(searchService, filterService, rdbs, router, searchConfigService, inPlaceSearch, filterConfig); super(searchService, filterService, rdbs, router, searchConfigService, inPlaceSearch, filterConfig, refreshFilters);
} }

View File

@@ -1,7 +1,7 @@
<h3>{{"search.filters.head" | translate}}</h3> <h3>{{"search.filters.head" | translate}}</h3>
<div *ngIf="(filters | async)?.hasSucceeded"> <div *ngIf="(filters | async)?.hasSucceeded">
<div *ngFor="let filter of (filters | async)?.payload; trackBy: trackUpdate"> <div *ngFor="let filter of (filters | async)?.payload; trackBy: trackUpdate">
<ds-search-filter [filter]="filter" [inPlaceSearch]="inPlaceSearch"></ds-search-filter> <ds-search-filter [filter]="filter" [inPlaceSearch]="inPlaceSearch" [refreshFilters]="refreshFilters"></ds-search-filter>
</div> </div>
</div> </div>
<a class="btn btn-primary" [routerLink]="[searchLink]" [queryParams]="clearParams | async" queryParamsHandling="merge" role="button"><i class="fas fa-undo"></i> {{"search.filters.reset" | translate}}</a> <a class="btn btn-primary" [routerLink]="[searchLink]" [queryParams]="clearParams | async" queryParamsHandling="merge" role="button"><i class="fas fa-undo"></i> {{"search.filters.reset" | translate}}</a>

View File

@@ -53,7 +53,7 @@ export class SearchFiltersComponent implements OnInit, OnDestroy {
/** /**
* Emits when the search filters values may be stale, and so they must be refreshed. * Emits when the search filters values may be stale, and so they must be refreshed.
*/ */
@Input() refreshFilters: Observable<any>; @Input() refreshFilters: BehaviorSubject<boolean>;
/** /**
* Link to the search page * Link to the search page

View File

@@ -13,14 +13,13 @@
[linkType]="linkType" [linkType]="linkType"
[context]="context" [context]="context"
[hidePaginationDetail]="hidePaginationDetail" [hidePaginationDetail]="hidePaginationDetail"
(contentChange)="contentChange.emit($event)"
(deselectObject)="deselectObject.emit($event)" (deselectObject)="deselectObject.emit($event)"
(selectObject)="selectObject.emit($event)" (selectObject)="selectObject.emit($event)">
>
</ds-viewable-collection> </ds-viewable-collection>
</div> </div>
<ds-themed-loading *ngIf="isLoading()" message="{{'loading.search-results' | translate}}"></ds-themed-loading> <ds-themed-loading *ngIf="isLoading()" message="{{'loading.search-results' | translate}}"></ds-themed-loading>
<ds-error <ds-error *ngIf="showError()"
*ngIf="showError()"
message="{{errorMessageLabel() | translate}}"></ds-error> message="{{errorMessageLabel() | translate}}"></ds-error>
<div *ngIf="searchResults?.payload?.page.length == 0 || searchResults?.statusCode == 400"> <div *ngIf="searchResults?.payload?.page.length == 0 || searchResults?.statusCode == 400">
{{ 'search.results.no-results' | translate }} {{ 'search.results.no-results' | translate }}

View File

@@ -85,6 +85,11 @@ export class SearchResultsComponent {
*/ */
@Input() selectionConfig: SelectionConfig = null; @Input() selectionConfig: SelectionConfig = null;
/**
* Emit when one of the listed object has changed.
*/
@Output() contentChange = new EventEmitter<any>();
@Output() deselectObject: EventEmitter<ListableObject> = new EventEmitter<ListableObject>(); @Output() deselectObject: EventEmitter<ListableObject> = new EventEmitter<ListableObject>();
@Output() selectObject: EventEmitter<ListableObject> = new EventEmitter<ListableObject>(); @Output() selectObject: EventEmitter<ListableObject> = new EventEmitter<ListableObject>();

View File

@@ -21,7 +21,7 @@ import { ListableObject } from '../../object-collection/shared/listable-object.m
templateUrl: '../../theme-support/themed.component.html', templateUrl: '../../theme-support/themed.component.html',
}) })
export class ThemedSearchResultsComponent extends ThemedComponent<SearchResultsComponent> { export class ThemedSearchResultsComponent extends ThemedComponent<SearchResultsComponent> {
protected inAndOutputNames: (keyof SearchResultsComponent & keyof this)[] = ['linkType', 'searchResults', 'searchConfig', 'sortConfig', 'viewMode', 'configuration', 'disableHeader', 'selectable', 'context', 'hidePaginationDetail', 'selectionConfig', 'deselectObject', 'selectObject']; protected inAndOutputNames: (keyof SearchResultsComponent & keyof this)[] = ['linkType', 'searchResults', 'searchConfig', 'sortConfig', 'viewMode', 'configuration', 'disableHeader', 'selectable', 'context', 'hidePaginationDetail', 'selectionConfig', 'contentChange', 'deselectObject', 'selectObject'];
@Input() linkType: CollectionElementLinkType; @Input() linkType: CollectionElementLinkType;
@@ -45,6 +45,8 @@ export class ThemedSearchResultsComponent extends ThemedComponent<SearchResultsC
@Input() selectionConfig: SelectionConfig = null; @Input() selectionConfig: SelectionConfig = null;
@Output() contentChange: EventEmitter<ListableObject> = new EventEmitter<ListableObject>();
@Output() deselectObject: EventEmitter<ListableObject> = new EventEmitter<ListableObject>(); @Output() deselectObject: EventEmitter<ListableObject> = new EventEmitter<ListableObject>();
@Output() selectObject: EventEmitter<ListableObject> = new EventEmitter<ListableObject>(); @Output() selectObject: EventEmitter<ListableObject> = new EventEmitter<ListableObject>();

View File

@@ -83,7 +83,7 @@ export class SearchSidebarComponent {
/** /**
* Emits when the search filters values may be stale, and so they must be refreshed. * Emits when the search filters values may be stale, and so they must be refreshed.
*/ */
@Input() refreshFilters: Observable<any>; @Input() refreshFilters: BehaviorSubject<boolean>;
/** /**
* Emits event when the user clicks a button to open or close the sidebar * Emits event when the user clicks a button to open or close the sidebar

View File

@@ -37,6 +37,7 @@
[context]="(currentContext$ | async)" [context]="(currentContext$ | async)"
[selectable]="selectable" [selectable]="selectable"
[selectionConfig]="selectionConfig" [selectionConfig]="selectionConfig"
(contentChange)="onContentChange($event)"
(deselectObject)="deselectObject.emit($event)" (deselectObject)="deselectObject.emit($event)"
(selectObject)="selectObject.emit($event)"></ds-themed-search-results> (selectObject)="selectObject.emit($event)"></ds-themed-search-results>
</div> </div>
@@ -49,6 +50,7 @@
[configuration]="(currentConfiguration$ | async)" [configuration]="(currentConfiguration$ | async)"
[currentScope]="(currentScope$ | async)" [currentScope]="(currentScope$ | async)"
[filters]="filtersRD$.asObservable()" [filters]="filtersRD$.asObservable()"
[refreshFilters]="refreshFilters"
[resultCount]="(resultsRD$ | async)?.payload?.totalElements" [resultCount]="(resultsRD$ | async)?.payload?.totalElements"
[searchOptions]="(searchOptions$ | async)" [searchOptions]="(searchOptions$ | async)"
[sortOptionsList]="(sortOptionsList$ | async)" [sortOptionsList]="(sortOptionsList$ | async)"
@@ -63,6 +65,7 @@
[configuration]="(currentConfiguration$ | async)" [configuration]="(currentConfiguration$ | async)"
[currentScope]="(currentScope$ | async)" [currentScope]="(currentScope$ | async)"
[filters]="filtersRD$.asObservable()" [filters]="filtersRD$.asObservable()"
[refreshFilters]="refreshFilters"
[resultCount]="(resultsRD$ | async)?.payload.totalElements" [resultCount]="(resultsRD$ | async)?.payload.totalElements"
[searchOptions]="(searchOptions$ | async)" [searchOptions]="(searchOptions$ | async)"
[sortOptionsList]="(sortOptionsList$ | async)" [sortOptionsList]="(sortOptionsList$ | async)"

View File

@@ -201,6 +201,11 @@ export class SearchComponent implements OnInit {
*/ */
isXsOrSm$: Observable<boolean>; isXsOrSm$: Observable<boolean>;
/**
* Emits when the search filters values may be stale, and so they must be refreshed.
*/
refreshFilters: BehaviorSubject<boolean> = new BehaviorSubject<boolean>(false);
/** /**
* Link to the search page * Link to the search page
*/ */
@@ -339,6 +344,15 @@ export class SearchComponent implements OnInit {
this.sidebarService.expand(); this.sidebarService.expand();
} }
/**
* Emit event to refresh filter content
* @param $event
*/
public onContentChange($event: any) {
this.retrieveFilters(this.lastSearchOptions);
this.refreshFilters.next(true);
}
/** /**
* Unsubscribe from the subscription * Unsubscribe from the subscription
*/ */