From 225a5640e55d5bb8475188635e4fae26ea3c1371 Mon Sep 17 00:00:00 2001 From: Giuseppe Digilio Date: Wed, 22 Sep 2021 17:20:04 +0200 Subject: [PATCH 01/21] [CST-4591] Add methods to retrieve collections by entity type --- .../core/data/collection-data.service.spec.ts | 2 +- src/app/core/data/collection-data.service.ts | 75 +++++++++++++++++-- 2 files changed, 68 insertions(+), 9 deletions(-) diff --git a/src/app/core/data/collection-data.service.spec.ts b/src/app/core/data/collection-data.service.spec.ts index 00aede27a6..031e5ecf47 100644 --- a/src/app/core/data/collection-data.service.spec.ts +++ b/src/app/core/data/collection-data.service.spec.ts @@ -17,7 +17,7 @@ import { createFailedRemoteDataObject$, createSuccessfulRemoteDataObject, createSuccessfulRemoteDataObject$ -} from 'src/app/shared/remote-data.utils'; +} from '../../shared/remote-data.utils'; import { cold, getTestScheduler, hot } from 'jasmine-marbles'; import { TestScheduler } from 'rxjs/testing'; import { Observable } from 'rxjs'; diff --git a/src/app/core/data/collection-data.service.ts b/src/app/core/data/collection-data.service.ts index f58f36450f..2014394000 100644 --- a/src/app/core/data/collection-data.service.ts +++ b/src/app/core/data/collection-data.service.ts @@ -27,12 +27,7 @@ import { CommunityDataService } from './community-data.service'; import { DSOChangeAnalyzer } from './dso-change-analyzer.service'; import { PaginatedList } from './paginated-list.model'; import { RemoteData } from './remote-data'; -import { - ContentSourceRequest, - FindListOptions, - UpdateContentSourceRequest, - RestRequest -} from './request.models'; +import { ContentSourceRequest, FindListOptions, RestRequest, UpdateContentSourceRequest } from './request.models'; import { RequestService } from './request.service'; import { BitstreamDataService } from './bitstream-data.service'; @@ -84,16 +79,48 @@ export class CollectionDataService extends ComColDataService { filter((collections: RemoteData>) => !collections.isResponsePending)); } + /** + * Get all collections the user is authorized to submit to + * + * @param query limit the returned collection to those with metadata values matching the query terms. + * @param entityType The entity type used to limit the returned collection + * @param options The [[FindListOptions]] object + * @param reRequestOnStale Whether or not the request should automatically be re-requested after + * the response becomes stale + * @param linksToFollow The array of [[FollowLinkConfig]] + * @return Observable>> + * collection list + */ + getAuthorizedCollectionByEntityType( + query: string, + entityType: string, + options: FindListOptions = {}, + reRequestOnStale = true, + ...linksToFollow: FollowLinkConfig[]): Observable>> { + const searchHref = 'findSubmitAuthorizedByEntityType'; + options = Object.assign({}, options, { + searchParams: [ + new RequestParam('query', query), + new RequestParam('entityType', entityType) + ] + }); + + return this.searchBy(searchHref, options, true, reRequestOnStale, ...linksToFollow).pipe( + filter((collections: RemoteData>) => !collections.isResponsePending)); + } + /** * Get all collections the user is authorized to submit to, by community * * @param communityId The community id * @param query limit the returned collection to those with metadata values matching the query terms. * @param options The [[FindListOptions]] object + * @param reRequestOnStale Whether or not the request should automatically be re- + * requested after the response becomes stale * @return Observable>> * collection list */ - getAuthorizedCollectionByCommunity(communityId: string, query: string, options: FindListOptions = {}): Observable>> { + getAuthorizedCollectionByCommunity(communityId: string, query: string, options: FindListOptions = {}, reRequestOnStale = true,): Observable>> { const searchHref = 'findSubmitAuthorizedByCommunity'; options = Object.assign({}, options, { searchParams: [ @@ -102,7 +129,38 @@ export class CollectionDataService extends ComColDataService { ] }); - return this.searchBy(searchHref, options).pipe( + return this.searchBy(searchHref, options, reRequestOnStale).pipe( + filter((collections: RemoteData>) => !collections.isResponsePending)); + } + /** + * Get all collections the user is authorized to submit to, by community and has the metadata + * + * @param communityId The community id + * @param entityType The entity type used to limit the returned collection + * @param options The [[FindListOptions]] object + * @param reRequestOnStale Whether or not the request should automatically be re-requested after + * the response becomes stale + * @param linksToFollow The array of [[FollowLinkConfig]] + * @return Observable>> + * collection list + */ + getAuthorizedCollectionByCommunityAndEntityType( + communityId: string, + entityType: string, + options: FindListOptions = {}, + reRequestOnStale = true, + ...linksToFollow: FollowLinkConfig[]): Observable>> { + const searchHref = 'findSubmitAuthorizedByCommunityAndEntityType'; + const searchParams = [ + new RequestParam('uuid', communityId), + new RequestParam('entityType', entityType) + ]; + + options = Object.assign({}, options, { + searchParams: searchParams + }); + + return this.searchBy(searchHref, options, true, reRequestOnStale, ...linksToFollow).pipe( filter((collections: RemoteData>) => !collections.isResponsePending)); } @@ -214,4 +272,5 @@ export class CollectionDataService extends ComColDataService { findOwningCollectionFor(item: Item): Observable> { return this.findByHref(item._links.owningCollection.href); } + } From b20b6d003f20420c9f098ad1188f5f0b418fb01f Mon Sep 17 00:00:00 2001 From: Giuseppe Digilio Date: Wed, 22 Sep 2021 17:29:47 +0200 Subject: [PATCH 02/21] [CST-4591] Add methods to retrieve entity type related to collections --- src/app/core/data/entity-type.service.ts | 74 +++++++++++++++++++++++- 1 file changed, 71 insertions(+), 3 deletions(-) diff --git a/src/app/core/data/entity-type.service.ts b/src/app/core/data/entity-type.service.ts index ca9ea15bc6..40b9373107 100644 --- a/src/app/core/data/entity-type.service.ts +++ b/src/app/core/data/entity-type.service.ts @@ -10,13 +10,14 @@ import { NotificationsService } from '../../shared/notifications/notifications.s import { HttpClient } from '@angular/common/http'; import { DefaultChangeAnalyzer } from './default-change-analyzer.service'; import { Injectable } from '@angular/core'; +import { FindListOptions } from './request.models'; import { Observable } from 'rxjs'; -import { switchMap, take, map } from 'rxjs/operators'; +import { filter, map, switchMap, take } from 'rxjs/operators'; import { RemoteData } from './remote-data'; import { RelationshipType } from '../shared/item-relationships/relationship-type.model'; import { PaginatedList } from './paginated-list.model'; import { ItemType } from '../shared/item-relationships/item-type.model'; -import { getRemoteDataPayload, getFirstSucceededRemoteData } from '../shared/operators'; +import { getFirstSucceededRemoteData, getRemoteDataPayload } from '../shared/operators'; import { RelationshipTypeService } from './relationship-type.service'; /** @@ -56,7 +57,7 @@ export class EntityTypeService extends DataService { /** * Check whether a given entity type is the left type of a given relationship type, as an observable boolean * @param relationshipType the relationship type for which to check whether the given entity type is the left type - * @param entityType the entity type for which to check whether it is the left type of the given relationship type + * @param itemType the entity type for which to check whether it is the left type of the given relationship type */ isLeftType(relationshipType: RelationshipType, itemType: ItemType): Observable { @@ -67,6 +68,73 @@ export class EntityTypeService extends DataService { ); } + /** + * Returns a list of entity types for which there is at least one collection in which the user is authorized to submit + * + * @param {FindListOptions} options + */ + getAllAuthorizedRelationshipType(options: FindListOptions = {}): Observable>> { + const searchHref = 'findAllByAuthorizedCollection'; + + return this.searchBy(searchHref, options).pipe( + filter((type: RemoteData>) => !type.isResponsePending)); + } + + /** + * Used to verify if there are one or more entities available + */ + hasMoreThanOneAuthorized(): Observable { + const findListOptions: FindListOptions = { + elementsPerPage: 2, + currentPage: 1 + }; + return this.getAllAuthorizedRelationshipType(findListOptions).pipe( + map((result: RemoteData>) => { + let output: boolean; + if (result.payload) { + output = ( result.payload.page.length > 1 ); + } else { + output = false; + } + return output; + }) + ); + } + + /** + * It returns a list of entity types for which there is at least one collection + * in which the user is authorized to submit supported by at least one external data source provider + * + * @param {FindListOptions} options + */ + getAllAuthorizedRelationshipTypeImport(options: FindListOptions = {}): Observable>> { + const searchHref = 'findAllByAuthorizedExternalSource'; + + return this.searchBy(searchHref, options).pipe( + filter((type: RemoteData>) => !type.isResponsePending)); + } + + /** + * Used to verify if there are one or more entities available. To use with external source import. + */ + hasMoreThanOneAuthorizedImport(): Observable { + const findListOptions: FindListOptions = { + elementsPerPage: 2, + currentPage: 1 + }; + return this.getAllAuthorizedRelationshipTypeImport(findListOptions).pipe( + map((result: RemoteData>) => { + let output: boolean; + if (result.payload) { + output = ( result.payload.page.length > 1 ); + } else { + output = false; + } + return output; + }) + ); + } + /** * Get the allowed relationship types for an entity type * @param entityTypeId From 1f941acda4a54fd2d4258a386a2c8545b7e40a52 Mon Sep 17 00:00:00 2001 From: Giuseppe Digilio Date: Wed, 22 Sep 2021 17:31:21 +0200 Subject: [PATCH 03/21] [CST-4591] Add entity selection field to collection create/edit form --- .../collection-form.component.ts | 96 +++++++++++-------- .../collection-form/collection-form.models.ts | 53 ++++++++++ src/assets/i18n/en.json5 | 4 + 3 files changed, 112 insertions(+), 41 deletions(-) create mode 100644 src/app/collection-page/collection-form/collection-form.models.ts diff --git a/src/app/collection-page/collection-form/collection-form.component.ts b/src/app/collection-page/collection-form/collection-form.component.ts index e8b368a25f..c4c8635991 100644 --- a/src/app/collection-page/collection-form/collection-form.component.ts +++ b/src/app/collection-page/collection-form/collection-form.component.ts @@ -1,18 +1,28 @@ -import { Component, Input } from '@angular/core'; +import { Component, Input, OnInit } from '@angular/core'; + +import { Observable } from 'rxjs'; +import { TranslateService } from '@ngx-translate/core'; import { DynamicFormControlModel, + DynamicFormOptionConfig, DynamicFormService, DynamicInputModel, + DynamicSelectModel, DynamicTextAreaModel } from '@ng-dynamic-forms/core'; + import { Collection } from '../../core/shared/collection.model'; import { ComColFormComponent } from '../../shared/comcol-forms/comcol-form/comcol-form.component'; -import { TranslateService } from '@ngx-translate/core'; import { NotificationsService } from '../../shared/notifications/notifications.service'; import { CommunityDataService } from '../../core/data/community-data.service'; import { AuthService } from '../../core/auth/auth.service'; import { RequestService } from '../../core/data/request.service'; import { ObjectCacheService } from '../../core/cache/object-cache.service'; +import { EntityTypeService } from '../../core/data/entity-type.service'; +import { ItemType } from '../../core/shared/item-relationships/item-type.model'; +import { MetadataValue } from '../../core/shared/metadata.models'; +import { getFirstSucceededRemoteListPayload } from '../../core/shared/operators'; +import { collectionFormEntityTypeSelectionConfig, collectionFormModels, } from './collection-form.models'; /** * Form used for creating and editing collections @@ -22,7 +32,7 @@ import { ObjectCacheService } from '../../core/cache/object-cache.service'; styleUrls: ['../../shared/comcol-forms/comcol-form/comcol-form.component.scss'], templateUrl: '../../shared/comcol-forms/comcol-form/comcol-form.component.html' }) -export class CollectionFormComponent extends ComColFormComponent { +export class CollectionFormComponent extends ComColFormComponent implements OnInit { /** * @type {Collection} A new collection when a collection is being created, an existing Input collection when a collection is being edited */ @@ -33,47 +43,17 @@ export class CollectionFormComponent extends ComColFormComponent { */ type = Collection.type; + /** + * The dynamic form field used for entity type selection + * @type {DynamicSelectModel} + */ + entityTypeSelection: DynamicSelectModel = new DynamicSelectModel(collectionFormEntityTypeSelectionConfig); + /** * The dynamic form fields used for creating/editing a collection * @type {(DynamicInputModel | DynamicTextAreaModel)[]} */ - formModel: DynamicFormControlModel[] = [ - new DynamicInputModel({ - id: 'title', - name: 'dc.title', - required: true, - validators: { - required: null - }, - errorMessages: { - required: 'Please enter a name for this title' - }, - }), - new DynamicTextAreaModel({ - id: 'description', - name: 'dc.description', - }), - new DynamicTextAreaModel({ - id: 'abstract', - name: 'dc.description.abstract', - }), - new DynamicTextAreaModel({ - id: 'rights', - name: 'dc.rights', - }), - new DynamicTextAreaModel({ - id: 'tableofcontents', - name: 'dc.description.tableofcontents', - }), - new DynamicTextAreaModel({ - id: 'license', - name: 'dc.rights.license', - }), - new DynamicTextAreaModel({ - id: 'provenance', - name: 'dc.description.provenance', - }), - ]; + formModel: DynamicFormControlModel[]; public constructor(protected formService: DynamicFormService, protected translate: TranslateService, @@ -81,7 +61,41 @@ export class CollectionFormComponent extends ComColFormComponent { protected authService: AuthService, protected dsoService: CommunityDataService, protected requestService: RequestService, - protected objectCache: ObjectCacheService) { + protected objectCache: ObjectCacheService, + protected entityTypeService: EntityTypeService) { super(formService, translate, notificationsService, authService, requestService, objectCache); } + + ngOnInit() { + + let currentRelationshipValue: MetadataValue[]; + if (this.dso && this.dso.metadata) { + currentRelationshipValue = this.dso.metadata['dspace.entity.type']; + } + + const entities$: Observable = this.entityTypeService.findAll({ elementsPerPage: 100, currentPage: 1 }).pipe( + getFirstSucceededRemoteListPayload() + ); + + // retrieve all entity types to populate the dropdowns selection + entities$.subscribe((entityTypes: ItemType[]) => { + + entityTypes.forEach((type: ItemType, index: number) => { + this.entityTypeSelection.add({ + disabled: false, + label: type.label, + value: type.label + } as DynamicFormOptionConfig); + if (currentRelationshipValue && currentRelationshipValue.length > 0 && currentRelationshipValue[0].value === type.label) { + this.entityTypeSelection.select(index); + this.entityTypeSelection.disabled = true; + } + }); + + this.formModel = [...collectionFormModels, this.entityTypeSelection]; + + super.ngOnInit(); + }); + + } } diff --git a/src/app/collection-page/collection-form/collection-form.models.ts b/src/app/collection-page/collection-form/collection-form.models.ts new file mode 100644 index 0000000000..dc26b787c4 --- /dev/null +++ b/src/app/collection-page/collection-form/collection-form.models.ts @@ -0,0 +1,53 @@ +import { DynamicFormControlModel, DynamicInputModel, DynamicTextAreaModel } from '@ng-dynamic-forms/core'; +import { DynamicSelectModelConfig } from '@ng-dynamic-forms/core/lib/model/select/dynamic-select.model'; + +export const collectionFormEntityTypeSelectionConfig: DynamicSelectModelConfig = { + id: 'entityType', + name: 'dspace.entity.type', + required: true, + disabled: false, + validators: { + required: null + }, + errorMessages: { + required: 'collection.form.errors.entityType.required' + }, +}; + +/** + * The dynamic form fields used for creating/editing a collection + * @type {(DynamicInputModel | DynamicTextAreaModel)[]} + */ +export const collectionFormModels: DynamicFormControlModel[] = [ + new DynamicInputModel({ + id: 'title', + name: 'dc.title', + required: true, + validators: { + required: null + }, + errorMessages: { + required: 'Please enter a name for this title' + }, + }), + new DynamicTextAreaModel({ + id: 'description', + name: 'dc.description', + }), + new DynamicTextAreaModel({ + id: 'abstract', + name: 'dc.description.abstract', + }), + new DynamicTextAreaModel({ + id: 'rights', + name: 'dc.rights', + }), + new DynamicTextAreaModel({ + id: 'tableofcontents', + name: 'dc.description.tableofcontents', + }), + new DynamicTextAreaModel({ + id: 'license', + name: 'dc.rights.license', + }) +]; diff --git a/src/assets/i18n/en.json5 b/src/assets/i18n/en.json5 index 8908ca42f6..f02f2afc84 100644 --- a/src/assets/i18n/en.json5 +++ b/src/assets/i18n/en.json5 @@ -854,6 +854,10 @@ "collection.form.title": "Name", + "collection.form.entityType": "Entity Type", + + "collection.form.errors.entityType.required": "Please choose an entity type for this collection", + "collection.listelement.badge": "Collection", From 51ada1c93377801b70b45cee9c15e589406ac4c6 Mon Sep 17 00:00:00 2001 From: Giuseppe Digilio Date: Wed, 22 Sep 2021 17:34:54 +0200 Subject: [PATCH 04/21] [CST-4591] Change collection dropdown in order to accept entity type as input --- .../collection-dropdown.component.html | 11 ++- .../collection-dropdown.component.spec.ts | 74 ++++++++++++++----- .../collection-dropdown.component.ts | 60 ++++++++++++++- ...ized-collection-selector.component.spec.ts | 24 +++++- ...uthorized-collection-selector.component.ts | 28 ++++++- ...create-item-parent-selector.component.html | 5 +- ...ate-item-parent-selector.component.spec.ts | 6 ++ .../create-item-parent-selector.component.ts | 10 ++- 8 files changed, 179 insertions(+), 39 deletions(-) diff --git a/src/app/shared/collection-dropdown/collection-dropdown.component.html b/src/app/shared/collection-dropdown/collection-dropdown.component.html index 36269294c1..ce5fe0deb8 100644 --- a/src/app/shared/collection-dropdown/collection-dropdown.component.html +++ b/src/app/shared/collection-dropdown/collection-dropdown.component.html @@ -11,14 +11,13 @@
-
-
-
\ No newline at end of file + + diff --git a/src/app/shared/collection-dropdown/collection-dropdown.component.spec.ts b/src/app/shared/collection-dropdown/collection-dropdown.component.spec.ts index f08df65ca4..612f5a1733 100644 --- a/src/app/shared/collection-dropdown/collection-dropdown.component.spec.ts +++ b/src/app/shared/collection-dropdown/collection-dropdown.component.spec.ts @@ -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[]): Observable>> { - 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); + }); }); diff --git a/src/app/shared/collection-dropdown/collection-dropdown.component.ts b/src/app/shared/collection-dropdown/collection-dropdown.component.ts index c91ddbdb0a..6c11613512 100644 --- a/src/app/shared/collection-dropdown/collection-dropdown.component.ts +++ b/src/app/shared/collection-dropdown/collection-dropdown.component.ts @@ -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(); + + /** + * Emit to notify the only selectable collection. + */ + @Output() theOnlySelectable = new EventEmitter(); + 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>> = 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>) => { 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>) { + 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 } + }); + }); + } + } + } diff --git a/src/app/shared/dso-selector/dso-selector/authorized-collection-selector/authorized-collection-selector.component.spec.ts b/src/app/shared/dso-selector/dso-selector/authorized-collection-selector/authorized-collection-selector.component.spec.ts index 55634dbf7f..b46df8ff36 100644 --- a/src/app/shared/dso-selector/dso-selector/authorized-collection-selector/authorized-collection-selector.component.spec.ts +++ b/src/app/shared/dso-selector/dso-selector/authorized-collection-selector/authorized-collection-selector.component.spec.ts @@ -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(); + }); }); }); }); diff --git a/src/app/shared/dso-selector/dso-selector/authorized-collection-selector/authorized-collection-selector.component.ts b/src/app/shared/dso-selector/dso-selector/authorized-collection-selector/authorized-collection-selector.component.ts index bca1727542..b6aa0b3413 100644 --- a/src/app/shared/dso-selector/dso-selector/authorized-collection-selector/authorized-collection-selector.component.ts +++ b/src/app/shared/dso-selector/dso-selector/authorized-collection-selector/authorized-collection-selector.component.ts @@ -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>>> { - return this.collectionDataService.getAuthorizedCollection(query, Object.assign({ + let searchListService$: Observable>> = 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, diff --git a/src/app/shared/dso-selector/modal-wrappers/create-item-parent-selector/create-item-parent-selector.component.html b/src/app/shared/dso-selector/modal-wrappers/create-item-parent-selector/create-item-parent-selector.component.html index 8761e4eb9e..5fccd58f48 100644 --- a/src/app/shared/dso-selector/modal-wrappers/create-item-parent-selector/create-item-parent-selector.component.html +++ b/src/app/shared/dso-selector/modal-wrappers/create-item-parent-selector/create-item-parent-selector.component.html @@ -6,6 +6,9 @@ diff --git a/src/app/shared/dso-selector/modal-wrappers/create-item-parent-selector/create-item-parent-selector.component.spec.ts b/src/app/shared/dso-selector/modal-wrappers/create-item-parent-selector/create-item-parent-selector.component.spec.ts index 3afb3c5e9a..90bd07c52b 100644 --- a/src/app/shared/dso-selector/modal-wrappers/create-item-parent-selector/create-item-parent-selector.component.spec.ts +++ b/src/app/shared/dso-selector/modal-wrappers/create-item-parent-selector/create-item-parent-selector.component.spec.ts @@ -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 } }); + }); }); diff --git a/src/app/shared/dso-selector/modal-wrappers/create-item-parent-selector/create-item-parent-selector.component.ts b/src/app/shared/dso-selector/modal-wrappers/create-item-parent-selector/create-item-parent-selector.component.ts index 03d7732fb0..b109be0af2 100644 --- a/src/app/shared/dso-selector/modal-wrappers/create-item-parent-selector/create-item-parent-selector.component.ts +++ b/src/app/shared/dso-selector/modal-wrappers/create-item-parent-selector/create-item-parent-selector.component.ts @@ -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); } } From 2e0095a587378f30ab707bab48b02c731f8e44cf Mon Sep 17 00:00:00 2001 From: Giuseppe Digilio Date: Wed, 22 Sep 2021 17:36:18 +0200 Subject: [PATCH 05/21] [CST-4591] Create entity scrollable dropdown --- .../entity-dropdown.component.html | 28 +++ .../entity-dropdown.component.scss | 19 ++ .../entity-dropdown.component.spec.ts | 167 ++++++++++++++ .../entity-dropdown.component.ts | 207 ++++++++++++++++++ src/app/shared/shared.module.ts | 5 +- 5 files changed, 425 insertions(+), 1 deletion(-) create mode 100644 src/app/shared/entity-dropdown/entity-dropdown.component.html create mode 100644 src/app/shared/entity-dropdown/entity-dropdown.component.scss create mode 100644 src/app/shared/entity-dropdown/entity-dropdown.component.spec.ts create mode 100644 src/app/shared/entity-dropdown/entity-dropdown.component.ts diff --git a/src/app/shared/entity-dropdown/entity-dropdown.component.html b/src/app/shared/entity-dropdown/entity-dropdown.component.html new file mode 100644 index 0000000000..59c242ef97 --- /dev/null +++ b/src/app/shared/entity-dropdown/entity-dropdown.component.html @@ -0,0 +1,28 @@ +
+ + + + +
diff --git a/src/app/shared/entity-dropdown/entity-dropdown.component.scss b/src/app/shared/entity-dropdown/entity-dropdown.component.scss new file mode 100644 index 0000000000..a5f43f359b --- /dev/null +++ b/src/app/shared/entity-dropdown/entity-dropdown.component.scss @@ -0,0 +1,19 @@ +.list-item:active { + color: white !important; +} + +.scrollable-menu { + height: auto; + max-height: var(--ds-dropdown-menu-max-height); + overflow-x: hidden; +} + +.entity-item { + border-bottom: var(--bs-dropdown-border-width) solid var(--bs-dropdown-border-color); +} + +#entityControlsDropdownMenu { + outline: 0; + left: 0 !important; + box-shadow: var(--bs-btn-focus-box-shadow); +} diff --git a/src/app/shared/entity-dropdown/entity-dropdown.component.spec.ts b/src/app/shared/entity-dropdown/entity-dropdown.component.spec.ts new file mode 100644 index 0000000000..0cc14cae22 --- /dev/null +++ b/src/app/shared/entity-dropdown/entity-dropdown.component.spec.ts @@ -0,0 +1,167 @@ +import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing'; +import { EntityDropdownComponent } from './entity-dropdown.component'; +import { getTestScheduler } from 'jasmine-marbles'; +import { createSuccessfulRemoteDataObject$ } from '../remote-data.utils'; +import { ItemType } from '../../core/shared/item-relationships/item-type.model'; +import { ChangeDetectorRef, NO_ERRORS_SCHEMA, Pipe, PipeTransform } from '@angular/core'; +import { EntityTypeService } from '../../core/data/entity-type.service'; +import { TestScheduler } from 'rxjs/testing'; +import { By } from '@angular/platform-browser'; +import { createPaginatedList } from '../testing/utils.test'; + +// tslint:disable-next-line:pipe-prefix +@Pipe({ name: 'translate' }) +class MockTranslatePipe implements PipeTransform { + transform(value: string): string { + return value; + } +} + +const entities: ItemType[] = [ + Object.assign(new ItemType(), { + id: 'ce64f48e-2c9b-411a-ac36-ee429c0e6a88', + label: 'Entity_1', + uuid: 'UUID-ce64f48e-2c9b-411a-ac36-ee429c0e6a88' + }), + Object.assign(new ItemType(), { + id: '59ee713b-ee53-4220-8c3f-9860dc84fe33', + label: 'Entity_2', + uuid: 'UUID-59ee713b-ee53-4220-8c3f-9860dc84fe33' + }), + Object.assign(new ItemType(), { + id: 'e9dbf393-7127-415f-8919-55be34a6e9ed', + label: 'Entity_3', + uuid: 'UUID-7127-415f-8919-55be34a6e9ed' + }), + Object.assign(new ItemType(), { + id: '59da2ff0-9bf4-45bf-88be-e35abd33f304', + label: 'Entity_4', + uuid: 'UUID-59da2ff0-9bf4-45bf-88be-e35abd33f304' + }), + Object.assign(new ItemType(), { + id: 'a5159760-f362-4659-9e81-e3253ad91ede', + label: 'Entity_5', + uuid: 'UUID-a5159760-f362-4659-9e81-e3253ad91ede' + }), +]; + +const listElementMock: ItemType = Object.assign( + new ItemType(), { + id: 'ce64f48e-2c9b-411a-ac36-ee429c0e6a88', + label: 'Entity_1', + uuid: 'UUID-ce64f48e-2c9b-411a-ac36-ee429c0e6a88' +} +); + +describe('EntityDropdownComponent', () => { + let component: EntityDropdownComponent; + let componentAsAny: any; + let fixture: ComponentFixture; + let scheduler: TestScheduler; + + const entityTypeServiceMock: any = jasmine.createSpyObj('EntityTypeService', { + getAllAuthorizedRelationshipType: jasmine.createSpy('getAllAuthorizedRelationshipType'), + getAllAuthorizedRelationshipTypeImport: jasmine.createSpy('getAllAuthorizedRelationshipTypeImport') + }); + + + let translatePipeSpy: jasmine.Spy; + + const paginatedEntities = createPaginatedList(entities); + const paginatedEntitiesRD$ = createSuccessfulRemoteDataObject$(paginatedEntities); + + beforeEach(waitForAsync(() => { + TestBed.configureTestingModule({ + imports: [], + declarations: [EntityDropdownComponent, MockTranslatePipe], + providers: [ + { provide: EntityTypeService, useValue: entityTypeServiceMock }, + ChangeDetectorRef + ], + schemas: [NO_ERRORS_SCHEMA] + }) + .compileComponents(); + })); + + beforeEach(() => { + scheduler = getTestScheduler(); + fixture = TestBed.createComponent(EntityDropdownComponent); + component = fixture.componentInstance; + componentAsAny = fixture.componentInstance; + componentAsAny.entityTypeService.getAllAuthorizedRelationshipType.and.returnValue(paginatedEntitiesRD$); + componentAsAny.entityTypeService.getAllAuthorizedRelationshipTypeImport.and.returnValue(paginatedEntitiesRD$); + component.isSubmission = true; + + translatePipeSpy = spyOn(MockTranslatePipe.prototype, 'transform'); + }); + + it('should translate entries', () => { + scheduler.schedule(() => fixture.detectChanges()); + scheduler.flush(); + + expect(translatePipeSpy).toHaveBeenCalledWith('entity_1.listelement.badge'); + }); + + it('should init component with entities list', () => { + spyOn(component.subs, 'push'); + spyOn(component, 'resetPagination'); + spyOn(component, 'populateEntityList').and.callThrough(); + + scheduler.schedule(() => fixture.detectChanges()); + scheduler.flush(); + const elements = fixture.debugElement.queryAll(By.css('.entity-item')); + + expect(elements.length).toEqual(5); + expect(component.subs.push).toHaveBeenCalled(); + expect(component.resetPagination).toHaveBeenCalled(); + expect(component.populateEntityList).toHaveBeenCalled(); + expect((component as any).entityTypeService.getAllAuthorizedRelationshipType).toHaveBeenCalled(); + }); + + it('should trigger onSelect method when select a new entity from list', () => { + scheduler.schedule(() => fixture.detectChanges()); + scheduler.flush(); + + spyOn(component, 'onSelect'); + const entityItem = fixture.debugElement.query(By.css('.entity-item:nth-child(2)')); + entityItem.triggerEventHandler('click', null); + + scheduler.schedule(() => fixture.detectChanges()); + scheduler.flush(); + + expect(component.onSelect).toHaveBeenCalled(); + }); + + it('should emit selectionChange event when selecting a new entity', () => { + spyOn(component.selectionChange, 'emit').and.callThrough(); + component.ngOnInit(); + component.onSelect(listElementMock as any); + fixture.detectChanges(); + + expect(component.selectionChange.emit).toHaveBeenCalledWith(listElementMock as any); + }); + + it('should change loader status', () => { + spyOn(component.isLoadingList, 'next').and.callThrough(); + component.hideShowLoader(true); + + expect(component.isLoadingList.next).toHaveBeenCalledWith(true); + }); + + it('reset pagination fields', () => { + component.resetPagination(); + + expect(component.currentPage).toEqual(1); + expect(component.hasNextPage).toEqual(true); + expect(component.searchListEntity).toEqual([]); + }); + + it('should invoke the method getAllAuthorizedRelationshipTypeImport of EntityTypeService when isSubmission is false', () => { + component.isSubmission = false; + + scheduler.schedule(() => fixture.detectChanges()); + scheduler.flush(); + + expect((component as any).entityTypeService.getAllAuthorizedRelationshipTypeImport).toHaveBeenCalled(); + }); +}); diff --git a/src/app/shared/entity-dropdown/entity-dropdown.component.ts b/src/app/shared/entity-dropdown/entity-dropdown.component.ts new file mode 100644 index 0000000000..13d50a8b79 --- /dev/null +++ b/src/app/shared/entity-dropdown/entity-dropdown.component.ts @@ -0,0 +1,207 @@ +import { + ChangeDetectorRef, + Component, + ElementRef, + EventEmitter, + HostListener, + Input, + OnDestroy, + OnInit, + Output +} from '@angular/core'; +import { BehaviorSubject, Observable, Subscription } from 'rxjs'; +import { hasValue } from '../empty.util'; +import { reduce, startWith, switchMap } from 'rxjs/operators'; +import { RemoteData } from '../../core/data/remote-data'; +import { FindListOptions } from '../../core/data/request.models'; +import { PaginatedList } from '../../core/data/paginated-list.model'; +import { EntityTypeService } from '../../core/data/entity-type.service'; +import { ItemType } from '../../core/shared/item-relationships/item-type.model'; +import { getFirstSucceededRemoteWithNotEmptyData } from '../../core/shared/operators'; + +@Component({ + selector: 'ds-entity-dropdown', + templateUrl: './entity-dropdown.component.html', + styleUrls: ['./entity-dropdown.component.scss'] +}) +export class EntityDropdownComponent implements OnInit, OnDestroy { + /** + * The entity list obtained from a search + * @type {Observable} + */ + public searchListEntity$: Observable; + + /** + * A boolean representing if dropdown list is scrollable to the bottom + * @type {boolean} + */ + private scrollableBottom = false; + + /** + * A boolean representing if dropdown list is scrollable to the top + * @type {boolean} + */ + private scrollableTop = false; + + /** + * The list of entity to render + */ + public searchListEntity: ItemType[] = []; + + /** + * TRUE if the parent operation is a 'new submission' operation, FALSE otherwise (eg.: is an 'Import metadata from an external source' operation). + */ + @Input() isSubmission: boolean; + + /** + * The entity to output to the parent component + */ + @Output() selectionChange = new EventEmitter(); + + /** + * A boolean representing if the loader is visible or not + */ + public isLoadingList: BehaviorSubject = new BehaviorSubject(false); + + /** + * A numeric representig current page + */ + public currentPage: number; + + /** + * A boolean representing if exist another page to render + */ + public hasNextPage: boolean; + + /** + * Array to track all subscriptions and unsubscribe them onDestroy + * @type {Array} + */ + public subs: Subscription[] = []; + + /** + * Initialize instance variables + * + * @param {ChangeDetectorRef} changeDetectorRef + * @param {EntityTypeService} entityTypeService + * @param {ElementRef} el + */ + constructor( + private changeDetectorRef: ChangeDetectorRef, + private entityTypeService: EntityTypeService, + private el: ElementRef + ) { } + + /** + * Method called on mousewheel event, it prevent the page scroll + * when arriving at the top/bottom of dropdown menu + * + * @param event + * mousewheel event + */ + @HostListener('mousewheel', ['$event']) onMousewheel(event) { + if (event.wheelDelta > 0 && this.scrollableTop) { + event.preventDefault(); + } + if (event.wheelDelta < 0 && this.scrollableBottom) { + event.preventDefault(); + } + } + + /** + * Initialize entity list + */ + ngOnInit() { + this.resetPagination(); + this.populateEntityList(this.currentPage); + } + + /** + * Check if dropdown scrollbar is at the top or bottom of the dropdown list + * + * @param event + */ + public onScroll(event) { + this.scrollableBottom = (event.target.scrollTop + event.target.clientHeight === event.target.scrollHeight); + this.scrollableTop = (event.target.scrollTop === 0); + } + + /** + * Method used from infitity scroll for retrive more data on scroll down + */ + public onScrollDown() { + if ( this.hasNextPage ) { + this.populateEntityList(++this.currentPage); + } + } + + /** + * Emit a [selectionChange] event when a new entity is selected from list + * + * @param event + * the selected [ItemType] + */ + public onSelect(event: ItemType) { + this.selectionChange.emit(event); + } + + /** + * Method called for populate the entity list + * @param page page number + */ + public populateEntityList(page: number) { + this.isLoadingList.next(true); + // Set the pagination info + const findOptions: FindListOptions = { + elementsPerPage: 10, + currentPage: page + }; + let searchListEntity$; + if (this.isSubmission) { + searchListEntity$ = this.entityTypeService.getAllAuthorizedRelationshipType(findOptions); + } else { + searchListEntity$ = this.entityTypeService.getAllAuthorizedRelationshipTypeImport(findOptions); + } + this.searchListEntity$ = searchListEntity$.pipe( + getFirstSucceededRemoteWithNotEmptyData(), + switchMap((entityType: RemoteData>) => { + if ( (this.searchListEntity.length + findOptions.elementsPerPage) >= entityType.payload.totalElements ) { + this.hasNextPage = false; + } + return entityType.payload.page; + }), + reduce((acc: any, value: any) => [...acc, value], []), + startWith([]) + ); + this.subs.push( + this.searchListEntity$.subscribe( + (next) => { this.searchListEntity.push(...next); }, undefined, + () => { this.hideShowLoader(false); this.changeDetectorRef.detectChanges(); } + ) + ); + } + + /** + * Reset pagination values + */ + public resetPagination() { + this.currentPage = 1; + this.hasNextPage = true; + this.searchListEntity = []; + } + + /** + * Hide/Show the entity list loader + * @param hideShow true for show, false otherwise + */ + public hideShowLoader(hideShow: boolean) { + this.isLoadingList.next(hideShow); + } + + /** + * Unsubscribe from all subscriptions + */ + ngOnDestroy(): void { + this.subs.filter((sub) => hasValue(sub)).forEach((sub) => sub.unsubscribe()); + } +} diff --git a/src/app/shared/shared.module.ts b/src/app/shared/shared.module.ts index 9b993e551f..b3120006a5 100644 --- a/src/app/shared/shared.module.ts +++ b/src/app/shared/shared.module.ts @@ -53,7 +53,8 @@ import { FormComponent } from './form/form.component'; import { DsDynamicOneboxComponent } from './form/builder/ds-dynamic-form-ui/models/onebox/dynamic-onebox.component'; import { DsDynamicScrollableDropdownComponent } from './form/builder/ds-dynamic-form-ui/models/scrollable-dropdown/dynamic-scrollable-dropdown.component'; import { - DsDynamicFormControlContainerComponent, dsDynamicFormControlMapFn, + DsDynamicFormControlContainerComponent, + dsDynamicFormControlMapFn, } from './form/builder/ds-dynamic-form-ui/ds-dynamic-form-control-container.component'; import { DsDynamicFormComponent } from './form/builder/ds-dynamic-form-ui/ds-dynamic-form.component'; import { DragClickDirective } from './utils/drag-click.directive'; @@ -202,6 +203,7 @@ import { EpersonSearchBoxComponent } from './resource-policies/form/eperson-grou import { GroupSearchBoxComponent } from './resource-policies/form/eperson-group-list/group-search-box/group-search-box.component'; import { FileDownloadLinkComponent } from './file-download-link/file-download-link.component'; import { CollectionDropdownComponent } from './collection-dropdown/collection-dropdown.component'; +import { EntityDropdownComponent } from './entity-dropdown/entity-dropdown.component'; import { DsSelectComponent } from './ds-select/ds-select.component'; import { VocabularyTreeviewComponent } from './vocabulary-treeview/vocabulary-treeview.component'; import { CurationFormComponent } from '../curation-form/curation-form.component'; @@ -432,6 +434,7 @@ const COMPONENTS = [ FileDownloadLinkComponent, BitstreamDownloadPageComponent, CollectionDropdownComponent, + EntityDropdownComponent, ExportMetadataSelectorComponent, ConfirmationModalComponent, VocabularyTreeviewComponent, From afdbf3541bdc2711c91ec2b6bc13ae9032bd685b Mon Sep 17 00:00:00 2001 From: Giuseppe Digilio Date: Wed, 22 Sep 2021 17:43:47 +0200 Subject: [PATCH 06/21] [CST-4591] Add dropdown for selecting entity type when creating a new submission or importing from external source --- ...space-new-external-dropdown.component.html | 24 +++ ...space-new-external-dropdown.component.scss | 16 ++ ...ce-new-external-dropdown.component.spec.ts | 189 +++++++++++++++++ ...-dspace-new-external-dropdown.component.ts | 106 ++++++++++ ...ace-new-submission-dropdown.component.html | 20 ++ ...ace-new-submission-dropdown.component.scss | 16 ++ ...-new-submission-dropdown.component.spec.ts | 194 ++++++++++++++++++ ...space-new-submission-dropdown.component.ts | 109 ++++++++++ .../my-dspace-new-submission.component.html | 8 +- ...my-dspace-new-submission.component.spec.ts | 18 +- .../my-dspace-new-submission.component.ts | 9 - .../my-dspace-page/my-dspace-page.module.ts | 6 +- 12 files changed, 684 insertions(+), 31 deletions(-) create mode 100644 src/app/my-dspace-page/my-dspace-new-submission/my-dspace-new-external-dropdown/my-dspace-new-external-dropdown.component.html create mode 100644 src/app/my-dspace-page/my-dspace-new-submission/my-dspace-new-external-dropdown/my-dspace-new-external-dropdown.component.scss create mode 100644 src/app/my-dspace-page/my-dspace-new-submission/my-dspace-new-external-dropdown/my-dspace-new-external-dropdown.component.spec.ts create mode 100644 src/app/my-dspace-page/my-dspace-new-submission/my-dspace-new-external-dropdown/my-dspace-new-external-dropdown.component.ts create mode 100644 src/app/my-dspace-page/my-dspace-new-submission/my-dspace-new-submission-dropdown/my-dspace-new-submission-dropdown.component.html create mode 100644 src/app/my-dspace-page/my-dspace-new-submission/my-dspace-new-submission-dropdown/my-dspace-new-submission-dropdown.component.scss create mode 100644 src/app/my-dspace-page/my-dspace-new-submission/my-dspace-new-submission-dropdown/my-dspace-new-submission-dropdown.component.spec.ts create mode 100644 src/app/my-dspace-page/my-dspace-new-submission/my-dspace-new-submission-dropdown/my-dspace-new-submission-dropdown.component.ts diff --git a/src/app/my-dspace-page/my-dspace-new-submission/my-dspace-new-external-dropdown/my-dspace-new-external-dropdown.component.html b/src/app/my-dspace-page/my-dspace-new-submission/my-dspace-new-external-dropdown/my-dspace-new-external-dropdown.component.html new file mode 100644 index 0000000000..141e628ac0 --- /dev/null +++ b/src/app/my-dspace-page/my-dspace-new-submission/my-dspace-new-external-dropdown/my-dspace-new-external-dropdown.component.html @@ -0,0 +1,24 @@ +
+ +
+
+ + +
diff --git a/src/app/my-dspace-page/my-dspace-new-submission/my-dspace-new-external-dropdown/my-dspace-new-external-dropdown.component.scss b/src/app/my-dspace-page/my-dspace-new-submission/my-dspace-new-external-dropdown/my-dspace-new-external-dropdown.component.scss new file mode 100644 index 0000000000..a156132e3f --- /dev/null +++ b/src/app/my-dspace-page/my-dspace-new-submission/my-dspace-new-external-dropdown/my-dspace-new-external-dropdown.component.scss @@ -0,0 +1,16 @@ +.parent { + display: flex; +} + +.upload { + flex: auto; +} + +.add { + flex: initial; +} + +#entityControlsDropdownMenu { + min-width: 18rem; + box-shadow: $btn-focus-box-shadow; +} diff --git a/src/app/my-dspace-page/my-dspace-new-submission/my-dspace-new-external-dropdown/my-dspace-new-external-dropdown.component.spec.ts b/src/app/my-dspace-page/my-dspace-new-submission/my-dspace-new-external-dropdown/my-dspace-new-external-dropdown.component.spec.ts new file mode 100644 index 0000000000..aa223fc10e --- /dev/null +++ b/src/app/my-dspace-page/my-dspace-new-submission/my-dspace-new-external-dropdown/my-dspace-new-external-dropdown.component.spec.ts @@ -0,0 +1,189 @@ +import { Component, DebugElement, NO_ERRORS_SCHEMA } from '@angular/core'; +import { async, ComponentFixture, inject, TestBed } from '@angular/core/testing'; +import { Router } from '@angular/router'; +import { By } from '@angular/platform-browser'; +import { CommonModule } from '@angular/common'; +import { TranslateModule } from '@ngx-translate/core'; +import { of as observableOf } from 'rxjs'; +import { createPaginatedList, createTestComponent } from '../../../shared/testing/utils.test'; +import { MyDSpaceNewExternalDropdownComponent } from './my-dspace-new-external-dropdown.component'; +import { EntityTypeService } from '../../../core/data/entity-type.service'; +import { ItemType } from '../../../core/shared/item-relationships/item-type.model'; +import { ResourceType } from '../../../core/shared/resource-type'; +import { createSuccessfulRemoteDataObject$ } from '../../../shared/remote-data.utils'; +import { PageInfo } from '../../../core/shared/page-info.model'; +import { RouterStub } from '../../../shared/testing/router.stub'; + +export function getMockEntityTypeService(): EntityTypeService { + const pageInfo = { elementsPerPage: 20, totalElements: 4, totalPages: 1, currentPage: 0 } as PageInfo; + const type1: ItemType = { + id: '1', + label: 'Publication', + uuid: '1', + type: new ResourceType('entitytype'), + _links: undefined + }; + const type2: ItemType = { + id: '2', + label: 'Journal', + uuid: '2', + type: new ResourceType('entitytype'), + _links: undefined + }; + const type3: ItemType = { + id: '2', + label: 'DataPackage', + uuid: '2', + type: new ResourceType('entitytype'), + _links: undefined + }; + const rd$ = createSuccessfulRemoteDataObject$(createPaginatedList([type1, type2, type3])); + return jasmine.createSpyObj('entityTypeService', { + getAllAuthorizedRelationshipTypeImport: rd$, + hasMoreThanOneAuthorizedImport: observableOf(true) + }); +} + +export function getMockEmptyEntityTypeService(): EntityTypeService { + const pageInfo = { elementsPerPage: 20, totalElements: 1, totalPages: 1, currentPage: 0 } as PageInfo; + const type1: ItemType = { + id: '1', + label: 'Publication', + uuid: '1', + type: new ResourceType('entitytype'), + _links: undefined + }; + const rd$ = createSuccessfulRemoteDataObject$(createPaginatedList([type1])); + return jasmine.createSpyObj('entityTypeService', { + getAllAuthorizedRelationshipTypeImport: rd$, + hasMoreThanOneAuthorizedImport: observableOf(false) + }); +} + +describe('MyDSpaceNewExternalDropdownComponent test', () => { + let testComp: TestComponent; + let testFixture: ComponentFixture; + let submissionComponent: MyDSpaceNewExternalDropdownComponent; + let submissionComponentFixture: ComponentFixture; + + const entityType1: ItemType = { + id: '1', + label: 'Publication', + uuid: '1', + type: new ResourceType('entitytype'), + _links: undefined + }; + + describe('With only one Entity', () => { + beforeEach(async(() => { + TestBed.configureTestingModule({ + imports: [ + CommonModule, + TranslateModule.forRoot(), + ], + declarations: [ + MyDSpaceNewExternalDropdownComponent, + TestComponent + ], + providers: [ + { provide: EntityTypeService, useValue: getMockEmptyEntityTypeService() }, + { provide: Router, useValue: new RouterStub() }, + MyDSpaceNewExternalDropdownComponent + ], + schemas: [NO_ERRORS_SCHEMA] + }).compileComponents(); + + const html = ``; + + testFixture = createTestComponent(html, TestComponent) as ComponentFixture; + testComp = testFixture.componentInstance; + + submissionComponentFixture = TestBed.createComponent(MyDSpaceNewExternalDropdownComponent); + submissionComponent = submissionComponentFixture.componentInstance; + submissionComponentFixture.detectChanges(); + })); + + afterEach(() => { + testFixture.destroy(); + submissionComponentFixture.destroy(); + }); + + it('should create MyDSpaceNewExternalDropdownComponent', inject([MyDSpaceNewExternalDropdownComponent], (app: MyDSpaceNewExternalDropdownComponent) => { + expect(app).toBeDefined(); + })); + + it('should be a single button', inject([MyDSpaceNewExternalDropdownComponent], (app: MyDSpaceNewExternalDropdownComponent) => { + submissionComponentFixture.detectChanges(); + const addDivElement: DebugElement = submissionComponentFixture.debugElement.query(By.css('.add')); + const addDiv = addDivElement.nativeElement; + expect(addDiv.innerHTML).toBeDefined(); + const buttonElement: DebugElement = addDivElement.query(By.css('.btn')); + const button = buttonElement.nativeElement; + expect(button.innerHTML).toBeDefined(); + const dropdownElement: DebugElement = submissionComponentFixture.debugElement.query(By.css('.dropdown-menu')); + expect(dropdownElement).toBeNull(); + })); + }); + + describe('With more than one Entity', () => { + beforeEach(async(() => { + TestBed.configureTestingModule({ + imports: [ + CommonModule, + TranslateModule.forRoot(), + ], + declarations: [ + MyDSpaceNewExternalDropdownComponent, + TestComponent + ], + providers: [ + { provide: EntityTypeService, useValue: getMockEntityTypeService() }, + { provide: Router, useValue: new RouterStub() }, + MyDSpaceNewExternalDropdownComponent + ], + schemas: [NO_ERRORS_SCHEMA] + }).compileComponents(); + + const html = ``; + + testFixture = createTestComponent(html, TestComponent) as ComponentFixture; + testComp = testFixture.componentInstance; + + submissionComponentFixture = TestBed.createComponent(MyDSpaceNewExternalDropdownComponent); + submissionComponent = submissionComponentFixture.componentInstance; + submissionComponentFixture.detectChanges(); + })); + + afterEach(() => { + testFixture.destroy(); + submissionComponentFixture.destroy(); + }); + + it('should create MyDSpaceNewExternalDropdownComponent', inject([MyDSpaceNewExternalDropdownComponent], (app: MyDSpaceNewExternalDropdownComponent) => { + expect(app).toBeDefined(); + })); + + it('should be a dropdown button', inject([MyDSpaceNewExternalDropdownComponent], (app: MyDSpaceNewExternalDropdownComponent) => { + const dropdownElement: DebugElement = submissionComponentFixture.debugElement.query(By.css('.dropdown-menu')); + const dropdown = dropdownElement.nativeElement; + expect(dropdown.innerHTML).toBeDefined(); + })); + + it('should invoke modalService.open', () => { + submissionComponent.openPage(entityType1); + + expect((submissionComponent as any).router.navigate).toHaveBeenCalled(); + }); + }); +}); + +// declare a test component +@Component({ + selector: 'ds-test-cmp', + template: `` +}) +class TestComponent { + reload = (event) => { + return; + } +} diff --git a/src/app/my-dspace-page/my-dspace-new-submission/my-dspace-new-external-dropdown/my-dspace-new-external-dropdown.component.ts b/src/app/my-dspace-page/my-dspace-new-submission/my-dspace-new-external-dropdown/my-dspace-new-external-dropdown.component.ts new file mode 100644 index 0000000000..e806f162f4 --- /dev/null +++ b/src/app/my-dspace-page/my-dspace-new-submission/my-dspace-new-external-dropdown/my-dspace-new-external-dropdown.component.ts @@ -0,0 +1,106 @@ +import { Component, OnDestroy, OnInit } from '@angular/core'; +import { Router } from '@angular/router'; + +import { Observable, of as observableOf, Subscription } from 'rxjs'; +import { map, mergeMap, take } from 'rxjs/operators'; + +import { EntityTypeService } from '../../../core/data/entity-type.service'; +import { ItemType } from '../../../core/shared/item-relationships/item-type.model'; +import { FindListOptions } from '../../../core/data/request.models'; +import { hasValue } from '../../../shared/empty.util'; +import { RemoteData } from '../../../core/data/remote-data'; +import { PaginatedList } from '../../../core/data/paginated-list.model'; + +/** + * This component represents the 'Import metadata from external source' dropdown menu + */ +@Component({ + selector: 'ds-my-dspace-new-external-dropdown', + styleUrls: ['./my-dspace-new-external-dropdown.component.scss'], + templateUrl: './my-dspace-new-external-dropdown.component.html' +}) +export class MyDSpaceNewExternalDropdownComponent implements OnInit, OnDestroy { + + /** + * Used to verify if there are one or more entities available + */ + public moreThanOne$: Observable; + + /** + * The entity observble (only if there is only one entity available) + */ + public singleEntity$: Observable; + + /** + * The entity object (only if there is only one entity available) + */ + public singleEntity: ItemType; + + /** + * TRUE if the page is initialized + */ + public initialized$: Observable; + + /** + * Array to track all subscriptions and unsubscribe them onDestroy + * @type {Array} + */ + public subs: Subscription[] = []; + + /** + * Initialize instance variables + * + * @param {EntityTypeService} entityTypeService + * @param {Router} router + */ + constructor(private entityTypeService: EntityTypeService, + private router: Router) { } + + /** + * Initialize entity type list + */ + ngOnInit() { + this.initialized$ = observableOf(false); + this.moreThanOne$ = this.entityTypeService.hasMoreThanOneAuthorizedImport(); + this.singleEntity$ = this.moreThanOne$.pipe( + mergeMap((response: boolean) => { + if (!response) { + const findListOptions: FindListOptions = { + elementsPerPage: 1, + currentPage: 1 + }; + return this.entityTypeService.getAllAuthorizedRelationshipTypeImport(findListOptions).pipe( + map((entities: RemoteData>) => { + this.initialized$ = observableOf(true); + return entities.payload.page[0]; + }), + take(1) + ); + } else { + this.initialized$ = observableOf(true); + return observableOf(null); + } + }), + take(1) + ); + this.subs.push( + this.singleEntity$.subscribe((result) => this.singleEntity = result ) + ); + } + + /** + * Method called on clicking the button 'Import metadata from external source'. It opens the page of the external import. + */ + openPage(entity: ItemType) { + this.router.navigate(['/import-external'], { queryParams: { entity: entity.label } }); + } + + /** + * Unsubscribe from the subscription + */ + ngOnDestroy(): void { + this.subs + .filter((subscription) => hasValue(subscription)) + .forEach((subscription) => subscription.unsubscribe()); + } +} diff --git a/src/app/my-dspace-page/my-dspace-new-submission/my-dspace-new-submission-dropdown/my-dspace-new-submission-dropdown.component.html b/src/app/my-dspace-page/my-dspace-new-submission/my-dspace-new-submission-dropdown/my-dspace-new-submission-dropdown.component.html new file mode 100644 index 0000000000..ac40bbb005 --- /dev/null +++ b/src/app/my-dspace-page/my-dspace-new-submission/my-dspace-new-submission-dropdown/my-dspace-new-submission-dropdown.component.html @@ -0,0 +1,20 @@ +
+ +
+
+ + +
diff --git a/src/app/my-dspace-page/my-dspace-new-submission/my-dspace-new-submission-dropdown/my-dspace-new-submission-dropdown.component.scss b/src/app/my-dspace-page/my-dspace-new-submission/my-dspace-new-submission-dropdown/my-dspace-new-submission-dropdown.component.scss new file mode 100644 index 0000000000..a156132e3f --- /dev/null +++ b/src/app/my-dspace-page/my-dspace-new-submission/my-dspace-new-submission-dropdown/my-dspace-new-submission-dropdown.component.scss @@ -0,0 +1,16 @@ +.parent { + display: flex; +} + +.upload { + flex: auto; +} + +.add { + flex: initial; +} + +#entityControlsDropdownMenu { + min-width: 18rem; + box-shadow: $btn-focus-box-shadow; +} diff --git a/src/app/my-dspace-page/my-dspace-new-submission/my-dspace-new-submission-dropdown/my-dspace-new-submission-dropdown.component.spec.ts b/src/app/my-dspace-page/my-dspace-new-submission/my-dspace-new-submission-dropdown/my-dspace-new-submission-dropdown.component.spec.ts new file mode 100644 index 0000000000..2e7361c560 --- /dev/null +++ b/src/app/my-dspace-page/my-dspace-new-submission/my-dspace-new-submission-dropdown/my-dspace-new-submission-dropdown.component.spec.ts @@ -0,0 +1,194 @@ +import { Component, DebugElement, NO_ERRORS_SCHEMA } from '@angular/core'; +import { async, ComponentFixture, inject, TestBed } from '@angular/core/testing'; +import { By } from '@angular/platform-browser'; +import { CommonModule } from '@angular/common'; +import { TranslateModule } from '@ngx-translate/core'; +import { NgbModal } from '@ng-bootstrap/ng-bootstrap'; +import { of as observableOf } from 'rxjs'; +import { createPaginatedList, createTestComponent } from '../../../shared/testing/utils.test'; +import { MyDSpaceNewSubmissionDropdownComponent } from './my-dspace-new-submission-dropdown.component'; +import { EntityTypeService } from '../../../core/data/entity-type.service'; +import { ItemType } from '../../../core/shared/item-relationships/item-type.model'; +import { ResourceType } from '../../../core/shared/resource-type'; +import { createSuccessfulRemoteDataObject$ } from '../../../shared/remote-data.utils'; +import { PageInfo } from '../../../core/shared/page-info.model'; + +export function getMockEntityTypeService(): EntityTypeService { + const type1: ItemType = { + id: '1', + label: 'Publication', + uuid: '1', + type: new ResourceType('entitytype'), + _links: undefined + }; + const type2: ItemType = { + id: '2', + label: 'Journal', + uuid: '2', + type: new ResourceType('entitytype'), + _links: undefined + }; + const type3: ItemType = { + id: '2', + label: 'DataPackage', + uuid: '2', + type: new ResourceType('entitytype'), + _links: undefined + }; + const rd$ = createSuccessfulRemoteDataObject$(createPaginatedList([type1, type2, type3])); + return jasmine.createSpyObj('entityTypeService', { + getAllAuthorizedRelationshipType: rd$, + hasMoreThanOneAuthorized: observableOf(true) + }); +} + +export function getMockEmptyEntityTypeService(): EntityTypeService { + const pageInfo = { elementsPerPage: 20, totalElements: 1, totalPages: 1, currentPage: 0 } as PageInfo; + const type1: ItemType = { + id: '1', + label: 'Publication', + uuid: '1', + type: new ResourceType('entitytype'), + _links: undefined + }; + const rd$ = createSuccessfulRemoteDataObject$(createPaginatedList([type1])); + return jasmine.createSpyObj('entityTypeService', { + getAllAuthorizedRelationshipType: rd$, + hasMoreThanOneAuthorized: observableOf(false) + }); +} + +describe('MyDSpaceNewSubmissionDropdownComponent test', () => { + let testComp: TestComponent; + let testFixture: ComponentFixture; + let submissionComponent: MyDSpaceNewSubmissionDropdownComponent; + let submissionComponentFixture: ComponentFixture; + + const entityType1: ItemType = { + id: '1', + label: 'Publication', + uuid: '1', + type: new ResourceType('entitytype'), + _links: undefined + }; + + const modalStub = { + open: () => null, + close: () => null, + dismiss: () => null + }; + + describe('With only one Entity', () => { + beforeEach(async(() => { + TestBed.configureTestingModule({ + imports: [ + CommonModule, + TranslateModule.forRoot(), + ], + declarations: [ + MyDSpaceNewSubmissionDropdownComponent, + TestComponent + ], + providers: [ + { provide: EntityTypeService, useValue: getMockEmptyEntityTypeService() }, + { provide: NgbModal, useValue: modalStub }, + MyDSpaceNewSubmissionDropdownComponent + ], + schemas: [NO_ERRORS_SCHEMA] + }).compileComponents(); + + const html = ``; + + testFixture = createTestComponent(html, TestComponent) as ComponentFixture; + testComp = testFixture.componentInstance; + + submissionComponentFixture = TestBed.createComponent(MyDSpaceNewSubmissionDropdownComponent); + submissionComponent = submissionComponentFixture.componentInstance; + submissionComponentFixture.detectChanges(); + })); + + afterEach(() => { + testFixture.destroy(); + submissionComponentFixture.destroy(); + }); + + it('should create MyDSpaceNewSubmissionDropdownComponent', inject([MyDSpaceNewSubmissionDropdownComponent], (app: MyDSpaceNewSubmissionDropdownComponent) => { + expect(app).toBeDefined(); + })); + + it('should be a single button', inject([MyDSpaceNewSubmissionDropdownComponent], (app: MyDSpaceNewSubmissionDropdownComponent) => { + submissionComponentFixture.detectChanges(); + const addDivElement: DebugElement = submissionComponentFixture.debugElement.query(By.css('.add')); + const addDiv = addDivElement.nativeElement; + expect(addDiv.innerHTML).toBeDefined(); + const buttonElement: DebugElement = addDivElement.query(By.css('.btn')); + const button = buttonElement.nativeElement; + expect(button.innerHTML).toBeDefined(); + const dropdownElement: DebugElement = submissionComponentFixture.debugElement.query(By.css('.dropdown-menu')); + expect(dropdownElement).toBeNull(); + })); + }); + + describe('With more than one Entity', () => { + beforeEach(async(() => { + TestBed.configureTestingModule({ + imports: [ + CommonModule, + TranslateModule.forRoot(), + ], + declarations: [ + MyDSpaceNewSubmissionDropdownComponent, + TestComponent + ], + providers: [ + { provide: EntityTypeService, useValue: getMockEntityTypeService() }, + { provide: NgbModal, useValue: modalStub }, + MyDSpaceNewSubmissionDropdownComponent + ], + schemas: [NO_ERRORS_SCHEMA] + }).compileComponents(); + + const html = ``; + + testFixture = createTestComponent(html, TestComponent) as ComponentFixture; + testComp = testFixture.componentInstance; + + submissionComponentFixture = TestBed.createComponent(MyDSpaceNewSubmissionDropdownComponent); + submissionComponent = submissionComponentFixture.componentInstance; + submissionComponentFixture.detectChanges(); + })); + + afterEach(() => { + testFixture.destroy(); + submissionComponentFixture.destroy(); + }); + + it('should create MyDSpaceNewSubmissionDropdownComponent', inject([MyDSpaceNewSubmissionDropdownComponent], (app: MyDSpaceNewSubmissionDropdownComponent) => { + expect(app).toBeDefined(); + })); + + it('should be a dropdown button', inject([MyDSpaceNewSubmissionDropdownComponent], (app: MyDSpaceNewSubmissionDropdownComponent) => { + const dropdownElement: DebugElement = submissionComponentFixture.debugElement.query(By.css('.dropdown-menu')); + const dropdown = dropdownElement.nativeElement; + expect(dropdown.innerHTML).toBeDefined(); + })); + + it('should invoke modalService.open', () => { + spyOn((submissionComponent as any).modalService, 'open').and.returnValue({ componentInstance: { } }); + submissionComponent.openDialog(entityType1); + + expect((submissionComponent as any).modalService.open).toHaveBeenCalled(); + }); + }); +}); + +// declare a test component +@Component({ + selector: 'ds-test-cmp', + template: `` +}) +class TestComponent { + reload = (event) => { + return; + } +} diff --git a/src/app/my-dspace-page/my-dspace-new-submission/my-dspace-new-submission-dropdown/my-dspace-new-submission-dropdown.component.ts b/src/app/my-dspace-page/my-dspace-new-submission/my-dspace-new-submission-dropdown/my-dspace-new-submission-dropdown.component.ts new file mode 100644 index 0000000000..0ff363b164 --- /dev/null +++ b/src/app/my-dspace-page/my-dspace-new-submission/my-dspace-new-submission-dropdown/my-dspace-new-submission-dropdown.component.ts @@ -0,0 +1,109 @@ +import { Component, OnDestroy, OnInit } from '@angular/core'; + +import { Observable, of as observableOf, Subscription } from 'rxjs'; +import { map, mergeMap, take } from 'rxjs/operators'; +import { NgbModal } from '@ng-bootstrap/ng-bootstrap'; + +import { EntityTypeService } from '../../../core/data/entity-type.service'; +import { ItemType } from '../../../core/shared/item-relationships/item-type.model'; +import { FindListOptions } from '../../../core/data/request.models'; +import { hasValue } from '../../../shared/empty.util'; +import { CreateItemParentSelectorComponent } from '../../../shared/dso-selector/modal-wrappers/create-item-parent-selector/create-item-parent-selector.component'; +import { RemoteData } from '../../../core/data/remote-data'; +import { PaginatedList } from '../../../core/data/paginated-list.model'; + +/** + * This component represents the new submission dropdown + */ +@Component({ + selector: 'ds-my-dspace-new-submission-dropdown', + styleUrls: ['./my-dspace-new-submission-dropdown.component.scss'], + templateUrl: './my-dspace-new-submission-dropdown.component.html' +}) +export class MyDSpaceNewSubmissionDropdownComponent implements OnInit, OnDestroy { + + /** + * Used to verify if there are one or more entities available + */ + public moreThanOne$: Observable; + + /** + * The entity observble (only if there is only one entity available) + */ + public singleEntity$: Observable; + + /** + * The entity object (only if there is only one entity available) + */ + public singleEntity: ItemType; + + /** + * TRUE if the page is initialized + */ + public initialized$: Observable; + + /** + * Array to track all subscriptions and unsubscribe them onDestroy + * @type {Array} + */ + public subs: Subscription[] = []; + + /** + * Initialize instance variables + * + * @param {EntityTypeService} entityTypeService + * @param {NgbModal} modalService + */ + constructor(private entityTypeService: EntityTypeService, + private modalService: NgbModal) { } + + /** + * Initialize entity type list + */ + ngOnInit() { + this.initialized$ = observableOf(false); + this.moreThanOne$ = this.entityTypeService.hasMoreThanOneAuthorized(); + this.singleEntity$ = this.moreThanOne$.pipe( + mergeMap((response: boolean) => { + if (!response) { + const findListOptions: FindListOptions = { + elementsPerPage: 1, + currentPage: 1 + }; + return this.entityTypeService.getAllAuthorizedRelationshipType(findListOptions).pipe( + map((entities: RemoteData>) => { + this.initialized$ = observableOf(true); + return entities.payload.page[0]; + }), + take(1) + ); + } else { + this.initialized$ = observableOf(true); + return observableOf(null); + } + }), + take(1) + ); + this.subs.push( + this.singleEntity$.subscribe((result) => this.singleEntity = result ) + ); + } + + /** + * Method called on clicking the button "New Submition", It opens a dialog for + * select a collection. + */ + openDialog(entity: ItemType) { + const modalRef = this.modalService.open(CreateItemParentSelectorComponent); + modalRef.componentInstance.entityType = entity.label; + } + + /** + * Unsubscribe from the subscription + */ + ngOnDestroy(): void { + this.subs + .filter((subscription) => hasValue(subscription)) + .forEach((subscription) => subscription.unsubscribe()); + } +} diff --git a/src/app/my-dspace-page/my-dspace-new-submission/my-dspace-new-submission.component.html b/src/app/my-dspace-page/my-dspace-new-submission/my-dspace-new-submission.component.html index 028b7df5a5..d0052b9355 100644 --- a/src/app/my-dspace-page/my-dspace-new-submission/my-dspace-new-submission.component.html +++ b/src/app/my-dspace-page/my-dspace-new-submission/my-dspace-new-submission.component.html @@ -8,14 +8,10 @@
- +
- - - +
diff --git a/src/app/my-dspace-page/my-dspace-new-submission/my-dspace-new-submission.component.spec.ts b/src/app/my-dspace-page/my-dspace-new-submission/my-dspace-new-submission.component.spec.ts index 7c6d8918cb..fb8ecbf65c 100644 --- a/src/app/my-dspace-page/my-dspace-new-submission/my-dspace-new-submission.component.spec.ts +++ b/src/app/my-dspace-page/my-dspace-new-submission/my-dspace-new-submission.component.spec.ts @@ -1,7 +1,6 @@ import { ChangeDetectorRef, Component, NO_ERRORS_SCHEMA } from '@angular/core'; import { ComponentFixture, inject, TestBed, waitForAsync } from '@angular/core/testing'; import { RouterTestingModule } from '@angular/router/testing'; -import { By } from '@angular/platform-browser'; import { TranslateLoader, TranslateModule } from '@ngx-translate/core'; import { ScrollToService } from '@nicky-lenaers/ngx-scroll-to'; @@ -25,6 +24,8 @@ import { HttpXsrfTokenExtractor } from '@angular/common/http'; import { CookieService } from '../../core/services/cookie.service'; import { CookieServiceMock } from '../../shared/mocks/cookie.service.mock'; import { HttpXsrfTokenExtractorMock } from '../../shared/mocks/http-xsrf-token-extractor.mock'; +import { getMockEntityTypeService } from './my-dspace-new-submission-dropdown/my-dspace-new-submission-dropdown.component.spec'; +import { EntityTypeService } from '../../core/data/entity-type.service'; describe('MyDSpaceNewSubmissionComponent test', () => { @@ -62,6 +63,7 @@ describe('MyDSpaceNewSubmissionComponent test', () => { { provide: HttpXsrfTokenExtractor, useValue: new HttpXsrfTokenExtractorMock('mock-token') }, { provide: CookieService, useValue: new CookieServiceMock() }, { provide: HostWindowService, useValue: new HostWindowServiceStub(800) }, + { provide: EntityTypeService, useValue: getMockEntityTypeService() }, ], schemas: [NO_ERRORS_SCHEMA] }).compileComponents(); @@ -104,20 +106,6 @@ describe('MyDSpaceNewSubmissionComponent test', () => { comp.uploaderComponent.uploader = uploader; }); - it('should call app.openDialog', (done) => { - spyOn(comp, 'openDialog'); - const submissionButton = fixture.debugElement.query(By.css('button.btn-primary')); - submissionButton.triggerEventHandler('click', null); - - fixture.detectChanges(); - - fixture.whenStable().then(() => { - expect(comp.openDialog).toHaveBeenCalled(); - done(); - }); - - }); - it('should show a collection selector if only one file are uploaded', (done) => { spyOn((comp as any).modalService, 'open').and.returnValue({ result: new Promise((res, rej) => {/****/}) }); comp.afterFileLoaded(['']); diff --git a/src/app/my-dspace-page/my-dspace-new-submission/my-dspace-new-submission.component.ts b/src/app/my-dspace-page/my-dspace-new-submission/my-dspace-new-submission.component.ts index c1e67561b2..580afd8ad4 100644 --- a/src/app/my-dspace-page/my-dspace-new-submission/my-dspace-new-submission.component.ts +++ b/src/app/my-dspace-page/my-dspace-new-submission/my-dspace-new-submission.component.ts @@ -14,7 +14,6 @@ import { HALEndpointService } from '../../core/shared/hal-endpoint.service'; import { NotificationType } from '../../shared/notifications/models/notification-type'; import { hasValue } from '../../shared/empty.util'; import { SearchResult } from '../../shared/search/search-result.model'; -import { CreateItemParentSelectorComponent } from '../../shared/dso-selector/modal-wrappers/create-item-parent-selector/create-item-parent-selector.component'; import { CollectionSelectorComponent } from '../collection-selector/collection-selector.component'; import { UploaderComponent } from '../../shared/uploader/uploader.component'; import { UploaderError } from '../../shared/uploader/uploader-error.model'; @@ -118,14 +117,6 @@ export class MyDSpaceNewSubmissionComponent implements OnDestroy, OnInit { this.notificationsService.error(null, this.translate.get(errorMessageKey)); } - /** - * Method called on clicking the button "New Submition", It opens a dialog for - * select a collection. - */ - openDialog() { - this.modalService.open(CreateItemParentSelectorComponent); - } - /** * Method invoked after all file are loaded from upload plugin */ diff --git a/src/app/my-dspace-page/my-dspace-page.module.ts b/src/app/my-dspace-page/my-dspace-page.module.ts index 52c80c90b0..a5a18effbc 100644 --- a/src/app/my-dspace-page/my-dspace-page.module.ts +++ b/src/app/my-dspace-page/my-dspace-page.module.ts @@ -11,6 +11,8 @@ import { MyDSpaceGuard } from './my-dspace.guard'; import { MyDSpaceConfigurationService } from './my-dspace-configuration.service'; import { CollectionSelectorComponent } from './collection-selector/collection-selector.component'; import { MyDspaceSearchModule } from './my-dspace-search.module'; +import { MyDSpaceNewSubmissionDropdownComponent } from './my-dspace-new-submission/my-dspace-new-submission-dropdown/my-dspace-new-submission-dropdown.component'; +import { MyDSpaceNewExternalDropdownComponent } from './my-dspace-new-submission/my-dspace-new-external-dropdown/my-dspace-new-external-dropdown.component'; import { ThemedMyDSpacePageComponent } from './themed-my-dspace-page.component'; const DECLARATIONS = [ @@ -18,7 +20,9 @@ const DECLARATIONS = [ ThemedMyDSpacePageComponent, MyDSpaceResultsComponent, MyDSpaceNewSubmissionComponent, - CollectionSelectorComponent + CollectionSelectorComponent, + MyDSpaceNewSubmissionDropdownComponent, + MyDSpaceNewExternalDropdownComponent ]; @NgModule({ From 6bf3b8e7cdaa347144148ca6f695b80b39f95910 Mon Sep 17 00:00:00 2001 From: Giuseppe Digilio Date: Wed, 22 Sep 2021 18:02:52 +0200 Subject: [PATCH 07/21] [CST-4591] Change import from external source page in order to work with entity type --- .../mocks/external-source.service.mock.ts | 1 + ...-import-external-collection.component.html | 7 ++- ...port-external-collection.component.spec.ts | 47 ++++++++++++++++- ...on-import-external-collection.component.ts | 35 ++++++++++++- ...ion-import-external-preview.component.html | 2 +- ...ssion-import-external-preview.component.ts | 7 ++- ...mport-external-searchbar.component.spec.ts | 22 +++++--- ...ion-import-external-searchbar.component.ts | 33 +++++++----- .../submission-import-external.component.html | 20 +++++--- ...bmission-import-external.component.spec.ts | 23 ++++++--- .../submission-import-external.component.ts | 50 +++++++++++-------- src/assets/i18n/en.json5 | 18 ++++++- 12 files changed, 204 insertions(+), 61 deletions(-) diff --git a/src/app/shared/mocks/external-source.service.mock.ts b/src/app/shared/mocks/external-source.service.mock.ts index fd6d7cdc46..89b2927939 100644 --- a/src/app/shared/mocks/external-source.service.mock.ts +++ b/src/app/shared/mocks/external-source.service.mock.ts @@ -53,6 +53,7 @@ export const externalSourceMyStaffDb: ExternalSource = { export function getMockExternalSourceService(): ExternalSourceService { return jasmine.createSpyObj('ExternalSourceService', { findAll: jasmine.createSpy('findAll'), + searchBy: jasmine.createSpy('searchBy'), getExternalSourceEntries: jasmine.createSpy('getExternalSourceEntries'), }); } diff --git a/src/app/submission/import-external/import-external-collection/submission-import-external-collection.component.html b/src/app/submission/import-external/import-external-collection/submission-import-external-collection.component.html index 73b41378c8..e1f03c40d5 100644 --- a/src/app/submission/import-external/import-external-collection/submission-import-external-collection.component.html +++ b/src/app/submission/import-external/import-external-collection/submission-import-external-collection.component.html @@ -5,7 +5,12 @@ diff --git a/src/app/submission/import-external/import-external-collection/submission-import-external-collection.component.spec.ts b/src/app/submission/import-external/import-external-collection/submission-import-external-collection.component.spec.ts index 1247eda0dc..3fb2d971b5 100644 --- a/src/app/submission/import-external/import-external-collection/submission-import-external-collection.component.spec.ts +++ b/src/app/submission/import-external/import-external-collection/submission-import-external-collection.component.spec.ts @@ -1,10 +1,11 @@ -import { Component, NO_ERRORS_SCHEMA, EventEmitter } from '@angular/core'; -import { waitForAsync, TestBed, ComponentFixture, inject } from '@angular/core/testing'; +import { Component, EventEmitter, NO_ERRORS_SCHEMA } from '@angular/core'; +import { ComponentFixture, fakeAsync, inject, TestBed, waitForAsync } from '@angular/core/testing'; import { TranslateModule } from '@ngx-translate/core'; import { createTestComponent } from '../../../shared/testing/utils.test'; import { SubmissionImportExternalCollectionComponent } from './submission-import-external-collection.component'; import { CollectionListEntry } from '../../../shared/collection-dropdown/collection-dropdown.component'; import { NgbActiveModal } from '@ng-bootstrap/ng-bootstrap'; +import { By } from '@angular/platform-browser'; describe('SubmissionImportExternalCollectionComponent test suite', () => { let comp: SubmissionImportExternalCollectionComponent; @@ -76,6 +77,48 @@ describe('SubmissionImportExternalCollectionComponent test suite', () => { expect(compAsAny.activeModal.dismiss).toHaveBeenCalled(); }); + + it('should be in loading state when hasChoice variable is different to true', () => { + comp.hasChoice = null; + expect(comp.isLoading()).toBeTrue(); + + comp.hasChoice = false; + expect(comp.isLoading()).toBeTrue(); + + comp.hasChoice = true; + expect(comp.isLoading()).toBeFalse(); + }); + + it('should set hasChoice variable on hasChoice event', () => { + comp.hasChoice = null; + + comp.onHasChoice(true); + expect(comp.hasChoice).toBe(true); + + comp.onHasChoice(false); + expect(comp.hasChoice).toBe(false); + }); + + it('should emit theOnlySelectable', () => { + spyOn(comp.selectedEvent, 'emit').and.callThrough(); + + const selected: any = {}; + comp.theOnlySelectable(selected); + + expect(comp.selectedEvent.emit).toHaveBeenCalledWith(selected); + }); + + it('dropdown should be invisible when the component is loading', fakeAsync(() => { + + spyOn(comp, 'isLoading').and.returnValue(true); + fixture.detectChanges(); + + fixture.whenStable().then(() => { + const dropdownMenu = fixture.debugElement.query(By.css('ds-collection-dropdown')).nativeElement; + expect(dropdownMenu.classList).toContain('d-none'); + }); + })); + }); }); diff --git a/src/app/submission/import-external/import-external-collection/submission-import-external-collection.component.ts b/src/app/submission/import-external/import-external-collection/submission-import-external-collection.component.ts index cbac0cb710..588bce89f9 100644 --- a/src/app/submission/import-external/import-external-collection/submission-import-external-collection.component.ts +++ b/src/app/submission/import-external/import-external-collection/submission-import-external-collection.component.ts @@ -1,4 +1,4 @@ -import { Component, Output, EventEmitter } from '@angular/core'; +import { Component, EventEmitter, Output } from '@angular/core'; import { CollectionListEntry } from '../../../shared/collection-dropdown/collection-dropdown.component'; import { NgbActiveModal } from '@ng-bootstrap/ng-bootstrap'; @@ -16,6 +16,16 @@ export class SubmissionImportExternalCollectionComponent { */ @Output() public selectedEvent = new EventEmitter(); + /** + * If present this value is used to filter collection list by entity type + */ + public entityType: string; + + /** + * If a collection choice is available + */ + public hasChoice: boolean = null; + /** * Initialize the component variables. * @param {NgbActiveModal} activeModal @@ -37,4 +47,27 @@ export class SubmissionImportExternalCollectionComponent { public closeCollectionModal(): void { this.activeModal.dismiss(false); } + + /** + * Propagate the onlySelectable collection + * @param theOnlySelectable + */ + public theOnlySelectable(theOnlySelectable: CollectionListEntry) { + this.selectedEvent.emit(theOnlySelectable); + } + + /** + * Set the hasChoice state + * @param hasChoice + */ + public onHasChoice(hasChoice: boolean) { + this.hasChoice = hasChoice; + } + + /** + * If the component is in loading state. + */ + public isLoading(): boolean { + return this.hasChoice !== true; + } } diff --git a/src/app/submission/import-external/import-external-preview/submission-import-external-preview.component.html b/src/app/submission/import-external/import-external-preview/submission-import-external-preview.component.html index 83c1ed82b6..bbb0dbcc94 100644 --- a/src/app/submission/import-external/import-external-preview/submission-import-external-preview.component.html +++ b/src/app/submission/import-external/import-external-preview/submission-import-external-preview.component.html @@ -1,5 +1,5 @@ diff --git a/src/app/shared/collection-dropdown/collection-dropdown.component.spec.ts b/src/app/shared/collection-dropdown/collection-dropdown.component.spec.ts index 612f5a1733..94cb7c857d 100644 --- a/src/app/shared/collection-dropdown/collection-dropdown.component.spec.ts +++ b/src/app/shared/collection-dropdown/collection-dropdown.component.spec.ts @@ -226,23 +226,11 @@ describe('CollectionDropdownComponent', () => { }); it('should emit hasChoice true when totalElements is greater then one', () => { - spyOn(component.hasChoice, 'emit').and.callThrough(); + spyOn(component.searchComplete, '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); + expect(component.searchComplete.emit).toHaveBeenCalledWith(); }); it('should emit theOnlySelectable when totalElements is equal to one', () => { diff --git a/src/app/shared/collection-dropdown/collection-dropdown.component.ts b/src/app/shared/collection-dropdown/collection-dropdown.component.ts index 6c11613512..9826c554bb 100644 --- a/src/app/shared/collection-dropdown/collection-dropdown.component.ts +++ b/src/app/shared/collection-dropdown/collection-dropdown.component.ts @@ -11,7 +11,7 @@ import { } from '@angular/core'; import { FormControl } from '@angular/forms'; -import { BehaviorSubject, Observable, Subscription } from 'rxjs'; +import { BehaviorSubject, from as observableFrom, Observable, of as observableOf, Subscription } from 'rxjs'; import { debounceTime, distinctUntilChanged, map, mergeMap, reduce, startWith, switchMap, take } from 'rxjs/operators'; import { hasValue } from '../empty.util'; @@ -22,10 +22,7 @@ import { Community } from '../../core/shared/community.model'; import { CollectionDataService } from '../../core/data/collection-data.service'; import { Collection } from '../../core/shared/collection.model'; import { followLink } from '../utils/follow-link-config.model'; -import { - getFirstSucceededRemoteDataPayload, - getFirstSucceededRemoteWithNotEmptyData -} from '../../core/shared/operators'; +import { getFirstCompletedRemoteData, getFirstSucceededRemoteDataPayload } from '../../core/shared/operators'; /** * An interface to represent a collection entry @@ -113,9 +110,9 @@ export class CollectionDropdownComponent implements OnInit, OnDestroy { @Input() entityType: string; /** - * Emit to notify whether collections to choice from are more than one + * Emit to notify whether search is complete */ - @Output() hasChoice = new EventEmitter(); + @Output() searchComplete = new EventEmitter(); /** * Emit to notify the only selectable collection. @@ -213,35 +210,44 @@ export class CollectionDropdownComponent implements OnInit, OnDestroy { query, this.entityType, findOptions, - false, + true, followLink('parentCommunity')); } else { searchListService$ = this.collectionDataService - .getAuthorizedCollection(query, findOptions, true, false, followLink('parentCommunity')); + .getAuthorizedCollection(query, findOptions, true, true, followLink('parentCommunity')); } this.searchListCollection$ = searchListService$.pipe( - getFirstSucceededRemoteWithNotEmptyData(), - switchMap((collections: RemoteData>) => { - if ( (this.searchListCollection.length + findOptions.elementsPerPage) >= collections.payload.totalElements ) { + getFirstCompletedRemoteData(), + switchMap((collectionsRD: RemoteData>) => { + this.searchComplete.emit(); + if (collectionsRD.hasSucceeded && collectionsRD.payload.totalElements > 0) { + if ( (this.searchListCollection.length + findOptions.elementsPerPage) >= collectionsRD.payload.totalElements ) { + this.hasNextPage = false; + this.emitSelectionEvents(collectionsRD); + return observableFrom(collectionsRD.payload.page).pipe( + mergeMap((collection: Collection) => collection.parentCommunity.pipe( + getFirstSucceededRemoteDataPayload(), + map((community: Community) => ({ + communities: [{ id: community.id, name: community.name }], + collection: { id: collection.id, uuid: collection.id, name: collection.name } + }) + ))), + reduce((acc: any, value: any) => [...acc, value], []), + ); + } + } else { this.hasNextPage = false; + return observableOf([]); } - this.emitSelectionEvents(collections); - return collections.payload.page; - }), - mergeMap((collection: Collection) => collection.parentCommunity.pipe( - getFirstSucceededRemoteDataPayload(), - map((community: Community) => ({ - communities: [{ id: community.id, name: community.name }], - collection: { id: collection.id, uuid: collection.id, name: collection.name } - }) - ))), - reduce((acc: any, value: any) => [...acc, value], []), - startWith([]) + }) ); - this.subs.push(this.searchListCollection$.subscribe( - (next) => { this.searchListCollection.push(...next); }, undefined, - () => { this.hideShowLoader(false); this.changeDetectorRef.detectChanges(); } - )); + this.subs.push( + this.searchListCollection$.subscribe((list: CollectionListEntry[]) => { + this.searchListCollection.push(...list); + this.hideShowLoader(false); + this.changeDetectorRef.detectChanges(); + }) + ); } /** @@ -284,7 +290,6 @@ export class CollectionDropdownComponent implements OnInit, OnDestroy { * @private */ private emitSelectionEvents(collections: RemoteData>) { - this.hasChoice.emit(collections.payload.totalElements > 1); if (collections.payload.totalElements === 1) { const collection = collections.payload.page[0]; collections.payload.page[0].parentCommunity.pipe( diff --git a/src/app/submission/import-external/import-external-collection/submission-import-external-collection.component.html b/src/app/submission/import-external/import-external-collection/submission-import-external-collection.component.html index e1f03c40d5..6fb6ab3382 100644 --- a/src/app/submission/import-external/import-external-collection/submission-import-external-collection.component.html +++ b/src/app/submission/import-external/import-external-collection/submission-import-external-collection.component.html @@ -1,5 +1,5 @@
-