79252: Fix dso-selector issues

This commit is contained in:
Kristof De Langhe
2021-05-06 18:03:46 +02:00
parent a69033917d
commit 86bf7ff4f2
6 changed files with 100 additions and 51 deletions

View File

@@ -10,6 +10,7 @@ import { createSuccessfulRemoteDataObject$ } from '../../../remote-data.utils';
import { createPaginatedList } from '../../../testing/utils.test'; import { createPaginatedList } from '../../../testing/utils.test';
import { Collection } from '../../../../core/shared/collection.model'; import { Collection } from '../../../../core/shared/collection.model';
import { DSpaceObjectType } from '../../../../core/shared/dspace-object-type.model'; import { DSpaceObjectType } from '../../../../core/shared/dspace-object-type.model';
import { NotificationsService } from '../../../notifications/notifications.service';
describe('AuthorizedCollectionSelectorComponent', () => { describe('AuthorizedCollectionSelectorComponent', () => {
let component: AuthorizedCollectionSelectorComponent; let component: AuthorizedCollectionSelectorComponent;
@@ -18,6 +19,8 @@ describe('AuthorizedCollectionSelectorComponent', () => {
let collectionService; let collectionService;
let collection; let collection;
let notificationsService: NotificationsService;
beforeEach(waitForAsync(() => { beforeEach(waitForAsync(() => {
collection = Object.assign(new Collection(), { collection = Object.assign(new Collection(), {
id: 'authorized-collection' id: 'authorized-collection'
@@ -25,12 +28,14 @@ describe('AuthorizedCollectionSelectorComponent', () => {
collectionService = jasmine.createSpyObj('collectionService', { collectionService = jasmine.createSpyObj('collectionService', {
getAuthorizedCollection: createSuccessfulRemoteDataObject$(createPaginatedList([collection])) getAuthorizedCollection: createSuccessfulRemoteDataObject$(createPaginatedList([collection]))
}); });
notificationsService = jasmine.createSpyObj('notificationsService', ['error']);
TestBed.configureTestingModule({ TestBed.configureTestingModule({
declarations: [AuthorizedCollectionSelectorComponent, VarDirective], declarations: [AuthorizedCollectionSelectorComponent, VarDirective],
imports: [TranslateModule.forRoot(), RouterTestingModule.withRoutes([])], imports: [TranslateModule.forRoot(), RouterTestingModule.withRoutes([])],
providers: [ providers: [
{ provide: SearchService, useValue: {} }, { provide: SearchService, useValue: {} },
{ provide: CollectionDataService, useValue: collectionService } { provide: CollectionDataService, useValue: collectionService },
{ provide: NotificationsService, useValue: notificationsService },
], ],
schemas: [NO_ERRORS_SCHEMA] schemas: [NO_ERRORS_SCHEMA]
}).compileComponents(); }).compileComponents();
@@ -45,10 +50,10 @@ describe('AuthorizedCollectionSelectorComponent', () => {
describe('search', () => { describe('search', () => {
it('should call getAuthorizedCollection and return the authorized collection in a SearchResult', (done) => { it('should call getAuthorizedCollection and return the authorized collection in a SearchResult', (done) => {
component.search('', 1).subscribe((result) => { component.search('', 1).subscribe((resultRD) => {
expect(collectionService.getAuthorizedCollection).toHaveBeenCalled(); expect(collectionService.getAuthorizedCollection).toHaveBeenCalled();
expect(result.page.length).toEqual(1); expect(resultRD.payload.page.length).toEqual(1);
expect(result.page[0].indexableObject).toEqual(collection); expect(resultRD.payload.page[0].indexableObject).toEqual(collection);
done(); done();
}); });
}); });

View File

@@ -3,13 +3,17 @@ import { DSOSelectorComponent } from '../dso-selector.component';
import { SearchService } from '../../../../core/shared/search/search.service'; import { SearchService } from '../../../../core/shared/search/search.service';
import { CollectionDataService } from '../../../../core/data/collection-data.service'; import { CollectionDataService } from '../../../../core/data/collection-data.service';
import { Observable } from 'rxjs/internal/Observable'; import { Observable } from 'rxjs/internal/Observable';
import { getFirstSucceededRemoteDataPayload } from '../../../../core/shared/operators'; import { getFirstCompletedRemoteData, getFirstSucceededRemoteDataPayload } from '../../../../core/shared/operators';
import { map } from 'rxjs/operators'; import { map } from 'rxjs/operators';
import { CollectionSearchResult } from '../../../object-collection/shared/collection-search-result.model'; import { CollectionSearchResult } from '../../../object-collection/shared/collection-search-result.model';
import { SearchResult } from '../../../search/search-result.model'; import { SearchResult } from '../../../search/search-result.model';
import { DSpaceObject } from '../../../../core/shared/dspace-object.model'; import { DSpaceObject } from '../../../../core/shared/dspace-object.model';
import { buildPaginatedList, PaginatedList } from '../../../../core/data/paginated-list.model'; import { buildPaginatedList, PaginatedList } from '../../../../core/data/paginated-list.model';
import { followLink } from '../../../utils/follow-link-config.model'; import { followLink } from '../../../utils/follow-link-config.model';
import { RemoteData } from '../../../../core/data/remote-data';
import { hasValue } from '../../../empty.util';
import { NotificationsService } from '../../../notifications/notifications.service';
import { TranslateService } from '@ngx-translate/core';
@Component({ @Component({
selector: 'ds-authorized-collection-selector', selector: 'ds-authorized-collection-selector',
@@ -21,8 +25,10 @@ import { followLink } from '../../../utils/follow-link-config.model';
*/ */
export class AuthorizedCollectionSelectorComponent extends DSOSelectorComponent { export class AuthorizedCollectionSelectorComponent extends DSOSelectorComponent {
constructor(protected searchService: SearchService, constructor(protected searchService: SearchService,
protected collectionDataService: CollectionDataService) { protected collectionDataService: CollectionDataService,
super(searchService); protected notifcationsService: NotificationsService,
protected translate: TranslateService) {
super(searchService, notifcationsService, translate);
} }
/** /**
@@ -37,13 +43,15 @@ export class AuthorizedCollectionSelectorComponent extends DSOSelectorComponent
* @param query Query to search objects for * @param query Query to search objects for
* @param page Page to retrieve * @param page Page to retrieve
*/ */
search(query: string, page: number): Observable<PaginatedList<SearchResult<DSpaceObject>>> { search(query: string, page: number): Observable<RemoteData<PaginatedList<SearchResult<DSpaceObject>>>> {
return this.collectionDataService.getAuthorizedCollection(query, Object.assign({ return this.collectionDataService.getAuthorizedCollection(query, Object.assign({
currentPage: page, currentPage: page,
elementsPerPage: this.defaultPagination.pageSize elementsPerPage: this.defaultPagination.pageSize
}),true, false, followLink('parentCommunity')).pipe( }),true, false, followLink('parentCommunity')).pipe(
getFirstSucceededRemoteDataPayload(), getFirstCompletedRemoteData(),
map((list) => buildPaginatedList(list.pageInfo, list.page.map((col) => Object.assign(new CollectionSearchResult(), { indexableObject: col })))) map((rd) => Object.assign(new RemoteData(null, null, null, null), rd, {
payload: hasValue(rd.payload) ? buildPaginatedList(rd.payload.pageInfo, rd.payload.page.map((col) => Object.assign(new CollectionSearchResult(), { indexableObject: col }))) : null,
}))
); );
} }
} }

View File

@@ -10,10 +10,11 @@
<div <div
infiniteScroll infiniteScroll
[infiniteScrollDistance]="1" [infiniteScrollDistance]="1"
[infiniteScrollThrottle]="300" [infiniteScrollThrottle]="0"
[infiniteScrollContainer]="'.scrollable-menu'" [infiniteScrollContainer]="'.scrollable-menu'"
[fromRoot]="true" [fromRoot]="true"
(scrolled)="onScrollDown()"> (scrolled)="onScrollDown()">
<ng-container *ngIf="listEntries">
<button class="list-group-item list-group-item-action border-0 disabled" <button class="list-group-item list-group-item-action border-0 disabled"
*ngIf="listEntries.length == 0"> *ngIf="listEntries.length == 0">
{{'dso-selector.no-results' | translate: { type: typesString } }} {{'dso-selector.no-results' | translate: { type: typesString } }}
@@ -27,7 +28,8 @@
<ds-listable-object-component-loader [object]="listEntry" [viewMode]="viewMode" <ds-listable-object-component-loader [object]="listEntry" [viewMode]="viewMode"
[linkType]=linkTypes.None [context]="getContext(listEntry.indexableObject.id)"></ds-listable-object-component-loader> [linkType]=linkTypes.None [context]="getContext(listEntry.indexableObject.id)"></ds-listable-object-component-loader>
</button> </button>
<button *ngIf="hasNextPage" </ng-container>
<button *ngIf="loading"
class="list-group-item list-group-item-action border-0 list-entry"> class="list-group-item list-group-item-action border-0 list-entry">
<ds-loading [showMessage]="false"></ds-loading> <ds-loading [showMessage]="false"></ds-loading>
</button> </button>

View File

@@ -6,10 +6,11 @@ import { SearchService } from '../../../core/shared/search/search.service';
import { DSpaceObjectType } from '../../../core/shared/dspace-object-type.model'; import { DSpaceObjectType } from '../../../core/shared/dspace-object-type.model';
import { ItemSearchResult } from '../../object-collection/shared/item-search-result.model'; import { ItemSearchResult } from '../../object-collection/shared/item-search-result.model';
import { Item } from '../../../core/shared/item.model'; import { Item } from '../../../core/shared/item.model';
import { createSuccessfulRemoteDataObject$ } from '../../remote-data.utils'; import { createFailedRemoteDataObject$, createSuccessfulRemoteDataObject$ } from '../../remote-data.utils';
import { PaginatedSearchOptions } from '../../search/paginated-search-options.model'; import { PaginatedSearchOptions } from '../../search/paginated-search-options.model';
import { hasValue } from '../../empty.util'; import { hasValue } from '../../empty.util';
import { createPaginatedList } from '../../testing/utils.test'; import { createPaginatedList } from '../../testing/utils.test';
import { NotificationsService } from '../../notifications/notifications.service';
describe('DSOSelectorComponent', () => { describe('DSOSelectorComponent', () => {
let component: DSOSelectorComponent; let component: DSOSelectorComponent;
@@ -59,12 +60,17 @@ describe('DSOSelectorComponent', () => {
}); });
} }
let notificationsService: NotificationsService;
beforeEach(waitForAsync(() => { beforeEach(waitForAsync(() => {
notificationsService = jasmine.createSpyObj('notificationsService', ['error']);
TestBed.configureTestingModule({ TestBed.configureTestingModule({
imports: [TranslateModule.forRoot()], imports: [TranslateModule.forRoot()],
declarations: [DSOSelectorComponent], declarations: [DSOSelectorComponent],
providers: [ providers: [
{ provide: SearchService, useValue: searchService }, { provide: SearchService, useValue: searchService },
{ provide: NotificationsService, useValue: notificationsService },
], ],
schemas: [NO_ERRORS_SCHEMA] schemas: [NO_ERRORS_SCHEMA]
}).compileComponents(); }).compileComponents();
@@ -104,4 +110,15 @@ describe('DSOSelectorComponent', () => {
}); });
}); });
}); });
describe('when search returns an error', () => {
beforeEach(() => {
spyOn(searchService, 'search').and.returnValue(createFailedRemoteDataObject$());
component.ngOnInit();
});
it('should display an error notification', () => {
expect(notificationsService.error).toHaveBeenCalled();
});
});
}); });

View File

@@ -27,10 +27,13 @@ import { DSpaceObjectType } from '../../../core/shared/dspace-object-type.model'
import { DSpaceObject } from '../../../core/shared/dspace-object.model'; import { DSpaceObject } from '../../../core/shared/dspace-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';
import { getFirstSucceededRemoteDataPayload } from '../../../core/shared/operators'; import { getFirstCompletedRemoteData, getFirstSucceededRemoteDataPayload } from '../../../core/shared/operators';
import { hasValue, isEmpty, isNotEmpty } from '../../empty.util'; import { hasNoValue, hasValue, isEmpty, isNotEmpty } from '../../empty.util';
import { PaginatedList, buildPaginatedList } from '../../../core/data/paginated-list.model'; import { PaginatedList, buildPaginatedList } from '../../../core/data/paginated-list.model';
import { SearchResult } from '../../search/search-result.model'; import { SearchResult } from '../../search/search-result.model';
import { RemoteData } from '../../../core/data/remote-data';
import { NotificationsService } from '../../notifications/notifications.service';
import { TranslateService } from '@ngx-translate/core';
@Component({ @Component({
selector: 'ds-dso-selector', selector: 'ds-dso-selector',
@@ -78,7 +81,7 @@ export class DSOSelectorComponent implements OnInit, OnDestroy {
/** /**
* List with search results of DSpace objects for the current query * List with search results of DSpace objects for the current query
*/ */
listEntries: SearchResult<DSpaceObject>[] = []; listEntries: SearchResult<DSpaceObject>[] = null;
/** /**
* The current page to load * The current page to load
@@ -93,9 +96,9 @@ export class DSOSelectorComponent implements OnInit, OnDestroy {
hasNextPage = false; hasNextPage = false;
/** /**
* Whether or not the list should be reset next time it receives a page to load * Whether or not new results are currently loading
*/ */
resetList = false; loading = false;
/** /**
* List of element references to all elements * List of element references to all elements
@@ -123,7 +126,9 @@ export class DSOSelectorComponent implements OnInit, OnDestroy {
*/ */
public subs: Subscription[] = []; public subs: Subscription[] = [];
constructor(protected searchService: SearchService) { constructor(protected searchService: SearchService,
protected notifcationsService: NotificationsService,
protected translate: TranslateService) {
} }
/** /**
@@ -136,7 +141,7 @@ export class DSOSelectorComponent implements OnInit, OnDestroy {
// Create an observable searching for the current DSO (return empty list if there's no current DSO) // Create an observable searching for the current DSO (return empty list if there's no current DSO)
let currentDSOResult$; let currentDSOResult$;
if (isNotEmpty(this.currentDSOId)) { if (isNotEmpty(this.currentDSOId)) {
currentDSOResult$ = this.search(this.getCurrentDSOQuery(), 1); currentDSOResult$ = this.search(this.getCurrentDSOQuery(), 1).pipe(getFirstSucceededRemoteDataPayload());
} else { } else {
currentDSOResult$ = observableOf(buildPaginatedList(undefined, [])); currentDSOResult$ = observableOf(buildPaginatedList(undefined, []));
} }
@@ -152,31 +157,41 @@ export class DSOSelectorComponent implements OnInit, OnDestroy {
this.currentPage$ this.currentPage$
).pipe( ).pipe(
switchMap(([currentDSOResult, query, page]: [PaginatedList<SearchResult<DSpaceObject>>, string, number]) => { switchMap(([currentDSOResult, query, page]: [PaginatedList<SearchResult<DSpaceObject>>, string, number]) => {
this.loading = true;
if (page === 1) { if (page === 1) {
// The first page is loading, this means we should reset the list instead of adding to it // The first page is loading, this means we should reset the list instead of adding to it
this.resetList = true; this.listEntries = null;
} }
return this.search(query, page).pipe( return this.search(query, page).pipe(
map((list) => { map((rd) => {
if (rd.hasSucceeded) {
// If it's the first page and no query is entered, add the current DSO to the start of the list // If it's the first page and no query is entered, add the current DSO to the start of the list
// If no query is entered, filter out the current DSO from the results, as it'll be displayed at the start of the list already // If no query is entered, filter out the current DSO from the results, as it'll be displayed at the start of the list already
list.page = [ rd.payload.page = [
...((isEmpty(query) && page === 1) ? currentDSOResult.page : []), ...((isEmpty(query) && page === 1) ? currentDSOResult.page : []),
...list.page.filter((result) => isNotEmpty(query) || result.indexableObject.id !== this.currentDSOId) ...rd.payload.page.filter((result) => isNotEmpty(query) || result.indexableObject.id !== this.currentDSOId)
]; ];
return list; } else if (rd.hasFailed) {
this.notifcationsService.error(this.translate.instant('dso-selector.error.title', { type: this.typesString }), rd.errorMessage);
}
return rd;
}) })
); );
}) })
).subscribe((list) => { ).subscribe((rd) => {
if (this.resetList) { this.loading = false;
this.listEntries = list.page; if (rd.hasSucceeded) {
this.resetList = false; if (hasNoValue(this.listEntries)) {
this.listEntries = rd.payload.page;
} else { } else {
this.listEntries.push(...list.page); this.listEntries.push(...rd.payload.page);
} }
// Check if there are more pages available after the current one // Check if there are more pages available after the current one
this.hasNextPage = list.totalElements > this.listEntries.length; this.hasNextPage = rd.payload.totalElements > this.listEntries.length;
} else {
this.listEntries = null;
this.hasNextPage = false;
}
})); }));
} }
@@ -192,7 +207,7 @@ export class DSOSelectorComponent implements OnInit, OnDestroy {
* @param query Query to search objects for * @param query Query to search objects for
* @param page Page to retrieve * @param page Page to retrieve
*/ */
search(query: string, page: number): Observable<PaginatedList<SearchResult<DSpaceObject>>> { search(query: string, page: number): Observable<RemoteData<PaginatedList<SearchResult<DSpaceObject>>>> {
return this.searchService.search( return this.searchService.search(
new PaginatedSearchOptions({ new PaginatedSearchOptions({
query: query, query: query,
@@ -202,7 +217,7 @@ export class DSOSelectorComponent implements OnInit, OnDestroy {
}) })
}) })
).pipe( ).pipe(
getFirstSucceededRemoteDataPayload() getFirstCompletedRemoteData()
); );
} }
@@ -210,7 +225,7 @@ export class DSOSelectorComponent implements OnInit, OnDestroy {
* When the user reaches the bottom of the page (or almost) and there's a next page available, increase the current page * When the user reaches the bottom of the page (or almost) and there's a next page available, increase the current page
*/ */
onScrollDown() { onScrollDown() {
if (this.hasNextPage) { if (this.hasNextPage && !this.loading) {
this.currentPage$.next(this.currentPage$.value + 1); this.currentPage$.next(this.currentPage$.value + 1);
} }
} }

View File

@@ -1168,6 +1168,8 @@
"dso-selector.edit.item.head": "Edit item", "dso-selector.edit.item.head": "Edit item",
"dso-selector.error.title": "An error occurred searching for a {{ type }}",
"dso-selector.export-metadata.dspaceobject.head": "Export metadata from", "dso-selector.export-metadata.dspaceobject.head": "Export metadata from",
"dso-selector.no-results": "No {{ type }} found", "dso-selector.no-results": "No {{ type }} found",