[CSTPER-3620] Fixed workspace followlink and search filters update

This commit is contained in:
Alessandro Martelli
2021-02-22 16:56:29 +01:00
parent c78cd9ad71
commit 523b7a497c
18 changed files with 130 additions and 47 deletions

View File

@@ -6,6 +6,7 @@
[configurationList]="(configurationList$ | async)" [configurationList]="(configurationList$ | async)"
[resultCount]="(resultsRD$ | async)?.payload.totalElements" [resultCount]="(resultsRD$ | async)?.payload.totalElements"
[viewModeList]="viewModeList" [viewModeList]="viewModeList"
[refreshFilters]="refreshFilters.asObservable()"
[inPlaceSearch]="inPlaceSearch"></ds-search-sidebar> [inPlaceSearch]="inPlaceSearch"></ds-search-sidebar>
<div class="col-12 col-md-9"> <div class="col-12 col-md-9">
<ds-search-form id="search-form" <ds-search-form id="search-form"
@@ -26,6 +27,7 @@
[resultCount]="(resultsRD$ | async)?.payload.totalElements" [resultCount]="(resultsRD$ | async)?.payload.totalElements"
(toggleSidebar)="closeSidebar()" (toggleSidebar)="closeSidebar()"
[ngClass]="{'active': !(isSidebarCollapsed() | async)}" [ngClass]="{'active': !(isSidebarCollapsed() | async)}"
[refreshFilters]="refreshFilters.asObservable()"
[inPlaceSearch]="inPlaceSearch"> [inPlaceSearch]="inPlaceSearch">
</ds-search-sidebar> </ds-search-sidebar>
<div id="search-content" class="col-12"> <div id="search-content" class="col-12">
@@ -39,7 +41,8 @@
</div> </div>
<ds-my-dspace-results [searchResults]="resultsRD$ | async" <ds-my-dspace-results [searchResults]="resultsRD$ | async"
[searchConfig]="searchOptions$ | async" [searchConfig]="searchOptions$ | async"
[context]="context$ | async"></ds-my-dspace-results> [context]="context$ | async"
(contentChange)="onResultsContentChange()"></ds-my-dspace-results>
</div> </div>
</div> </div>
</div> </div>

View File

@@ -7,7 +7,7 @@ import {
OnInit OnInit
} from '@angular/core'; } from '@angular/core';
import { BehaviorSubject, Observable, Subscription } from 'rxjs'; import { BehaviorSubject, Observable, Subject, Subscription } from 'rxjs';
import { map, switchMap, tap, } from 'rxjs/operators'; import { map, switchMap, tap, } from 'rxjs/operators';
import { PaginatedList } from '../core/data/paginated-list.model'; import { PaginatedList } from '../core/data/paginated-list.model';
@@ -101,6 +101,11 @@ export class MyDSpacePageComponent implements OnInit {
*/ */
context$: Observable<Context>; context$: Observable<Context>;
/**
* Emit an event every time search sidebars must refresh their contents.
*/
refreshFilters: Subject<any> = new Subject<any>();
constructor(private service: SearchService, constructor(private service: SearchService,
private sidebarService: SidebarService, private sidebarService: SidebarService,
private windowService: HostWindowService, private windowService: HostWindowService,
@@ -148,6 +153,14 @@ export class MyDSpacePageComponent implements OnInit {
} }
/**
* Handle the contentChange event from within the my dspace content.
* Notify search sidebars to refresh their content.
*/
onResultsContentChange() {
this.refreshFilters.next();
}
/** /**
* Set the sidebar to a collapsed state * Set the sidebar to a collapsed state
*/ */
@@ -184,5 +197,6 @@ export class MyDSpacePageComponent implements OnInit {
if (hasValue(this.sub)) { if (hasValue(this.sub)) {
this.sub.unsubscribe(); this.sub.unsubscribe();
} }
this.refreshFilters.complete();
} }
} }

View File

@@ -5,7 +5,8 @@
[sortConfig]="searchConfig.sort" [sortConfig]="searchConfig.sort"
[objects]="searchResults" [objects]="searchResults"
[hideGear]="true" [hideGear]="true"
[context]="context"> [context]="context"
(contentChange)="contentChange.emit()">
</ds-viewable-collection> </ds-viewable-collection>
</div> </div>
<ds-loading *ngIf="isLoading()" message="{{'loading.mydspace-results' | translate}}"></ds-loading> <ds-loading *ngIf="isLoading()" message="{{'loading.mydspace-results' | translate}}"></ds-loading>

View File

@@ -1,4 +1,4 @@
import { Component, Input } from '@angular/core'; import { Component, EventEmitter, Input, Output } from '@angular/core';
import { RemoteData } from '../../core/data/remote-data'; import { RemoteData } from '../../core/data/remote-data';
import { DSpaceObject } from '../../core/shared/dspace-object.model'; import { DSpaceObject } from '../../core/shared/dspace-object.model';
import { fadeIn, fadeInOut } from '../../shared/animations/fade'; import { fadeIn, fadeInOut } from '../../shared/animations/fade';
@@ -41,6 +41,12 @@ export class MyDSpaceResultsComponent {
* The current context for the search results * The current context for the search results
*/ */
@Input() context: Context; @Input() context: Context;
/**
* Emit when one of the results has changed.
*/
@Output() contentChange = new EventEmitter<any>();
/** /**
* A boolean representing if search results entry are separated by a line * A boolean representing if search results entry are separated by a line
*/ */

View File

@@ -11,7 +11,6 @@ import { ClaimedTaskDataService } from './claimed-task-data.service';
import { of as observableOf } from 'rxjs/internal/observable/of'; import { of as observableOf } from 'rxjs/internal/observable/of';
import { FindListOptions } from '../data/request.models'; import { FindListOptions } from '../data/request.models';
import { RequestParam } from '../cache/models/request-param.model'; import { RequestParam } from '../cache/models/request-param.model';
import { followLink } from '../../shared/utils/follow-link-config.model';
import { getTestScheduler } from 'jasmine-marbles'; import { getTestScheduler } from 'jasmine-marbles';
import { TestScheduler } from 'rxjs/testing'; import { TestScheduler } from 'rxjs/testing';
import { HttpOptions } from '../dspace-rest/dspace-rest.service'; import { HttpOptions } from '../dspace-rest/dspace-rest.service';
@@ -120,13 +119,7 @@ describe('ClaimedTaskDataService', () => {
new RequestParam('uuid', 'a0db0fde-1d12-4d43-bd0d-0f43df8d823c') new RequestParam('uuid', 'a0db0fde-1d12-4d43-bd0d-0f43df8d823c')
]; ];
expect(service.searchTask).toHaveBeenCalledWith('findByItem', expect(service.searchTask).toHaveBeenCalledWith('findByItem', findListOptions);
findListOptions,
followLink('workflowitem',
null,
true,
false,
true));
}); });
}); });
}); });

View File

@@ -16,7 +16,6 @@ import { CLAIMED_TASK } from './models/claimed-task-object.resource-type';
import { ProcessTaskResponse } from './models/process-task-response'; import { ProcessTaskResponse } from './models/process-task-response';
import { TasksService } from './tasks.service'; import { TasksService } from './tasks.service';
import { RemoteData } from '../data/remote-data'; import { RemoteData } from '../data/remote-data';
import { followLink } from '../../shared/utils/follow-link-config.model';
import { FindListOptions } from '../data/request.models'; import { FindListOptions } from '../data/request.models';
import { RequestParam } from '../cache/models/request-param.model'; import { RequestParam } from '../cache/models/request-param.model';
import { HttpOptions } from '../dspace-rest/dspace-rest.service'; import { HttpOptions } from '../dspace-rest/dspace-rest.service';
@@ -116,12 +115,7 @@ export class ClaimedTaskDataService extends TasksService<ClaimedTask> {
options.searchParams = [ options.searchParams = [
new RequestParam('uuid', uuid) new RequestParam('uuid', uuid)
]; ];
return this.searchTask('findByItem', options, followLink('workflowitem', return this.searchTask('findByItem', options).pipe(getFirstSucceededRemoteData());
null,
true,
false,
true))
.pipe(getFirstSucceededRemoteData());
} }
} }

View File

@@ -11,7 +11,6 @@ import { PoolTaskDataService } from './pool-task-data.service';
import { getTestScheduler } from 'jasmine-marbles'; import { getTestScheduler } from 'jasmine-marbles';
import { TestScheduler } from 'rxjs/testing'; import { TestScheduler } from 'rxjs/testing';
import { of as observableOf } from 'rxjs/internal/observable/of'; import { of as observableOf } from 'rxjs/internal/observable/of';
import { followLink } from '../../shared/utils/follow-link-config.model';
import { FindListOptions } from '../data/request.models'; import { FindListOptions } from '../data/request.models';
import { RequestParam } from '../cache/models/request-param.model'; import { RequestParam } from '../cache/models/request-param.model';
import { HttpOptions } from '../dspace-rest/dspace-rest.service'; import { HttpOptions } from '../dspace-rest/dspace-rest.service';
@@ -74,12 +73,7 @@ describe('PoolTaskDataService', () => {
new RequestParam('uuid', 'a0db0fde-1d12-4d43-bd0d-0f43df8d823c') new RequestParam('uuid', 'a0db0fde-1d12-4d43-bd0d-0f43df8d823c')
]; ];
expect(service.searchTask).toHaveBeenCalledWith('findByItem', findListOptions, expect(service.searchTask).toHaveBeenCalledWith('findByItem', findListOptions);
followLink('workflowitem',
null,
true,
false,
true));
}); });
}); });

View File

@@ -15,10 +15,9 @@ import { PoolTask } from './models/pool-task-object.model';
import { POOL_TASK } from './models/pool-task-object.resource-type'; import { POOL_TASK } from './models/pool-task-object.resource-type';
import { TasksService } from './tasks.service'; import { TasksService } from './tasks.service';
import { RemoteData } from '../data/remote-data'; import { RemoteData } from '../data/remote-data';
import { followLink } from '../../shared/utils/follow-link-config.model';
import { FindListOptions } from '../data/request.models'; import { FindListOptions } from '../data/request.models';
import { RequestParam } from '../cache/models/request-param.model'; import { RequestParam } from '../cache/models/request-param.model';
import { getFirstSucceededRemoteData } from '../shared/operators'; import { getFirstCompletedRemoteData } from '../shared/operators';
/** /**
* The service handling all REST requests for PoolTask * The service handling all REST requests for PoolTask
@@ -71,12 +70,7 @@ export class PoolTaskDataService extends TasksService<PoolTask> {
options.searchParams = [ options.searchParams = [
new RequestParam('uuid', uuid) new RequestParam('uuid', uuid)
]; ];
return this.searchTask('findByItem', options, followLink('workflowitem', return this.searchTask('findByItem', options).pipe(getFirstCompletedRemoteData());
null,
true,
false,
true))
.pipe(getFirstSucceededRemoteData());
} }
/** /**

View File

@@ -1,7 +1,7 @@
import { Router } from '@angular/router'; import { Router } from '@angular/router';
import { Component, Injector, OnInit } from '@angular/core'; import { Component, Injector, OnInit } from '@angular/core';
import { map, switchMap, tap} from 'rxjs/operators'; import { map, switchMap, take, tap } from 'rxjs/operators';
import { RemoteData } from '../../core/data/remote-data'; import { RemoteData } from '../../core/data/remote-data';
import { DataService } from '../../core/data/data.service'; import { DataService } from '../../core/data/data.service';
@@ -64,6 +64,7 @@ export abstract class MyDSpaceReloadableActionsComponent<T extends DSpaceObject,
startActionExecution(): Observable<DSpaceObject> { startActionExecution(): Observable<DSpaceObject> {
this.processing$.next(true); this.processing$.next(true);
return this.actionExecution().pipe( return this.actionExecution().pipe(
take(1),
switchMap((res: ProcessTaskResponse) => { switchMap((res: ProcessTaskResponse) => {
if (res.hasSucceeded) { if (res.hasSucceeded) {
return this._reloadObject().pipe( return this._reloadObject().pipe(

View File

@@ -18,6 +18,7 @@
[importable]="importable" [importable]="importable"
[importConfig]="importConfig" [importConfig]="importConfig"
(importObject)="importObject.emit($event)" (importObject)="importObject.emit($event)"
(contentChange)="contentChange.emit()"
*ngIf="(currentMode$ | async) === viewModeEnum.ListElement"> *ngIf="(currentMode$ | async) === viewModeEnum.ListElement">
</ds-object-list> </ds-object-list>

View File

@@ -53,6 +53,11 @@ export class ObjectCollectionComponent implements OnInit {
@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>();
/**
* Emit when one of the collection's object has changed.
*/
@Output() contentChange = new EventEmitter<any>();
/** /**
* Whether or not to add an import button to the object elements * Whether or not to add an import button to the object elements
*/ */

View File

@@ -1,4 +1,14 @@
import { Component, ComponentFactoryResolver, ElementRef, Input, OnDestroy, OnInit, ViewChild } from '@angular/core'; import {
Component,
ComponentFactoryResolver,
ElementRef,
Input,
OnDestroy,
OnInit,
Output,
ViewChild,
EventEmitter
} 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';
import { Context } from '../../../../core/shared/context.model'; import { Context } from '../../../../core/shared/context.model';
@@ -76,6 +86,11 @@ export class ListableObjectComponentLoaderComponent implements OnInit, OnDestroy
*/ */
@ViewChild('badges', { static: true }) badges: ElementRef; @ViewChild('badges', { static: true }) badges: ElementRef;
/**
* Emit when the listable object has been reloaded.
*/
@Output() contentChange = new EventEmitter<ListableObject>();
/** /**
* Whether or not the "Private" badge should be displayed for this listable object * Whether or not the "Private" badge should be displayed for this listable object
*/ */
@@ -141,6 +156,7 @@ export class ListableObjectComponentLoaderComponent implements OnInit, OnDestroy
componentRef.destroy(); componentRef.destroy();
this.object = reloadedObject; this.object = reloadedObject;
this.instantiateComponent(reloadedObject); this.instantiateComponent(reloadedObject);
this.contentChange.emit(reloadedObject);
} }
}); });
} }

View File

@@ -22,7 +22,9 @@
[importConfig]="importConfig" [importConfig]="importConfig"
(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"></ds-listable-object-component-loader> [listID]="selectionConfig?.listId"
(contentChange)="contentChange.emit()"
></ds-listable-object-component-loader>
</li> </li>
</ul> </ul>
</ds-pagination> </ds-pagination>

View File

@@ -76,6 +76,11 @@ export class ObjectListComponent {
*/ */
@Input() importConfig: { importLabel: string }; @Input() importConfig: { importLabel: string };
/**
* Emit when one of the listed object has changed.
*/
@Output() contentChange = new EventEmitter<any>();
/** /**
* The current listable objects * The current listable objects
*/ */

View File

@@ -7,7 +7,7 @@ import { NoopAnimationsModule } from '@angular/platform-browser/animations';
import { SearchFilterService } from '../../../core/shared/search/search-filter.service'; import { SearchFilterService } from '../../../core/shared/search/search-filter.service';
import { SearchFiltersComponent } from './search-filters.component'; import { SearchFiltersComponent } from './search-filters.component';
import { SearchService } from '../../../core/shared/search/search.service'; import { SearchService } from '../../../core/shared/search/search.service';
import { of as observableOf } from 'rxjs'; import { of as observableOf, Subject } 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';
@@ -66,4 +66,26 @@ describe('SearchFiltersComponent', () => {
}); });
}); });
describe('when refreshSearch observable is present and emit events', () => {
let refreshFiltersEmitter: Subject<any>;
beforeEach(() => {
spyOn(comp, 'initFilters').and.callFake(() => { /****/});
refreshFiltersEmitter = new Subject();
comp.refreshFilters = refreshFiltersEmitter.asObservable();
comp.ngOnInit();
});
it('should reinitialize search filters', () => {
expect(comp.initFilters).toHaveBeenCalledTimes(1);
refreshFiltersEmitter.next();
expect(comp.initFilters).toHaveBeenCalledTimes(2);
});
});
}); });

View File

@@ -1,4 +1,4 @@
import { Component, Inject, Input, OnInit } from '@angular/core'; import { Component, Inject, Input, OnDestroy, OnInit } from '@angular/core';
import { Observable } from 'rxjs'; import { Observable } from 'rxjs';
import { map, switchMap } from 'rxjs/operators'; import { map, switchMap } from 'rxjs/operators';
@@ -12,17 +12,19 @@ import { getFirstSucceededRemoteData } from '../../../core/shared/operators';
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 { currentPath } from '../../utils/route.utils'; import { currentPath } from '../../utils/route.utils';
import { Router } from '@angular/router'; import { Router } from '@angular/router';
import { hasValue } from '../../empty.util';
@Component({ @Component({
selector: 'ds-search-filters', selector: 'ds-search-filters',
styleUrls: ['./search-filters.component.scss'], styleUrls: ['./search-filters.component.scss'],
templateUrl: './search-filters.component.html', templateUrl: './search-filters.component.html',
}) })
/** /**
* This component represents the part of the search sidebar that contains filters. * This component represents the part of the search sidebar that contains filters.
*/ */
export class SearchFiltersComponent implements OnInit { export class SearchFiltersComponent implements OnInit, OnDestroy {
/** /**
* An observable containing configuration about which filters are shown and how they are shown * An observable containing configuration about which filters are shown and how they are shown
*/ */
@@ -39,11 +41,18 @@ export class SearchFiltersComponent implements OnInit {
*/ */
@Input() inPlaceSearch; @Input() inPlaceSearch;
/**
* Emits when the search filters values may be stale, and so they must be refreshed.
*/
@Input() refreshFilters: Observable<any>;
/** /**
* Link to the search page * Link to the search page
*/ */
searchLink: string; searchLink: string;
subs = [];
/** /**
* Initialize instance variables * Initialize instance variables
* @param {SearchService} searchService * @param {SearchService} searchService
@@ -58,9 +67,12 @@ export class SearchFiltersComponent implements OnInit {
} }
ngOnInit(): void { ngOnInit(): void {
this.filters = this.searchConfigService.searchOptions.pipe(
switchMap((options) => this.searchService.getConfig(options.scope, options.configuration).pipe(getFirstSucceededRemoteData())), this.initFilters();
);
if (this.refreshFilters) {
this.subs.push(this.refreshFilters.subscribe(() => this.initFilters()));
}
this.clearParams = this.searchConfigService.getCurrentFrontendFilters().pipe(map((filters) => { this.clearParams = this.searchConfigService.getCurrentFrontendFilters().pipe(map((filters) => {
Object.keys(filters).forEach((f) => filters[f] = null); Object.keys(filters).forEach((f) => filters[f] = null);
@@ -69,6 +81,12 @@ export class SearchFiltersComponent implements OnInit {
this.searchLink = this.getSearchLink(); this.searchLink = this.getSearchLink();
} }
initFilters() {
this.filters = this.searchConfigService.searchOptions.pipe(
switchMap((options) => this.searchService.getConfig(options.scope, options.configuration).pipe(getFirstSucceededRemoteData())),
);
}
/** /**
* @returns {string} The base path to the search page, or the current page when inPlaceSearch is true * @returns {string} The base path to the search page, or the current page when inPlaceSearch is true
*/ */
@@ -85,4 +103,12 @@ export class SearchFiltersComponent implements OnInit {
trackUpdate(index, config: SearchFilterConfig) { trackUpdate(index, config: SearchFilterConfig) {
return config ? config.name : undefined; return config ? config.name : undefined;
} }
ngOnDestroy() {
this.subs.forEach((sub) => {
if (hasValue(sub)) {
sub.unsubscribe();
}
});
}
} }

View File

@@ -11,7 +11,7 @@
<ds-view-mode-switch *ngIf="showViewModes" [viewModeList]="viewModeList" class="d-none d-md-block"></ds-view-mode-switch> <ds-view-mode-switch *ngIf="showViewModes" [viewModeList]="viewModeList" class="d-none d-md-block"></ds-view-mode-switch>
<div class="sidebar-content"> <div class="sidebar-content">
<ds-search-switch-configuration [inPlaceSearch]="inPlaceSearch" *ngIf="configurationList" [configurationList]="configurationList"></ds-search-switch-configuration> <ds-search-switch-configuration [inPlaceSearch]="inPlaceSearch" *ngIf="configurationList" [configurationList]="configurationList"></ds-search-switch-configuration>
<ds-search-filters [inPlaceSearch]="inPlaceSearch"></ds-search-filters> <ds-search-filters [refreshFilters]="refreshFilters" [inPlaceSearch]="inPlaceSearch"></ds-search-filters>
<ds-search-settings></ds-search-settings> <ds-search-settings></ds-search-settings>
</div> </div>
</div> </div>

View File

@@ -1,6 +1,7 @@
import { Component, EventEmitter, Input, Output } from '@angular/core'; import { Component, EventEmitter, Input, Output } from '@angular/core';
import { SearchConfigurationOption } from '../search-switch-configuration/search-configuration-option.model'; import { SearchConfigurationOption } from '../search-switch-configuration/search-configuration-option.model';
import { Observable } from 'rxjs';
/** /**
* This component renders a simple item page. * This component renders a simple item page.
@@ -44,6 +45,11 @@ export class SearchSidebarComponent {
*/ */
@Input() inPlaceSearch; @Input() inPlaceSearch;
/**
* Emits when the search filters values may be stale, and so they must be refreshed.
*/
@Input() refreshFilters: Observable<any>;
/** /**
* 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
*/ */