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

View File

@@ -5,21 +5,16 @@ import { By } from '@angular/platform-browser';
import { getTestScheduler } from 'jasmine-marbles'; import { getTestScheduler } from 'jasmine-marbles';
import { TestScheduler } from 'rxjs/testing'; import { TestScheduler } from 'rxjs/testing';
import { TranslateLoader, TranslateModule } from '@ngx-translate/core'; import { TranslateLoader, TranslateModule } from '@ngx-translate/core';
import { of as observableOf } from 'rxjs';
import { CollectionDropdownComponent } from './collection-dropdown.component'; import { CollectionDropdownComponent } from './collection-dropdown.component';
import { RemoteData } from '../../core/data/remote-data'; import { buildPaginatedList } from '../../core/data/paginated-list.model';
import { buildPaginatedList, PaginatedList } from '../../core/data/paginated-list.model'; import { createSuccessfulRemoteDataObject$ } from '../remote-data.utils';
import { createSuccessfulRemoteDataObject, createSuccessfulRemoteDataObject$ } from '../remote-data.utils';
import { PageInfo } from '../../core/shared/page-info.model'; import { PageInfo } from '../../core/shared/page-info.model';
import { Collection } from '../../core/shared/collection.model'; import { Collection } from '../../core/shared/collection.model';
import { CollectionDataService } from '../../core/data/collection-data.service'; import { CollectionDataService } from '../../core/data/collection-data.service';
import { TranslateLoaderMock } from '../mocks/translate-loader.mock'; import { TranslateLoaderMock } from '../mocks/translate-loader.mock';
import { Community } from '../../core/shared/community.model'; import { Community } from '../../core/shared/community.model';
import { MockElementRef } from '../testing/element-ref.mock'; 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(), { const community: Community = Object.assign(new Community(), {
id: 'ce64f48e-2c9b-411a-ac36-ee429c0e6a88', 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', () => { describe('CollectionDropdownComponent', () => {
let component: CollectionDropdownComponent; let component: CollectionDropdownComponent;
let componentAsAny: any; let componentAsAny: any;
@@ -117,12 +101,19 @@ describe('CollectionDropdownComponent', () => {
let scheduler: TestScheduler; let scheduler: TestScheduler;
const collectionDataServiceMock: any = jasmine.createSpyObj('CollectionDataService', { 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 paginatedCollection = buildPaginatedList(new PageInfo(), collections);
const paginatedCollectionRD$ = createSuccessfulRemoteDataObject$(paginatedCollection); 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(() => { beforeEach(waitForAsync(() => {
TestBed.configureTestingModule({ TestBed.configureTestingModule({
imports: [ imports: [
@@ -150,6 +141,7 @@ describe('CollectionDropdownComponent', () => {
component = fixture.componentInstance; component = fixture.componentInstance;
componentAsAny = component; componentAsAny = component;
componentAsAny.collectionDataService.getAuthorizedCollection.and.returnValue(paginatedCollectionRD$); componentAsAny.collectionDataService.getAuthorizedCollection.and.returnValue(paginatedCollectionRD$);
componentAsAny.collectionDataService.getAuthorizedCollectionByEntityType.and.returnValue(paginatedCollectionRD$);
}); });
it('should init component with collection list', () => { it('should init component with collection list', () => {
@@ -225,4 +217,48 @@ describe('CollectionDropdownComponent', () => {
expect(component.hasNextPage).toEqual(true); expect(component.hasNextPage).toEqual(true);
expect(component.searchListCollection).toEqual([]); 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, ElementRef,
EventEmitter, EventEmitter,
HostListener, HostListener,
Input,
OnDestroy, OnDestroy,
OnInit, OnInit,
Output Output
@@ -11,7 +12,7 @@ import {
import { FormControl } from '@angular/forms'; import { FormControl } from '@angular/forms';
import { BehaviorSubject, Observable, Subscription } from 'rxjs'; 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 { hasValue } from '../empty.util';
import { RemoteData } from '../../core/data/remote-data'; import { RemoteData } from '../../core/data/remote-data';
@@ -106,6 +107,21 @@ export class CollectionDropdownComponent implements OnInit, OnDestroy {
*/ */
currentQuery: string; 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( constructor(
private changeDetectorRef: ChangeDetectorRef, private changeDetectorRef: ChangeDetectorRef,
private collectionDataService: CollectionDataService, private collectionDataService: CollectionDataService,
@@ -190,14 +206,26 @@ export class CollectionDropdownComponent implements OnInit, OnDestroy {
elementsPerPage: 10, elementsPerPage: 10,
currentPage: page currentPage: page
}; };
this.searchListCollection$ = this.collectionDataService let searchListService$: Observable<RemoteData<PaginatedList<Collection>>> = null;
.getAuthorizedCollection(query, findOptions, true, false, followLink('parentCommunity')) if (this.entityType) {
.pipe( 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(), getFirstSucceededRemoteWithNotEmptyData(),
switchMap((collections: RemoteData<PaginatedList<Collection>>) => { switchMap((collections: RemoteData<PaginatedList<Collection>>) => {
if ( (this.searchListCollection.length + findOptions.elementsPerPage) >= collections.payload.totalElements ) { if ( (this.searchListCollection.length + findOptions.elementsPerPage) >= collections.payload.totalElements ) {
this.hasNextPage = false; this.hasNextPage = false;
} }
this.emitSelectionEvents(collections);
return collections.payload.page; return collections.payload.page;
}), }),
mergeMap((collection: Collection) => collection.parentCommunity.pipe( mergeMap((collection: Collection) => collection.parentCommunity.pipe(
@@ -247,4 +275,28 @@ export class CollectionDropdownComponent implements OnInit, OnDestroy {
hideShowLoader(hideShow: boolean) { hideShowLoader(hideShow: boolean) {
this.isLoadingList.next(hideShow); 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' id: 'authorized-collection'
}); });
collectionService = jasmine.createSpyObj('collectionService', { collectionService = jasmine.createSpyObj('collectionService', {
getAuthorizedCollection: createSuccessfulRemoteDataObject$(createPaginatedList([collection])) getAuthorizedCollection: createSuccessfulRemoteDataObject$(createPaginatedList([collection])),
getAuthorizedCollectionByEntityType: createSuccessfulRemoteDataObject$(createPaginatedList([collection]))
}); });
notificationsService = jasmine.createSpyObj('notificationsService', ['error']); notificationsService = jasmine.createSpyObj('notificationsService', ['error']);
TestBed.configureTestingModule({ TestBed.configureTestingModule({
@@ -49,12 +50,27 @@ describe('AuthorizedCollectionSelectorComponent', () => {
}); });
describe('search', () => { 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) => { component.search('', 1).subscribe((resultRD) => {
expect(collectionService.getAuthorizedCollection).toHaveBeenCalled(); expect(collectionService.getAuthorizedCollection).toHaveBeenCalled();
expect(resultRD.payload.page.length).toEqual(1); expect(resultRD.payload.page.length).toEqual(1);
expect(resultRD.payload.page[0].indexableObject).toEqual(collection); 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 { 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';
import { getFirstCompletedRemoteData } from '../../../../core/shared/operators'; import { getFirstCompletedRemoteData } 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';
@@ -14,6 +14,8 @@ import { RemoteData } from '../../../../core/data/remote-data';
import { hasValue } from '../../../empty.util'; import { hasValue } from '../../../empty.util';
import { NotificationsService } from '../../../notifications/notifications.service'; import { NotificationsService } from '../../../notifications/notifications.service';
import { TranslateService } from '@ngx-translate/core'; import { TranslateService } from '@ngx-translate/core';
import { Collection } from '../../../../core/shared/collection.model';
import { FindListOptions } from '../../../../core/data/request.models';
@Component({ @Component({
selector: 'ds-authorized-collection-selector', selector: 'ds-authorized-collection-selector',
@@ -24,6 +26,11 @@ import { TranslateService } from '@ngx-translate/core';
* Component rendering a list of collections to select from * Component rendering a list of collections to select from
*/ */
export class AuthorizedCollectionSelectorComponent extends DSOSelectorComponent { 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, constructor(protected searchService: SearchService,
protected collectionDataService: CollectionDataService, protected collectionDataService: CollectionDataService,
protected notifcationsService: NotificationsService, protected notifcationsService: NotificationsService,
@@ -44,10 +51,23 @@ export class AuthorizedCollectionSelectorComponent extends DSOSelectorComponent
* @param page Page to retrieve * @param page Page to retrieve
*/ */
search(query: string, page: number): Observable<RemoteData<PaginatedList<SearchResult<DSpaceObject>>>> { 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, currentPage: page,
elementsPerPage: this.defaultPagination.pageSize 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(), getFirstCompletedRemoteData(),
map((rd) => Object.assign(new RemoteData(null, null, null, null), rd, { 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, 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>
<div class="modal-body"> <div class="modal-body">
<h5 *ngIf="header" class="px-2">{{header | translate}}</h5> <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>
</div> </div>

View File

@@ -69,4 +69,10 @@ describe('CreateItemParentSelectorComponent', () => {
expect(router.navigate).toHaveBeenCalledWith(['/submit'], { queryParams: { collection: collection.uuid } }); 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 { ActivatedRoute, NavigationExtras, Router } from '@angular/router';
import { DSpaceObjectType } from '../../../../core/shared/dspace-object-type.model'; 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';
@@ -22,6 +22,11 @@ export class CreateItemParentSelectorComponent extends DSOSelectorModalWrapperCo
action = SelectorActionType.CREATE; action = SelectorActionType.CREATE;
header = 'dso-selector.create.item.sub-level'; 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) { constructor(protected activeModal: NgbActiveModal, protected route: ActivatedRoute, private router: Router) {
super(activeModal, route); super(activeModal, route);
} }
@@ -35,6 +40,9 @@ export class CreateItemParentSelectorComponent extends DSOSelectorModalWrapperCo
['collection']: dso.uuid, ['collection']: dso.uuid,
} }
}; };
if (this.entityType) {
navigationExtras.queryParams.entityType = this.entityType;
}
this.router.navigate(['/submit'], navigationExtras); this.router.navigate(['/submit'], navigationExtras);
} }
} }