[CSTPER-138] Fixed issue where results are the same when pagination changed while retrieving external source providers

This commit is contained in:
Giuseppe Digilio
2020-10-21 18:01:29 +02:00
parent 988747e392
commit 826957a66d
4 changed files with 129 additions and 58 deletions

View File

@@ -1,6 +1,6 @@
import { ChangeDetectorRef, Component, EventEmitter, Input, OnInit, Output } from '@angular/core'; import { ChangeDetectorRef, Component, EventEmitter, Input, OnDestroy, OnInit, Output } from '@angular/core';
import { of as observableOf, Observable } from 'rxjs'; import { Observable, of as observableOf, Subscription } from 'rxjs';
import { catchError, tap } from 'rxjs/operators'; import { catchError, tap } from 'rxjs/operators';
import { ExternalSourceService } from '../../../core/data/external-source.service'; import { ExternalSourceService } from '../../../core/data/external-source.service';
@@ -12,6 +12,7 @@ import { createSuccessfulRemoteDataObject } from '../../../shared/remote-data.ut
import { FindListOptions } from '../../../core/data/request.models'; import { FindListOptions } from '../../../core/data/request.models';
import { getFirstSucceededRemoteDataPayload } from '../../../core/shared/operators'; import { getFirstSucceededRemoteDataPayload } from '../../../core/shared/operators';
import { HostWindowService } from '../../../shared/host-window.service'; import { HostWindowService } from '../../../shared/host-window.service';
import { hasValue } from '../../../shared/empty.util';
/** /**
* Interface for the selected external source element. * Interface for the selected external source element.
@@ -37,7 +38,7 @@ export interface ExternalSourceData {
styleUrls: ['./submission-import-external-searchbar.component.scss'], styleUrls: ['./submission-import-external-searchbar.component.scss'],
templateUrl: './submission-import-external-searchbar.component.html' templateUrl: './submission-import-external-searchbar.component.html'
}) })
export class SubmissionImportExternalSearchbarComponent implements OnInit { export class SubmissionImportExternalSearchbarComponent implements OnInit, OnDestroy {
/** /**
* The init external source value. * The init external source value.
*/ */
@@ -76,6 +77,11 @@ export class SubmissionImportExternalSearchbarComponent implements OnInit {
*/ */
protected findListOptions: FindListOptions; protected findListOptions: FindListOptions;
/**
* The subscription to unsubscribe
*/
protected sub: Subscription;
/** /**
* Initialize the component variables. * Initialize the component variables.
* @param {ExternalSourceService} externalService * @param {ExternalSourceService} externalService
@@ -145,7 +151,7 @@ export class SubmissionImportExternalSearchbarComponent implements OnInit {
elementsPerPage: 5, elementsPerPage: 5,
currentPage: this.findListOptions.currentPage + 1, currentPage: this.findListOptions.currentPage + 1,
}); });
this.externalService.findAll(this.findListOptions).pipe( this.sub = this.externalService.findAll(this.findListOptions).pipe(
catchError(() => { catchError(() => {
const pageInfo = new PageInfo(); const pageInfo = new PageInfo();
const paginatedList = new PaginatedList(pageInfo, []); const paginatedList = new PaginatedList(pageInfo, []);
@@ -159,7 +165,7 @@ export class SubmissionImportExternalSearchbarComponent implements OnInit {
}) })
this.pageInfo = externalSource.payload.pageInfo; this.pageInfo = externalSource.payload.pageInfo;
this.cdr.detectChanges(); this.cdr.detectChanges();
}) });
} }
} }
@@ -169,4 +175,13 @@ export class SubmissionImportExternalSearchbarComponent implements OnInit {
public search(): void { public search(): void {
this.externalSourceData.emit({ sourceId: this.selectedElement.id, query: this.searchString }); this.externalSourceData.emit({ sourceId: this.selectedElement.id, query: this.searchString });
} }
/**
* Unsubscribe from all subscriptions
*/
ngOnDestroy(): void {
if (hasValue(this.sub)) {
this.sub.unsubscribe();
}
}
} }

View File

@@ -4,7 +4,7 @@
<h2 id="header" class="pb-2">{{'submission.import-external.title' | translate}}</h2> <h2 id="header" class="pb-2">{{'submission.import-external.title' | translate}}</h2>
<ds-submission-import-external-searchbar <ds-submission-import-external-searchbar
[initExternalSourceData]="routeData" [initExternalSourceData]="routeData"
(externalSourceData) = "getExternalsourceData($event)"> (externalSourceData) = "getExternalSourceData($event)">
</ds-submission-import-external-searchbar> </ds-submission-import-external-searchbar>
</div> </div>
</div> </div>
@@ -20,7 +20,8 @@
[context]="context" [context]="context"
[importable]="true" [importable]="true"
[importConfig]="importConfig" [importConfig]="importConfig"
(importObject)="import($event)"> (importObject)="import($event)"
(pageChange)="paginationChange();">
</ds-viewable-collection> </ds-viewable-collection>
<ds-loading *ngIf="(isLoading$ | async)" <ds-loading *ngIf="(isLoading$ | async)"
message="{{'loading.search-results' | translate}}"></ds-loading> message="{{'loading.search-results' | translate}}"></ds-loading>

View File

@@ -1,15 +1,19 @@
import { Component, NO_ERRORS_SCHEMA } from '@angular/core'; import { Component, NO_ERRORS_SCHEMA } from '@angular/core';
import { async, TestBed, ComponentFixture, inject } from '@angular/core/testing'; import { async, ComponentFixture, inject, TestBed } from '@angular/core/testing';
import { getTestScheduler } from 'jasmine-marbles';
import { TranslateModule } from '@ngx-translate/core'; import { TranslateModule } from '@ngx-translate/core';
import { Router } from '@angular/router'; import { Router } from '@angular/router';
import { NgbModal } from '@ng-bootstrap/ng-bootstrap'; import { NgbModal } from '@ng-bootstrap/ng-bootstrap';
import { of as observableOf, of } from 'rxjs/internal/observable/of'; import { of as observableOf } from 'rxjs';
import { TestScheduler } from 'rxjs/testing';
import { SubmissionImportExternalComponent } from './submission-import-external.component'; import { SubmissionImportExternalComponent } from './submission-import-external.component';
import { ExternalSourceService } from '../../core/data/external-source.service'; import { ExternalSourceService } from '../../core/data/external-source.service';
import { getMockExternalSourceService } from '../../shared/mocks/external-source.service.mock'; import { getMockExternalSourceService } from '../../shared/mocks/external-source.service.mock';
import { SearchConfigurationService } from '../../core/shared/search/search-configuration.service'; import { SearchConfigurationService } from '../../core/shared/search/search-configuration.service';
import { RouteService } from '../../core/services/route.service'; import { RouteService } from '../../core/services/route.service';
import { createTestComponent, createPaginatedList } from '../../shared/testing/utils.test'; import { createPaginatedList, createTestComponent } from '../../shared/testing/utils.test';
import { RouterStub } from '../../shared/testing/router.stub'; import { RouterStub } from '../../shared/testing/router.stub';
import { VarDirective } from '../../shared/utils/var.directive'; import { VarDirective } from '../../shared/utils/var.directive';
import { routeServiceStub } from '../../shared/testing/route-service.stub'; import { routeServiceStub } from '../../shared/testing/route-service.stub';
@@ -18,17 +22,20 @@ import { PaginationComponentOptions } from '../../shared/pagination/pagination-c
import { createSuccessfulRemoteDataObject } from '../../shared/remote-data.utils'; import { createSuccessfulRemoteDataObject } from '../../shared/remote-data.utils';
import { ExternalSourceEntry } from '../../core/shared/external-source-entry.model'; import { ExternalSourceEntry } from '../../core/shared/external-source-entry.model';
import { SubmissionImportExternalPreviewComponent } from './import-external-preview/submission-import-external-preview.component'; import { SubmissionImportExternalPreviewComponent } from './import-external-preview/submission-import-external-preview.component';
import { BehaviorSubject } from 'rxjs/internal/BehaviorSubject';
describe('SubmissionImportExternalComponent test suite', () => { describe('SubmissionImportExternalComponent test suite', () => {
let comp: SubmissionImportExternalComponent; let comp: SubmissionImportExternalComponent;
let compAsAny: any; let compAsAny: any;
let fixture: ComponentFixture<SubmissionImportExternalComponent>; let fixture: ComponentFixture<SubmissionImportExternalComponent>;
let scheduler: TestScheduler;
const ngbModal = jasmine.createSpyObj('modal', ['open']); const ngbModal = jasmine.createSpyObj('modal', ['open']);
const mockSearchOptions = of(new PaginatedSearchOptions({ const mockSearchOptions = observableOf(new PaginatedSearchOptions({
pagination: Object.assign(new PaginationComponentOptions(), { pagination: Object.assign(new PaginationComponentOptions(), {
pageSize: 10, pageSize: 10,
currentPage: 0 currentPage: 0
}) }),
query: 'test'
})); }));
const searchConfigServiceStub = { const searchConfigServiceStub = {
paginatedSearchOptions: mockSearchOptions paginatedSearchOptions: mockSearchOptions
@@ -83,6 +90,7 @@ describe('SubmissionImportExternalComponent test suite', () => {
fixture = TestBed.createComponent(SubmissionImportExternalComponent); fixture = TestBed.createComponent(SubmissionImportExternalComponent);
comp = fixture.componentInstance; comp = fixture.componentInstance;
compAsAny = comp; compAsAny = comp;
scheduler = getTestScheduler();
}); });
afterEach(() => { afterEach(() => {
@@ -102,25 +110,31 @@ describe('SubmissionImportExternalComponent test suite', () => {
}); });
it('Should init component properly (with route data)', () => { it('Should init component properly (with route data)', () => {
const expectedEntries = createSuccessfulRemoteDataObject(createPaginatedList([])); spyOn(compAsAny, 'retrieveExternalSources');
const searchOptions = new PaginatedSearchOptions({ spyOn(compAsAny.routeService, 'getQueryParameterValue').and.returnValues(observableOf('source'), observableOf('dummy'));
pagination: Object.assign(new PaginationComponentOptions(), {
pageSize: 10,
currentPage: 0
})
});
spyOn(compAsAny.routeService, 'getQueryParameterValue').and.returnValue(observableOf('dummy'));
fixture.detectChanges(); fixture.detectChanges();
expect(comp.routeData).toEqual({ sourceId: 'dummy', query: 'dummy' }); expect(compAsAny.retrieveExternalSources).toHaveBeenCalledWith('source', 'dummy');
});
it('Should call \'getExternalSourceEntries\' properly', () => {
comp.routeData = { sourceId: '', query: '' };
comp.isLoading$ = new BehaviorSubject(false);
scheduler.schedule(() => compAsAny.retrieveExternalSources('orcidV2', 'test'));
scheduler.flush();
expect(comp.routeData).toEqual({ sourceId: 'orcidV2', query: 'test' });
expect(comp.isLoading$.value).toBe(true); expect(comp.isLoading$.value).toBe(true);
expect(comp.entriesRD$.value).toEqual(expectedEntries); expect(compAsAny.externalService.getExternalSourceEntries).toHaveBeenCalled();
expect(compAsAny.externalService.getExternalSourceEntries).toHaveBeenCalledWith('dummy', searchOptions);
}); });
it('Should call \'router.navigate\'', () => { it('Should call \'router.navigate\'', () => {
spyOn(compAsAny, 'retrieveExternalSources');
compAsAny.router.navigate.and.returnValue( new Promise(() => {return;}))
const event = { sourceId: 'orcidV2', query: 'dummy' }; const event = { sourceId: 'orcidV2', query: 'dummy' };
comp.getExternalsourceData(event);
scheduler.schedule(() => comp.getExternalSourceData(event));
scheduler.flush();
expect(compAsAny.router.navigate).toHaveBeenCalledWith([], { queryParams: { source: event.sourceId, query: event.query }, replaceUrl: true }); expect(compAsAny.router.navigate).toHaveBeenCalledWith([], { queryParams: { source: event.sourceId, query: event.query }, replaceUrl: true });
}); });

View File

@@ -1,22 +1,25 @@
import { Component, OnInit } from '@angular/core'; import { Component, OnDestroy, OnInit } from '@angular/core';
import { Router } from '@angular/router'; import { Router } from '@angular/router';
import { combineLatest, BehaviorSubject } from 'rxjs';
import { BehaviorSubject, combineLatest, Subscription } from 'rxjs';
import { filter, flatMap, take } from 'rxjs/operators';
import { NgbModal, NgbModalRef } from '@ng-bootstrap/ng-bootstrap';
import { ExternalSourceService } from '../../core/data/external-source.service'; import { ExternalSourceService } from '../../core/data/external-source.service';
import { ExternalSourceData } from './import-external-searchbar/submission-import-external-searchbar.component'; import { ExternalSourceData } from './import-external-searchbar/submission-import-external-searchbar.component';
import { RemoteData } from '../../core/data/remote-data'; import { RemoteData } from '../../core/data/remote-data';
import { PaginatedList } from '../../core/data/paginated-list'; import { PaginatedList } from '../../core/data/paginated-list';
import { ExternalSourceEntry } from '../../core/shared/external-source-entry.model'; import { ExternalSourceEntry } from '../../core/shared/external-source-entry.model';
import { SearchConfigurationService } from '../../core/shared/search/search-configuration.service'; import { SearchConfigurationService } from '../../core/shared/search/search-configuration.service';
import { switchMap, filter, take } from 'rxjs/operators';
import { PaginatedSearchOptions } from '../../shared/search/paginated-search-options.model';
import { Context } from '../../core/shared/context.model'; import { Context } from '../../core/shared/context.model';
import { PaginationComponentOptions } from '../../shared/pagination/pagination-component-options.model'; import { PaginationComponentOptions } from '../../shared/pagination/pagination-component-options.model';
import { RouteService } from '../../core/services/route.service'; import { RouteService } from '../../core/services/route.service';
import { createSuccessfulRemoteDataObject } from '../../shared/remote-data.utils'; import { createSuccessfulRemoteDataObject } from '../../shared/remote-data.utils';
import { NgbModal, NgbModalRef } from '@ng-bootstrap/ng-bootstrap';
import { SubmissionImportExternalPreviewComponent } from './import-external-preview/submission-import-external-preview.component'; import { SubmissionImportExternalPreviewComponent } from './import-external-preview/submission-import-external-preview.component';
import { fadeIn } from '../../shared/animations/fade'; import { fadeIn } from '../../shared/animations/fade';
import { PageInfo } from '../../core/shared/page-info.model'; import { PageInfo } from '../../core/shared/page-info.model';
import { hasValue, isNotEmpty } from '../../shared/empty.util';
import { getFinishedRemoteData } from '../../core/shared/operators';
/** /**
* This component allows to submit a new workspaceitem importing the data from an external source. * This component allows to submit a new workspaceitem importing the data from an external source.
@@ -25,9 +28,10 @@ import { PageInfo } from '../../core/shared/page-info.model';
selector: 'ds-submission-import-external', selector: 'ds-submission-import-external',
styleUrls: ['./submission-import-external.component.scss'], styleUrls: ['./submission-import-external.component.scss'],
templateUrl: './submission-import-external.component.html', templateUrl: './submission-import-external.component.html',
animations: [ fadeIn ] animations: [fadeIn]
}) })
export class SubmissionImportExternalComponent implements OnInit { export class SubmissionImportExternalComponent implements OnInit, OnDestroy {
/** /**
* The external source search data from the routing service. * The external source search data from the routing service.
*/ */
@@ -61,7 +65,7 @@ export class SubmissionImportExternalComponent implements OnInit {
*/ */
public initialPagination = Object.assign(new PaginationComponentOptions(), { public initialPagination = Object.assign(new PaginationComponentOptions(), {
id: 'submission-external-source-relation-list', id: 'submission-external-source-relation-list',
pageSize: 5 pageSize: 10
}); });
/** /**
* The context to displaying lists for * The context to displaying lists for
@@ -72,6 +76,11 @@ export class SubmissionImportExternalComponent implements OnInit {
*/ */
public modalRef: NgbModalRef; public modalRef: NgbModalRef;
/**
* The subscription to unsubscribe
*/
protected subs: Subscription[] = [];
/** /**
* Initialize the component variables. * Initialize the component variables.
* @param {SearchConfigurationService} searchConfigService * @param {SearchConfigurationService} searchConfigService
@@ -86,7 +95,8 @@ export class SubmissionImportExternalComponent implements OnInit {
private routeService: RouteService, private routeService: RouteService,
private router: Router, private router: Router,
private modalService: NgbModal, private modalService: NgbModal,
) { } ) {
}
/** /**
* Get the entries for the selected external source and set initial configuration. * Get the entries for the selected external source and set initial configuration.
@@ -102,42 +112,28 @@ export class SubmissionImportExternalComponent implements OnInit {
}; };
this.entriesRD$ = new BehaviorSubject(createSuccessfulRemoteDataObject(new PaginatedList(new PageInfo(), []))); this.entriesRD$ = new BehaviorSubject(createSuccessfulRemoteDataObject(new PaginatedList(new PageInfo(), [])));
this.isLoading$ = new BehaviorSubject(false); this.isLoading$ = new BehaviorSubject(false);
combineLatest( this.subs.push(combineLatest(
[ [
this.routeService.getQueryParameterValue('source'), this.routeService.getQueryParameterValue('source'),
this.routeService.getQueryParameterValue('query') this.routeService.getQueryParameterValue('query')
]).pipe( ]).pipe(
take(1),
filter(([source, query]) => source && query && source !== '' && query !== ''),
filter(([source, query]) => source !== this.routeData.sourceId || query !== this.routeData.query),
switchMap(([source, query]) => {
this.routeData.sourceId = source;
this.routeData.query = query;
this.isLoading$.next(true);
return this.searchConfigService.paginatedSearchOptions.pipe(
switchMap((searchOptions: PaginatedSearchOptions) => {
return this.externalService.getExternalSourceEntries(this.routeData.sourceId, searchOptions);
}),
take(1) take(1)
) ).subscribe(([source, query]: [string, string]) => {
}), this.retrieveExternalSources(source, query);
).subscribe((rdData) => { }));
this.entriesRD$.next(rdData);
this.isLoading$.next(false);
});
} }
/** /**
* Get the data from the searchbar and changes the router data. * Get the data from the searchbar and changes the router data.
*/ */
public getExternalsourceData(event: ExternalSourceData): void { public getExternalSourceData(event: ExternalSourceData): void {
this.router.navigate( this.router.navigate(
[], [],
{ {
queryParams: { source: event.sourceId, query: event.query }, queryParams: { source: event.sourceId, query: event.query },
replaceUrl: true replaceUrl: true
} }
); ).then(() => this.retrieveExternalSources(event.sourceId, event.query));
} }
/** /**
@@ -151,4 +147,49 @@ export class SubmissionImportExternalComponent implements OnInit {
const modalComp = this.modalRef.componentInstance; const modalComp = this.modalRef.componentInstance;
modalComp.externalSourceEntry = entry; modalComp.externalSourceEntry = entry;
} }
/**
* Retrieve external sources on pagination change
*/
paginationChange() {
this.retrieveExternalSources(this.routeData.sourceId, this.routeData.query);
}
/**
* Unsubscribe from all subscriptions
*/
ngOnDestroy(): void {
this.subs
.filter((sub) => hasValue(sub))
.forEach((sub) => sub.unsubscribe());
}
/**
* Retrieve external source entries
*
* @param source The source tupe
* @param query The query string to search
*/
private retrieveExternalSources(source: string, query: string): void {
if (isNotEmpty(source) && isNotEmpty(query)) {
this.routeData.sourceId = source;
this.routeData.query = query;
this.isLoading$.next(true);
this.subs.push(
this.searchConfigService.paginatedSearchOptions.pipe(
filter((searchOptions) => searchOptions.query === query),
take(1),
flatMap((searchOptions) => this.externalService.getExternalSourceEntries(this.routeData.sourceId, searchOptions).pipe(
getFinishedRemoteData(),
take(1)
)),
take(1)
).subscribe((rdData) => {
this.entriesRD$.next(rdData);
this.isLoading$.next(false);
})
);
}
}
} }