[CST-4591] Change collection dropdown in order to accept entity type as input

This commit is contained in:
Giuseppe Digilio
2021-09-22 17:34:54 +02:00
parent 1f941acda4
commit 51ada1c933
8 changed files with 179 additions and 39 deletions

View File

@@ -11,14 +11,13 @@
<div
class="scrollable-menu"
aria-labelledby="dropdownMenuButton"
(scroll)="onScroll($event)">
<div
(scroll)="onScroll($event)"
infiniteScroll
[infiniteScrollDistance]="2"
[infiniteScrollDistance]="5"
[infiniteScrollThrottle]="300"
[infiniteScrollUpDistance]="1.5"
[infiniteScrollContainer]="'.scrollable-menu'"
[fromRoot]="true"
[scrollWindow]="false"
(scrolled)="onScrollDown()">
<button class="dropdown-item disabled" *ngIf="searchListCollection?.length == 0 && !(isLoadingList | async)">
{{'submission.sections.general.no-collection' | translate}}
@@ -39,5 +38,5 @@
<ds-loading message="{{'loading.default' | translate}}">
</ds-loading>
</button>
</div>
</div>
</div>

View File

@@ -5,21 +5,16 @@ import { By } from '@angular/platform-browser';
import { getTestScheduler } from 'jasmine-marbles';
import { TestScheduler } from 'rxjs/testing';
import { TranslateLoader, TranslateModule } from '@ngx-translate/core';
import { of as observableOf } from 'rxjs';
import { CollectionDropdownComponent } from './collection-dropdown.component';
import { RemoteData } from '../../core/data/remote-data';
import { buildPaginatedList, PaginatedList } from '../../core/data/paginated-list.model';
import { createSuccessfulRemoteDataObject, createSuccessfulRemoteDataObject$ } from '../remote-data.utils';
import { buildPaginatedList } from '../../core/data/paginated-list.model';
import { createSuccessfulRemoteDataObject$ } from '../remote-data.utils';
import { PageInfo } from '../../core/shared/page-info.model';
import { Collection } from '../../core/shared/collection.model';
import { CollectionDataService } from '../../core/data/collection-data.service';
import { TranslateLoaderMock } from '../mocks/translate-loader.mock';
import { Community } from '../../core/shared/community.model';
import { MockElementRef } from '../testing/element-ref.mock';
import { FollowLinkConfig } from '../utils/follow-link-config.model';
import { FindListOptions } from '../../core/data/request.models';
import { Observable } from 'rxjs/internal/Observable';
const community: Community = Object.assign(new Community(), {
id: 'ce64f48e-2c9b-411a-ac36-ee429c0e6a88',
@@ -99,17 +94,6 @@ const listElementMock = {
}
};
// tslint:disable-next-line: max-classes-per-file
class CollectionDataServiceMock {
getAuthorizedCollection(query: string, options: FindListOptions = {}, ...linksToFollow: FollowLinkConfig<Collection>[]): Observable<RemoteData<PaginatedList<Collection>>> {
return observableOf(
createSuccessfulRemoteDataObject(
buildPaginatedList(new PageInfo(), collections)
)
);
}
}
describe('CollectionDropdownComponent', () => {
let component: CollectionDropdownComponent;
let componentAsAny: any;
@@ -117,12 +101,19 @@ describe('CollectionDropdownComponent', () => {
let scheduler: TestScheduler;
const collectionDataServiceMock: any = jasmine.createSpyObj('CollectionDataService', {
getAuthorizedCollection: jasmine.createSpy('getAuthorizedCollection')
getAuthorizedCollection: jasmine.createSpy('getAuthorizedCollection'),
getAuthorizedCollectionByEntityType: jasmine.createSpy('getAuthorizedCollectionByEntityType')
});
const paginatedCollection = buildPaginatedList(new PageInfo(), collections);
const paginatedCollectionRD$ = createSuccessfulRemoteDataObject$(paginatedCollection);
const paginatedEmptyCollection = buildPaginatedList(new PageInfo(), []);
const paginatedEmptyCollectionRD$ = createSuccessfulRemoteDataObject$(paginatedEmptyCollection);
const paginatedOneElementCollection = buildPaginatedList(new PageInfo(), [collections[0]]);
const paginatedOneElementCollectionRD$ = createSuccessfulRemoteDataObject$(paginatedOneElementCollection);
beforeEach(waitForAsync(() => {
TestBed.configureTestingModule({
imports: [
@@ -150,6 +141,7 @@ describe('CollectionDropdownComponent', () => {
component = fixture.componentInstance;
componentAsAny = component;
componentAsAny.collectionDataService.getAuthorizedCollection.and.returnValue(paginatedCollectionRD$);
componentAsAny.collectionDataService.getAuthorizedCollectionByEntityType.and.returnValue(paginatedCollectionRD$);
});
it('should init component with collection list', () => {
@@ -225,4 +217,48 @@ describe('CollectionDropdownComponent', () => {
expect(component.hasNextPage).toEqual(true);
expect(component.searchListCollection).toEqual([]);
});
it('should invoke the method getAuthorizedCollectionByEntityType of CollectionDataService when entityType is set',() => {
component.entityType = 'rel';
scheduler.schedule(() => fixture.detectChanges());
scheduler.flush();
expect((component as any).collectionDataService.getAuthorizedCollectionByEntityType).toHaveBeenCalled();
});
it('should emit hasChoice true when totalElements is greater then one', () => {
spyOn(component.hasChoice, 'emit').and.callThrough();
component.ngOnInit();
fixture.detectChanges();
expect(component.hasChoice.emit).toHaveBeenCalledWith(true);
});
it('should emit hasChoice false when totalElements is not greater then one', () => {
componentAsAny.collectionDataService.getAuthorizedCollection.and.returnValue(paginatedEmptyCollectionRD$);
componentAsAny.collectionDataService.getAuthorizedCollectionByEntityType.and.returnValue(paginatedEmptyCollectionRD$);
spyOn(component.hasChoice, 'emit').and.callThrough();
component.ngOnInit();
fixture.detectChanges();
expect(component.hasChoice.emit).toHaveBeenCalledWith(false);
});
it('should emit theOnlySelectable when totalElements is equal to one', () => {
componentAsAny.collectionDataService.getAuthorizedCollection.and.returnValue(paginatedOneElementCollectionRD$);
componentAsAny.collectionDataService.getAuthorizedCollectionByEntityType.and.returnValue(paginatedOneElementCollectionRD$);
spyOn(component.theOnlySelectable, 'emit').and.callThrough();
component.ngOnInit();
fixture.detectChanges();
const expectedTheOnlySelectable = {
communities: [ { id: 'ce64f48e-2c9b-411a-ac36-ee429c0e6a88', name: 'Community 1', uuid: 'ce64f48e-2c9b-411a-ac36-ee429c0e6a88' } ],
collection: { id: 'ce64f48e-2c9b-411a-ac36-ee429c0e6a88', uuid: 'ce64f48e-2c9b-411a-ac36-ee429c0e6a88', name: 'Collection 1' }
};
expect(component.theOnlySelectable.emit).toHaveBeenCalledWith(expectedTheOnlySelectable);
});
});

View File

@@ -4,6 +4,7 @@ import {
ElementRef,
EventEmitter,
HostListener,
Input,
OnDestroy,
OnInit,
Output
@@ -11,7 +12,7 @@ import {
import { FormControl } from '@angular/forms';
import { BehaviorSubject, Observable, Subscription } from 'rxjs';
import { debounceTime, distinctUntilChanged, map, mergeMap, reduce, startWith, switchMap } from 'rxjs/operators';
import { debounceTime, distinctUntilChanged, map, mergeMap, reduce, startWith, switchMap, take } from 'rxjs/operators';
import { hasValue } from '../empty.util';
import { RemoteData } from '../../core/data/remote-data';
@@ -106,6 +107,21 @@ export class CollectionDropdownComponent implements OnInit, OnDestroy {
*/
currentQuery: string;
/**
* If present this value is used to filter collection list by entity type
*/
@Input() entityType: string;
/**
* Emit to notify whether collections to choice from are more than one
*/
@Output() hasChoice = new EventEmitter<boolean>();
/**
* Emit to notify the only selectable collection.
*/
@Output() theOnlySelectable = new EventEmitter<CollectionListEntry>();
constructor(
private changeDetectorRef: ChangeDetectorRef,
private collectionDataService: CollectionDataService,
@@ -190,14 +206,26 @@ export class CollectionDropdownComponent implements OnInit, OnDestroy {
elementsPerPage: 10,
currentPage: page
};
this.searchListCollection$ = this.collectionDataService
.getAuthorizedCollection(query, findOptions, true, false, followLink('parentCommunity'))
.pipe(
let searchListService$: Observable<RemoteData<PaginatedList<Collection>>> = null;
if (this.entityType) {
searchListService$ = this.collectionDataService
.getAuthorizedCollectionByEntityType(
query,
this.entityType,
findOptions,
false,
followLink('parentCommunity'));
} else {
searchListService$ = this.collectionDataService
.getAuthorizedCollection(query, findOptions, true, false, followLink('parentCommunity'));
}
this.searchListCollection$ = searchListService$.pipe(
getFirstSucceededRemoteWithNotEmptyData(),
switchMap((collections: RemoteData<PaginatedList<Collection>>) => {
if ( (this.searchListCollection.length + findOptions.elementsPerPage) >= collections.payload.totalElements ) {
this.hasNextPage = false;
}
this.emitSelectionEvents(collections);
return collections.payload.page;
}),
mergeMap((collection: Collection) => collection.parentCommunity.pipe(
@@ -247,4 +275,28 @@ export class CollectionDropdownComponent implements OnInit, OnDestroy {
hideShowLoader(hideShow: boolean) {
this.isLoadingList.next(hideShow);
}
/**
* Emit events related to the number of selectable collections.
* hasChoice containing whether there are more then one selectable collections.
* theOnlySelectable containing the only collection available.
* @param collections
* @private
*/
private emitSelectionEvents(collections: RemoteData<PaginatedList<Collection>>) {
this.hasChoice.emit(collections.payload.totalElements > 1);
if (collections.payload.totalElements === 1) {
const collection = collections.payload.page[0];
collections.payload.page[0].parentCommunity.pipe(
getFirstSucceededRemoteDataPayload(),
take(1)
).subscribe((community: Community) => {
this.theOnlySelectable.emit({
communities: [{ id: community.id, name: community.name, uuid: community.id }],
collection: { id: collection.id, uuid: collection.id, name: collection.name }
});
});
}
}
}

View File

@@ -26,7 +26,8 @@ describe('AuthorizedCollectionSelectorComponent', () => {
id: 'authorized-collection'
});
collectionService = jasmine.createSpyObj('collectionService', {
getAuthorizedCollection: createSuccessfulRemoteDataObject$(createPaginatedList([collection]))
getAuthorizedCollection: createSuccessfulRemoteDataObject$(createPaginatedList([collection])),
getAuthorizedCollectionByEntityType: createSuccessfulRemoteDataObject$(createPaginatedList([collection]))
});
notificationsService = jasmine.createSpyObj('notificationsService', ['error']);
TestBed.configureTestingModule({
@@ -49,12 +50,27 @@ describe('AuthorizedCollectionSelectorComponent', () => {
});
describe('search', () => {
it('should call getAuthorizedCollection and return the authorized collection in a SearchResult', (done) => {
describe('when has no entity type', () => {
it('should call getAuthorizedCollection and return the authorized collection in a SearchResult', (done) => {
component.search('', 1).subscribe((resultRD) => {
expect(collectionService.getAuthorizedCollection).toHaveBeenCalled();
expect(collectionService.getAuthorizedCollection).toHaveBeenCalled();
expect(resultRD.payload.page.length).toEqual(1);
expect(resultRD.payload.page[0].indexableObject).toEqual(collection);
done();
done();
});
});
});
describe('when has entity type', () => {
it('should call getAuthorizedCollectionByEntityType and return the authorized collection in a SearchResult', (done) => {
component.entityType = 'test';
fixture.detectChanges();
component.search('', 1).subscribe((resultRD) => {
expect(collectionService.getAuthorizedCollectionByEntityType).toHaveBeenCalled();
expect(resultRD.payload.page.length).toEqual(1);
expect(resultRD.payload.page[0].indexableObject).toEqual(collection);
done();
});
});
});
});

View File

@@ -1,8 +1,8 @@
import { Component } from '@angular/core';
import { Component, Input } from '@angular/core';
import { DSOSelectorComponent } from '../dso-selector.component';
import { SearchService } from '../../../../core/shared/search/search.service';
import { CollectionDataService } from '../../../../core/data/collection-data.service';
import { Observable } from 'rxjs/internal/Observable';
import { Observable } from 'rxjs';
import { getFirstCompletedRemoteData } from '../../../../core/shared/operators';
import { map } from 'rxjs/operators';
import { CollectionSearchResult } from '../../../object-collection/shared/collection-search-result.model';
@@ -14,6 +14,8 @@ import { RemoteData } from '../../../../core/data/remote-data';
import { hasValue } from '../../../empty.util';
import { NotificationsService } from '../../../notifications/notifications.service';
import { TranslateService } from '@ngx-translate/core';
import { Collection } from '../../../../core/shared/collection.model';
import { FindListOptions } from '../../../../core/data/request.models';
@Component({
selector: 'ds-authorized-collection-selector',
@@ -24,6 +26,11 @@ import { TranslateService } from '@ngx-translate/core';
* Component rendering a list of collections to select from
*/
export class AuthorizedCollectionSelectorComponent extends DSOSelectorComponent {
/**
* If present this value is used to filter collection list by entity type
*/
@Input() entityType: string;
constructor(protected searchService: SearchService,
protected collectionDataService: CollectionDataService,
protected notifcationsService: NotificationsService,
@@ -44,10 +51,23 @@ export class AuthorizedCollectionSelectorComponent extends DSOSelectorComponent
* @param page Page to retrieve
*/
search(query: string, page: number): Observable<RemoteData<PaginatedList<SearchResult<DSpaceObject>>>> {
return this.collectionDataService.getAuthorizedCollection(query, Object.assign({
let searchListService$: Observable<RemoteData<PaginatedList<Collection>>> = null;
const findOptions: FindListOptions = {
currentPage: page,
elementsPerPage: this.defaultPagination.pageSize
}),true, false, followLink('parentCommunity')).pipe(
};
if (this.entityType) {
searchListService$ = this.collectionDataService
.getAuthorizedCollectionByEntityType(
query,
this.entityType,
findOptions);
} else {
searchListService$ = this.collectionDataService
.getAuthorizedCollection(query, findOptions, true, false, followLink('parentCommunity'));
}
return searchListService$.pipe(
getFirstCompletedRemoteData(),
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

@@ -6,6 +6,9 @@
</div>
<div class="modal-body">
<h5 *ngIf="header" class="px-2">{{header | translate}}</h5>
<ds-authorized-collection-selector [currentDSOId]="dsoRD?.payload.uuid" [types]="selectorTypes" (onSelect)="selectObject($event)"></ds-authorized-collection-selector>
<ds-authorized-collection-selector [currentDSOId]="dsoRD?.payload.uuid"
[entityType]="entityType"
[types]="selectorTypes"
(onSelect)="selectObject($event)"></ds-authorized-collection-selector>
</div>
</div>

View File

@@ -69,4 +69,10 @@ describe('CreateItemParentSelectorComponent', () => {
expect(router.navigate).toHaveBeenCalledWith(['/submit'], { queryParams: { collection: collection.uuid } });
});
it('should call navigate on the router with entityType parameter', () => {
const entityType = 'Person';
component.entityType = entityType;
component.navigate(collection);
expect(router.navigate).toHaveBeenCalledWith(['/submit'], { queryParams: { collection: collection.uuid, entityType: entityType } });
});
});

View File

@@ -1,4 +1,4 @@
import { Component, OnInit } from '@angular/core';
import { Component, Input, OnInit } from '@angular/core';
import { ActivatedRoute, NavigationExtras, Router } from '@angular/router';
import { DSpaceObjectType } from '../../../../core/shared/dspace-object-type.model';
import { DSpaceObject } from '../../../../core/shared/dspace-object.model';
@@ -22,6 +22,11 @@ export class CreateItemParentSelectorComponent extends DSOSelectorModalWrapperCo
action = SelectorActionType.CREATE;
header = 'dso-selector.create.item.sub-level';
/**
* If present this value is used to filter collection list by entity type
*/
@Input() entityType: string;
constructor(protected activeModal: NgbActiveModal, protected route: ActivatedRoute, private router: Router) {
super(activeModal, route);
}
@@ -35,6 +40,9 @@ export class CreateItemParentSelectorComponent extends DSOSelectorModalWrapperCo
['collection']: dso.uuid,
}
};
if (this.entityType) {
navigationExtras.queryParams.entityType = this.entityType;
}
this.router.navigate(['/submit'], navigationExtras);
}
}