From 9e050722902dfefd81423e261ba7ea5e255c917a Mon Sep 17 00:00:00 2001 From: Danilo Di Nuzzo Date: Tue, 16 Jun 2020 14:26:59 +0200 Subject: [PATCH] [CST-3090] done --- .../my-dspace-new-submission.component.html | 4 +- .../my-dspace-new-submission.component.ts | 15 +- src/app/core/data/collection-data.service.ts | 16 +- .../collection-dropdown.component.html | 43 +++ .../collection-dropdown.component.scss | 15 + .../collection-dropdown.component.spec.ts | 200 +++++++++++++ .../collection-dropdown.component.ts | 229 +++++++++++++++ ...create-item-parent-selector.component.html | 11 + .../create-item-parent-selector.component.ts | 3 +- src/app/shared/shared.module.ts | 7 +- .../submission-form-collection.component.html | 28 +- ...bmission-form-collection.component.spec.ts | 274 +----------------- .../submission-form-collection.component.ts | 104 +------ 13 files changed, 550 insertions(+), 399 deletions(-) create mode 100644 src/app/shared/collection-dropdown/collection-dropdown.component.html create mode 100644 src/app/shared/collection-dropdown/collection-dropdown.component.scss create mode 100644 src/app/shared/collection-dropdown/collection-dropdown.component.spec.ts create mode 100644 src/app/shared/collection-dropdown/collection-dropdown.component.ts create mode 100644 src/app/shared/dso-selector/modal-wrappers/create-item-parent-selector/create-item-parent-selector.component.html 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 911ba26b31..4809f206ae 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 @@ -7,9 +7,9 @@
- +
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 81d66bb5f7..8d20a5736a 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 @@ -15,6 +15,9 @@ 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 { Router } from '@angular/router'; +import { NgbModal } from '@ng-bootstrap/ng-bootstrap'; +import { CreateItemParentSelectorComponent } from 'src/app/shared/dso-selector/modal-wrappers/create-item-parent-selector/create-item-parent-selector.component'; /** * This component represents the whole mydspace page header @@ -55,7 +58,9 @@ export class MyDSpaceNewSubmissionComponent implements OnDestroy, OnInit { private halService: HALEndpointService, private notificationsService: NotificationsService, private store: Store, - private translate: TranslateService) { + private translate: TranslateService, + private router: Router, + private modalService: NgbModal) { } /** @@ -105,6 +110,14 @@ export class MyDSpaceNewSubmissionComponent implements OnDestroy, OnInit { this.notificationsService.error(null, this.translate.get('mydspace.upload.upload-failed')); } + /** + * Method called on clicking the button "New Submition", It opens a dialog for + * select a collection. + */ + openDialog() { + this.modalService.open(CreateItemParentSelectorComponent); + } + /** * Unsubscribe from the subscription */ diff --git a/src/app/core/data/collection-data.service.ts b/src/app/core/data/collection-data.service.ts index 0639a7d8ca..d28421356a 100644 --- a/src/app/core/data/collection-data.service.ts +++ b/src/app/core/data/collection-data.service.ts @@ -72,14 +72,18 @@ export class CollectionDataService extends ComColDataService { /** * 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 options The [[FindListOptions]] object * @return Observable>> * collection list */ - getAuthorizedCollection(options: FindListOptions = {}): Observable>> { + getAuthorizedCollection(query: string, options: FindListOptions = {}, ...linksToFollow: Array>): Observable>> { const searchHref = 'findAuthorized'; + options = Object.assign({}, options, { + searchParams: [new RequestParam('query', query)] + }); - return this.searchBy(searchHref, options).pipe( + return this.searchBy(searchHref, options, ...linksToFollow).pipe( filter((collections: RemoteData>) => !collections.isResponsePending)); } @@ -87,14 +91,18 @@ export class CollectionDataService extends ComColDataService { * 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 * @return Observable>> * collection list */ - getAuthorizedCollectionByCommunity(communityId: string, options: FindListOptions = {}): Observable>> { + getAuthorizedCollectionByCommunity(communityId: string, query: string, options: FindListOptions = {}): Observable>> { const searchHref = 'findAuthorizedByCommunity'; options = Object.assign({}, options, { - searchParams: [new RequestParam('uuid', communityId)] + searchParams: [ + new RequestParam('uuid', communityId), + new RequestParam('query', query) + ] }); return this.searchBy(searchHref, options).pipe( diff --git a/src/app/shared/collection-dropdown/collection-dropdown.component.html b/src/app/shared/collection-dropdown/collection-dropdown.component.html new file mode 100644 index 0000000000..0674084a43 --- /dev/null +++ b/src/app/shared/collection-dropdown/collection-dropdown.component.html @@ -0,0 +1,43 @@ +
+ +
+ +
+
+ + + +
+
\ No newline at end of file diff --git a/src/app/shared/collection-dropdown/collection-dropdown.component.scss b/src/app/shared/collection-dropdown/collection-dropdown.component.scss new file mode 100644 index 0000000000..deecc39510 --- /dev/null +++ b/src/app/shared/collection-dropdown/collection-dropdown.component.scss @@ -0,0 +1,15 @@ +.scrollable-menu { + height: auto; + max-height: $dropdown-menu-max-height; + overflow-x: hidden; +} + +.collection-item { + border-bottom: $dropdown-border-width solid $dropdown-border-color; +} + +#collectionControlsDropdownMenu { + outline: 0; + left: 0 !important; + box-shadow: $btn-focus-box-shadow; +} diff --git a/src/app/shared/collection-dropdown/collection-dropdown.component.spec.ts b/src/app/shared/collection-dropdown/collection-dropdown.component.spec.ts new file mode 100644 index 0000000000..33c848f9c4 --- /dev/null +++ b/src/app/shared/collection-dropdown/collection-dropdown.component.spec.ts @@ -0,0 +1,200 @@ +import { async, ComponentFixture, TestBed, fakeAsync, tick } from '@angular/core/testing'; + +import { CollectionDropdownComponent } from './collection-dropdown.component'; +import { FollowLinkConfig } from '../utils/follow-link-config.model'; +import { Observable, of } from 'rxjs'; +import { RemoteData } from 'src/app/core/data/remote-data'; +import { PaginatedList } from 'src/app/core/data/paginated-list'; +import { cold, getTestScheduler, hot } from 'jasmine-marbles'; +import { createSuccessfulRemoteDataObject } from '../remote-data.utils'; +import { PageInfo } from 'src/app/core/shared/page-info.model'; +import { Collection } from '../../core/shared/collection.model'; +import { NO_ERRORS_SCHEMA, ChangeDetectorRef, ElementRef } from '@angular/core'; +import { CollectionDataService } from 'src/app/core/data/collection-data.service'; +import { FindListOptions } from 'src/app/core/data/request.models'; +import { TranslateModule, TranslateLoader } from '@ngx-translate/core'; +import { TranslateLoaderMock } from '../mocks/translate-loader.mock'; +import { TestScheduler } from 'rxjs/testing'; +import { By } from '@angular/platform-browser'; +import { Community } from 'src/app/core/shared/community.model'; + +const community: Community = Object.assign(new Community(), { + id: 'ce64f48e-2c9b-411a-ac36-ee429c0e6a88', + uuid: 'ce64f48e-2c9b-411a-ac36-ee429c0e6a88', + name: 'Community 1' +}); + +const collections: Collection[] = [ + Object.assign(new Collection(), { + id: 'ce64f48e-2c9b-411a-ac36-ee429c0e6a88', + name: 'Collection 1', + metadata: [ + { + key: 'dc.title', + language: 'en_US', + value: 'Community 1-Collection 1' + }], + parentCommunity: of( + new RemoteData(false, false, true, undefined, community, 200) + ) + }), + Object.assign(new Collection(), { + id: '59ee713b-ee53-4220-8c3f-9860dc84fe33', + name: 'Collection 2', + metadata: [ + { + key: 'dc.title', + language: 'en_US', + value: 'Community 1-Collection 2' + }], + parentCommunity: of( + new RemoteData(false, false, true, undefined, community, 200) + ) + }), + Object.assign(new Collection(), { + id: 'e9dbf393-7127-415f-8919-55be34a6e9ed', + name: 'Collection 3', + metadata: [ + { + key: 'dc.title', + language: 'en_US', + value: 'Community 1-Collection 3' + }], + parentCommunity: of( + new RemoteData(false, false, true, undefined, community, 200) + ) + }), + Object.assign(new Collection(), { + id: '59da2ff0-9bf4-45bf-88be-e35abd33f304', + name: 'Collection 4', + metadata: [ + { + key: 'dc.title', + language: 'en_US', + value: 'Community 1-Collection 4' + }], + parentCommunity: of( + new RemoteData(false, false, true, undefined, community, 200) + ) + }), + Object.assign(new Collection(), { + id: 'a5159760-f362-4659-9e81-e3253ad91ede', + name: 'Collection 5', + metadata: [ + { + key: 'dc.title', + language: 'en_US', + value: 'Community 1-Collection 5' + }], + parentCommunity: of( + new RemoteData(false, false, true, undefined, community, 200) + ) + }) +]; + +const listElementMock = { + communities: [ + { + id: 'ce64f48e-2c9b-411a-ac36-ee429c0e6a88', + name: 'Community 1' + } + ], + collection: { + id: 'e9dbf393-7127-415f-8919-55be34a6e9ed', + uuid: 'e9dbf393-7127-415f-8919-55be34a6e9ed', + name: 'Collection 3' + } + }; + +// tslint:disable-next-line: max-classes-per-file +class CollectionDataServiceMock { + getAuthorizedCollection(query: string, options: FindListOptions = {}, ...linksToFollow: Array>): Observable>> { + return of( + createSuccessfulRemoteDataObject( + new PaginatedList(new PageInfo(), collections) + ) + ); + } +} + +describe('CollectionDropdownComponent', () => { + let component: CollectionDropdownComponent; + let fixture: ComponentFixture; + let scheduler: TestScheduler; + const searchedCollection = 'TEXT'; + + beforeEach(async(() => { + TestBed.configureTestingModule({ + imports: [ + TranslateModule.forRoot({ + loader: { + provide: TranslateLoader, + useClass: TranslateLoaderMock + } + }) + ], + declarations: [ CollectionDropdownComponent ], + providers: [ + {provide: CollectionDataService, useClass: CollectionDataServiceMock}, + {provide: ChangeDetectorRef, useValue: {}}, + {provide: ElementRef, userValue: {}} + ], + schemas: [NO_ERRORS_SCHEMA] + }) + .compileComponents(); + })); + + beforeEach(() => { + scheduler = getTestScheduler(); + fixture = TestBed.createComponent(CollectionDropdownComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should populate collections list with five items', () => { + const elements = fixture.debugElement.queryAll(By.css('.collection-item')); + expect(elements.length).toEqual(5); + }); + + it('should trigger onSelect method when select a new collection from list', fakeAsync(() => { + spyOn(component, 'onSelect'); + const collectionItem = fixture.debugElement.query(By.css('.collection-item:nth-child(2)')); + collectionItem.triggerEventHandler('click', null); + fixture.detectChanges(); + tick(); + fixture.whenStable().then(() => { + expect(component.onSelect).toHaveBeenCalled(); + }); + })); + + it('should emit collectionChange event when selecting a new collection', () => { + 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 reset collections list after reset of searchField', fakeAsync(() => { + spyOn(component, 'reset').and.callThrough(); + spyOn(component.searchField, 'setValue').and.callThrough(); + spyOn(component, 'resetPagination').and.callThrough(); + spyOn(component, 'populateCollectionList').and.callThrough(); + component.reset(); + const input = fixture.debugElement.query(By.css('input.form-control')); + const el = input.nativeElement; + el.value = searchedCollection; + el.dispatchEvent(new Event('input')); + fixture.detectChanges(); + tick(250); + + fixture.whenStable().then(() => { + expect(component.reset).toHaveBeenCalled(); + expect(component.searchField.setValue).toHaveBeenCalledWith(''); + expect(component.resetPagination).toHaveBeenCalled(); + expect(component.currentQuery).toEqual(''); + expect(component.populateCollectionList).toHaveBeenCalledWith(component.currentQuery, component.currentPage); + }); + })); +}); diff --git a/src/app/shared/collection-dropdown/collection-dropdown.component.ts b/src/app/shared/collection-dropdown/collection-dropdown.component.ts new file mode 100644 index 0000000000..e9e0445ca6 --- /dev/null +++ b/src/app/shared/collection-dropdown/collection-dropdown.component.ts @@ -0,0 +1,229 @@ +import { Component, OnInit, HostListener, ChangeDetectorRef, OnDestroy, Output, EventEmitter, ViewChild, ElementRef, AfterViewInit, AfterViewChecked } from '@angular/core'; +import { FormControl } from '@angular/forms'; +import { Observable, of, Subscription } from 'rxjs'; +import { hasValue, isNotEmpty } from '../empty.util'; +import { find, map, mergeMap, filter, reduce, startWith, debounceTime, distinctUntilChanged } from 'rxjs/operators'; +import { RemoteData } from 'src/app/core/data/remote-data'; +import { FindListOptions } from 'src/app/core/data/request.models'; +import { PaginatedList } from 'src/app/core/data/paginated-list'; +import { Community } from 'src/app/core/shared/community.model'; +import { CollectionDataService } from 'src/app/core/data/collection-data.service'; +import { Collection } from '../../core/shared/collection.model'; +import { followLink } from '../utils/follow-link-config.model'; + +/** + * An interface to represent a collection entry + */ +interface CollectionListEntryItem { + id: string; + uuid: string; + name: string; +} + +/** + * An interface to represent an entry in the collection list + */ +interface CollectionListEntry { + communities: CollectionListEntryItem[], + collection: CollectionListEntryItem +} + +@Component({ + selector: 'ds-collection-dropdown', + templateUrl: './collection-dropdown.component.html', + styleUrls: ['./collection-dropdown.component.scss'] +}) +export class CollectionDropdownComponent implements OnInit, OnDestroy { + + /** + * The search form control + * @type {FormControl} + */ + public searchField: FormControl = new FormControl(); + + /** + * The collection list obtained from a search + * @type {Observable} + */ + public searchListCollection$: 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; + + /** + * Array to track all subscriptions and unsubscribe them onDestroy + * @type {Array} + */ + private subs: Subscription[] = []; + + /** + * The list of collection to render + */ + searchListCollection: CollectionListEntry[] = []; + + @Output() selectionChange = new EventEmitter(); + /** + * A boolean representing if the loader is visible or not + */ + isLoadingList: boolean; + + /** + * A numeric representig current page + */ + currentPage: number; + + /** + * A boolean representing if exist another page to render + */ + hasNextPage: boolean; + + /** + * Current seach query used to filter collection list + */ + currentQuery: string; + + constructor( + private changeDetectorRef: ChangeDetectorRef, + private collectionDataService: CollectionDataService, + 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 collection list + */ + ngOnInit() { + this.subs.push(this.searchField.valueChanges.pipe( + debounceTime(200), + distinctUntilChanged(), + startWith('') + ).subscribe( + (next) => { + if (hasValue(next)) { + this.resetPagination(); + this.currentQuery = next; + this.populateCollectionList(this.currentQuery, this.currentPage); + } + } + )); + // Workaround for prevent the scroll of main page when this component is placed in a dialog + setTimeout(() => this.el.nativeElement.querySelector('input').focus(), 0); + } + + /** + * Check if dropdown scrollbar is at the top or bottom of the dropdown list + * + * @param event + */ + 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 + */ + onScrollDown() { + if ( this.hasNextPage ) { + this.populateCollectionList(this.currentQuery, ++this.currentPage); + } + } + + /** + * Emit a [selectionChange] event when a new collection is selected from list + * + * @param event + * the selected [CollectionListEntry] + */ + onSelect(event: CollectionListEntry) { + this.selectionChange.emit(event); + } + + /** + * Method called for populate the collection list + * @param query text for filter the collection list + * @param page page number + */ + populateCollectionList(query?: string, page?: number) { + this.isLoadingList = true; + // Set the pagination info + const findOptions: FindListOptions = { + elementsPerPage: 10, + currentPage: page + }; + this.searchListCollection$ = this.collectionDataService + .getAuthorizedCollection(query, findOptions, followLink('parentCommunity')) + .pipe( + find((collections: RemoteData>) => !collections.isResponsePending && collections.hasSucceeded), + mergeMap((collections: RemoteData>) => { + if ( (this.searchListCollection.length + findOptions.elementsPerPage) >= collections.payload.totalElements ) { + this.hasNextPage = false; + } + return collections.payload.page; + }), + filter((collectionData: Collection) => isNotEmpty(collectionData)), + mergeMap((collection: Collection) => collection.parentCommunity.pipe( + find((communityResponse: RemoteData) => !communityResponse.isResponsePending && communityResponse.hasSucceeded), + mergeMap((communityResponse: RemoteData) => of(communityResponse.payload)), + 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.isLoadingList = false; this.changeDetectorRef.detectChanges(); } + )); + } + + /** + * Unsubscribe from all subscriptions + */ + ngOnDestroy(): void { + this.subs.filter((sub) => hasValue(sub)).forEach((sub) => sub.unsubscribe()); + } + + /** + * Reset search form control + */ + reset() { + this.searchField.setValue(''); + } + + /** + * Reset pagination values + */ + resetPagination() { + this.currentPage = 1; + this.currentQuery = ''; + this.hasNextPage = true; + this.searchListCollection = []; + } +} 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 new file mode 100644 index 0000000000..ef8865ad87 --- /dev/null +++ b/src/app/shared/dso-selector/modal-wrappers/create-item-parent-selector/create-item-parent-selector.component.html @@ -0,0 +1,11 @@ +
+ + +
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 02a0bd79cd..45d15ae306 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 @@ -13,7 +13,8 @@ import { DSOSelectorModalWrapperComponent, SelectorActionType } from '../dso-sel @Component({ selector: 'ds-create-item-parent-selector', // styleUrls: ['./create-item-parent-selector.component.scss'], - templateUrl: '../dso-selector-modal-wrapper.component.html', + // templateUrl: '../dso-selector-modal-wrapper.component.html', + templateUrl: './create-item-parent-selector.component.html' }) export class CreateItemParentSelectorComponent extends DSOSelectorModalWrapperComponent implements OnInit { objectType = DSpaceObjectType.ITEM; diff --git a/src/app/shared/shared.module.ts b/src/app/shared/shared.module.ts index 67d7db5c5d..8ef3f91257 100644 --- a/src/app/shared/shared.module.ts +++ b/src/app/shared/shared.module.ts @@ -202,6 +202,7 @@ import { ResourcePolicyTargetResolver } from './resource-policies/resolvers/reso import { ResourcePolicyResolver } from './resource-policies/resolvers/resource-policy.resolver'; import { EpersonSearchBoxComponent } from './resource-policies/form/eperson-group-list/eperson-search-box/eperson-search-box.component'; import { GroupSearchBoxComponent } from './resource-policies/form/eperson-group-list/group-search-box/group-search-box.component'; +import { CollectionDropdownComponent } from './collection-dropdown/collection-dropdown.component'; const MODULES = [ // Do NOT include UniversalModule, HttpModule, or JsonpModule here @@ -386,7 +387,8 @@ const COMPONENTS = [ ResourcePolicyFormComponent, EpersonGroupListComponent, EpersonSearchBoxComponent, - GroupSearchBoxComponent + GroupSearchBoxComponent, + CollectionDropdownComponent ]; const ENTRY_COMPONENTS = [ @@ -504,8 +506,7 @@ const DIRECTIVES = [ ...COMPONENTS, ...DIRECTIVES, ...ENTRY_COMPONENTS, - ...SHARED_ITEM_PAGE_COMPONENTS, - + ...SHARED_ITEM_PAGE_COMPONENTS ], providers: [ ...PROVIDERS diff --git a/src/app/submission/form/collection/submission-form-collection.component.html b/src/app/submission/form/collection/submission-form-collection.component.html index 6f4a8a864c..ad53be200c 100644 --- a/src/app/submission/form/collection/submission-form-collection.component.html +++ b/src/app/submission/form/collection/submission-form-collection.component.html @@ -20,31 +20,9 @@ class="dropdown-menu" id="collectionControlsDropdownMenu" aria-labelledby="collectionControlsMenuButton"> -
- -
- -
- - -
+ + diff --git a/src/app/submission/form/collection/submission-form-collection.component.spec.ts b/src/app/submission/form/collection/submission-form-collection.component.spec.ts index 105d94b966..5baa1013ab 100644 --- a/src/app/submission/form/collection/submission-form-collection.component.spec.ts +++ b/src/app/submission/form/collection/submission-form-collection.component.spec.ts @@ -1,17 +1,14 @@ -import { ChangeDetectorRef, Component, CUSTOM_ELEMENTS_SCHEMA, DebugElement, SimpleChange } from '@angular/core'; +import { ChangeDetectorRef, Component, CUSTOM_ELEMENTS_SCHEMA, DebugElement } from '@angular/core'; import { async, ComponentFixture, fakeAsync, inject, TestBed, tick } from '@angular/core/testing'; import { By } from '@angular/platform-browser'; import { FormsModule, ReactiveFormsModule } from '@angular/forms'; -import { of as observableOf } from 'rxjs'; -import { filter } from 'rxjs/operators'; import { TranslateModule } from '@ngx-translate/core'; import { NgbModule } from '@ng-bootstrap/ng-bootstrap'; import { Store } from '@ngrx/store'; -import { cold } from 'jasmine-marbles'; import { SubmissionServiceStub } from '../../../shared/testing/submission-service.stub'; -import { mockSubmissionId, mockSubmissionRestResponse } from '../../../shared/mocks/submission.mock'; +import { mockSubmissionId } from '../../../shared/mocks/submission.mock'; import { SubmissionService } from '../../submission.service'; import { SubmissionFormCollectionComponent } from './submission-form-collection.component'; import { CommunityDataService } from '../../../core/data/community-data.service'; @@ -19,173 +16,9 @@ import { SubmissionJsonPatchOperationsService } from '../../../core/submission/s import { SubmissionJsonPatchOperationsServiceStub } from '../../../shared/testing/submission-json-patch-operations-service.stub'; import { JsonPatchOperationsBuilder } from '../../../core/json-patch/builder/json-patch-operations-builder'; import { JsonPatchOperationPathCombiner } from '../../../core/json-patch/builder/json-patch-operation-path-combiner'; -import { RemoteData } from '../../../core/data/remote-data'; -import { Community } from '../../../core/shared/community.model'; -import { PaginatedList } from '../../../core/data/paginated-list'; -import { PageInfo } from '../../../core/shared/page-info.model'; -import { Collection } from '../../../core/shared/collection.model'; import { createTestComponent } from '../../../shared/testing/utils.test'; import { CollectionDataService } from '../../../core/data/collection-data.service'; -const subcommunities = [Object.assign(new Community(), { - name: 'SubCommunity 1', - id: '123456789-1', - metadata: [ - { - key: 'dc.title', - language: 'en_US', - value: 'SubCommunity 1' - }] -}), - Object.assign(new Community(), { - name: 'SubCommunity 1', - id: '123456789s-1', - metadata: [ - { - key: 'dc.title', - language: 'en_US', - value: 'SubCommunity 1' - }] - }) -]; - -const mockCommunity1Collection1 = Object.assign(new Collection(), { - name: 'Community 1-Collection 1', - id: '1234567890-1', - metadata: [ - { - key: 'dc.title', - language: 'en_US', - value: 'Community 1-Collection 1' - }] -}); - -const mockCommunity1Collection2 = Object.assign(new Collection(), { - name: 'Community 1-Collection 2', - id: '1234567890-2', - metadata: [ - { - key: 'dc.title', - language: 'en_US', - value: 'Community 1-Collection 2' - }] -}); - -const mockCommunity2Collection1 = Object.assign(new Collection(), { - name: 'Community 2-Collection 1', - id: '1234567890-3', - metadata: [ - { - key: 'dc.title', - language: 'en_US', - value: 'Community 2-Collection 1' - }] -}); - -const mockCommunity2Collection2 = Object.assign(new Collection(), { - name: 'Community 2-Collection 2', - id: '1234567890-4', - metadata: [ - { - key: 'dc.title', - language: 'en_US', - value: 'Community 2-Collection 2' - }] -}); - -const mockCommunity = Object.assign(new Community(), { - name: 'Community 1', - id: '123456789-1', - metadata: [ - { - key: 'dc.title', - language: 'en_US', - value: 'Community 1' - }], - collections: observableOf(new RemoteData(true, true, true, - undefined, new PaginatedList(new PageInfo(), [mockCommunity1Collection1, mockCommunity1Collection2]))), - subcommunities: observableOf(new RemoteData(true, true, true, - undefined, new PaginatedList(new PageInfo(), subcommunities))), -}); - -const mockCommunity2 = Object.assign(new Community(), { - name: 'Community 2', - id: '123456789-2', - metadata: [ - { - key: 'dc.title', - language: 'en_US', - value: 'Community 2' - }], - collections: observableOf(new RemoteData(true, true, true, - undefined, new PaginatedList(new PageInfo(), [mockCommunity2Collection1, mockCommunity2Collection2]))), - subcommunities: observableOf(new RemoteData(true, true, true, - undefined, new PaginatedList(new PageInfo(), []))), -}); - -const mockCommunity1Collection1Rd = observableOf(new RemoteData(true, true, true, - undefined, mockCommunity1Collection1)); - -const mockCommunityList = observableOf(new RemoteData(true, true, true, - undefined, new PaginatedList(new PageInfo(), [mockCommunity, mockCommunity2]))); - -const mockCommunityCollectionList = observableOf(new RemoteData(true, true, true, - undefined, new PaginatedList(new PageInfo(), [mockCommunity1Collection1, mockCommunity1Collection2]))); - -const mockCommunity2CollectionList = observableOf(new RemoteData(true, true, true, - undefined, new PaginatedList(new PageInfo(), [mockCommunity2Collection1, mockCommunity2Collection2]))); - -const mockCollectionList = [ - { - communities: [ - { - id: '123456789-1', - name: 'Community 1' - } - ], - collection: { - id: '1234567890-1', - name: 'Community 1-Collection 1' - } - }, - { - communities: [ - { - id: '123456789-1', - name: 'Community 1' - } - ], - collection: { - id: '1234567890-2', - name: 'Community 1-Collection 2' - } - }, - { - communities: [ - { - id: '123456789-2', - name: 'Community 2' - } - ], - collection: { - id: '1234567890-3', - name: 'Community 2-Collection 1' - } - }, - { - communities: [ - { - id: '123456789-2', - name: 'Community 2' - } - ], - collection: { - id: '1234567890-4', - name: 'Community 2-Collection 2' - } - } -]; - describe('SubmissionFormCollectionComponent Component', () => { let comp: SubmissionFormCollectionComponent; @@ -197,8 +30,6 @@ describe('SubmissionFormCollectionComponent Component', () => { const submissionId = mockSubmissionId; const collectionId = '1234567890-1'; const definition = 'traditional'; - const submissionRestResponse = mockSubmissionRestResponse; - const searchedCollection = 'Community 2-Collection 2'; const communityDataService: any = jasmine.createSpyObj('communityDataService', { findAll: jasmine.createSpy('findAll') @@ -299,72 +130,11 @@ describe('SubmissionFormCollectionComponent Component', () => { expect(compAsAny.pathCombiner).toEqual(expected); }); - it('should init collection list properly', () => { - communityDataService.findAll.and.returnValue(mockCommunityList); - collectionDataService.findById.and.returnValue(mockCommunity1Collection1Rd); - collectionDataService.getAuthorizedCollectionByCommunity.and.returnValues(mockCommunityCollectionList, mockCommunity2CollectionList); - - comp.ngOnChanges({ - currentCollectionId: new SimpleChange(null, collectionId, true) - }); - - expect(comp.searchListCollection$).toBeObservable(cold('(ab)', { - a: [], - b: mockCollectionList - })); - - expect(comp.selectedCollectionName$).toBeObservable(cold('(a|)', { - a: 'Community 1-Collection 1' - })); - }); - - it('should show only the searched collection', () => { - comp.searchListCollection$ = observableOf(mockCollectionList); - fixture.detectChanges(); - - comp.searchField.setValue(searchedCollection); - fixture.detectChanges(); - - comp.searchListCollection$.pipe( - filter(() => !comp.disabled$.getValue()) - ).subscribe((list) => { - expect(list).toEqual([mockCollectionList[3]]); - }); - - }); - - it('should emit collectionChange event when selecting a new collection', () => { - spyOn(comp.searchField, 'reset').and.callThrough(); - spyOn(comp.collectionChange, 'emit').and.callThrough(); - jsonPatchOpServiceStub.jsonPatchByResourceID.and.returnValue(observableOf(submissionRestResponse)); - comp.ngOnInit(); - comp.onSelect(mockCollectionList[1]); - fixture.detectChanges(); - - expect(comp.searchField.reset).toHaveBeenCalled(); - expect(comp.collectionChange.emit).toHaveBeenCalledWith(submissionRestResponse[0] as any); - expect(submissionServiceStub.changeSubmissionCollection).toHaveBeenCalled(); - expect(comp.selectedCollectionId).toBe(mockCollectionList[1].collection.id); - expect(comp.selectedCollectionName$).toBeObservable(cold('(a|)', { - a: mockCollectionList[1].collection.name - })); - - }); - - it('should reset searchField when dropdown menu has been closed', () => { - spyOn(comp.searchField, 'reset').and.callThrough(); - comp.toggled(false); - - expect(comp.searchField.reset).toHaveBeenCalled(); - }); - describe('', () => { let dropdowBtn: DebugElement; let dropdownMenu: DebugElement; beforeEach(() => { - - comp.searchListCollection$ = observableOf(mockCollectionList); fixture.detectChanges(); dropdowBtn = fixture.debugElement.query(By.css('#collectionControlsMenuButton')); dropdownMenu = fixture.debugElement.query(By.css('#collectionControlsDropdownMenu')); @@ -387,46 +157,6 @@ describe('SubmissionFormCollectionComponent Component', () => { fixture.whenStable().then(() => { expect(comp.onClose).toHaveBeenCalled(); expect(dropdownMenu.nativeElement.classList).toContain('show'); - expect(dropdownMenu.queryAll(By.css('.collection-item')).length).toBe(4); - }); - })); - - it('should trigger onSelect method when select a new collection from dropdown menu', fakeAsync(() => { - - spyOn(comp, 'onSelect'); - dropdowBtn.triggerEventHandler('click', null); - tick(); - fixture.detectChanges(); - - const secondLink: DebugElement = dropdownMenu.query(By.css('.collection-item:nth-child(2)')); - secondLink.triggerEventHandler('click', null); - tick(); - fixture.detectChanges(); - - fixture.whenStable().then(() => { - - expect(comp.onSelect).toHaveBeenCalled(); - }); - })); - - it('should update searchField on input type', fakeAsync(() => { - - dropdowBtn.triggerEventHandler('click', null); - tick(); - fixture.detectChanges(); - - fixture.whenStable().then(() => { - const input = fixture.debugElement.query(By.css('input.form-control')); - const el = input.nativeElement; - - expect(el.value).toBe(''); - - el.value = searchedCollection; - el.dispatchEvent(new Event('input')); - - fixture.detectChanges(); - - expect(fixture.componentInstance.searchField.value).toEqual(searchedCollection); }); })); diff --git a/src/app/submission/form/collection/submission-form-collection.component.ts b/src/app/submission/form/collection/submission-form-collection.component.ts index f84764d6a4..691d93aed1 100644 --- a/src/app/submission/form/collection/submission-form-collection.component.ts +++ b/src/app/submission/form/collection/submission-form-collection.component.ts @@ -7,52 +7,27 @@ import { OnChanges, OnInit, Output, - SimpleChanges + SimpleChanges, + ViewChild } from '@angular/core'; -import { FormControl } from '@angular/forms'; -import { BehaviorSubject, combineLatest, Observable, of as observableOf, Subscription } from 'rxjs'; +import { BehaviorSubject, Observable, of as observableOf, Subscription } from 'rxjs'; import { - debounceTime, - distinctUntilChanged, - filter, find, - flatMap, - map, - mergeMap, - reduce, - startWith + map } from 'rxjs/operators'; import { Collection } from '../../../core/shared/collection.model'; import { CommunityDataService } from '../../../core/data/community-data.service'; -import { Community } from '../../../core/shared/community.model'; -import { hasValue, isEmpty, isNotEmpty } from '../../../shared/empty.util'; +import { hasValue, isNotEmpty } from '../../../shared/empty.util'; import { RemoteData } from '../../../core/data/remote-data'; import { JsonPatchOperationPathCombiner } from '../../../core/json-patch/builder/json-patch-operation-path-combiner'; import { JsonPatchOperationsBuilder } from '../../../core/json-patch/builder/json-patch-operations-builder'; -import { PaginatedList } from '../../../core/data/paginated-list'; import { SubmissionService } from '../../submission.service'; import { SubmissionObject } from '../../../core/submission/models/submission-object.model'; import { SubmissionJsonPatchOperationsService } from '../../../core/submission/submission-json-patch-operations.service'; import { CollectionDataService } from '../../../core/data/collection-data.service'; -import { FindListOptions } from '../../../core/data/request.models'; - -/** - * An interface to represent a collection entry - */ -interface CollectionListEntryItem { - id: string; - name: string; -} - -/** - * An interface to represent an entry in the collection list - */ -interface CollectionListEntry { - communities: CollectionListEntryItem[], - collection: CollectionListEntryItem -} +import { CollectionDropdownComponent } from 'src/app/shared/collection-dropdown/collection-dropdown.component'; /** * This component allows to show the current collection the submission belonging to and to change it. @@ -100,18 +75,6 @@ export class SubmissionFormCollectionComponent implements OnChanges, OnInit { */ public processingChange$ = new BehaviorSubject(false); - /** - * The search form control - * @type {FormControl} - */ - public searchField: FormControl = new FormControl(); - - /** - * The collection list obtained from a search - * @type {Observable} - */ - public searchListCollection$: Observable; - /** * The selected collection id * @type {string} @@ -148,6 +111,11 @@ export class SubmissionFormCollectionComponent implements OnChanges, OnInit { */ private subs: Subscription[] = []; + /** + * The html child that contains the collections list + */ + @ViewChild(CollectionDropdownComponent, {static: false}) collectionDropdown: CollectionDropdownComponent; + /** * Initialize instance variables * @@ -204,51 +172,6 @@ export class SubmissionFormCollectionComponent implements OnChanges, OnInit { find((collectionRD: RemoteData) => isNotEmpty(collectionRD.payload)), map((collectionRD: RemoteData) => collectionRD.payload.name) ); - - const findOptions: FindListOptions = { - elementsPerPage: 1000 - }; - - // Retrieve collection list only when is the first change - if (changes.currentCollectionId.isFirstChange()) { - // @TODO replace with search/top browse endpoint - // @TODO implement community/subcommunity hierarchy - const communities$ = this.communityDataService.findAll(findOptions).pipe( - find((communities: RemoteData>) => isNotEmpty(communities.payload)), - mergeMap((communities: RemoteData>) => communities.payload.page)); - - const listCollection$ = communities$.pipe( - flatMap((communityData: Community) => { - return this.collectionDataService.getAuthorizedCollectionByCommunity(communityData.uuid, findOptions).pipe( - find((collections: RemoteData>) => !collections.isResponsePending && collections.hasSucceeded), - mergeMap((collections: RemoteData>) => collections.payload.page), - filter((collectionData: Collection) => isNotEmpty(collectionData)), - map((collectionData: Collection) => ({ - communities: [{ id: communityData.id, name: communityData.name }], - collection: { id: collectionData.id, name: collectionData.name } - })) - ); - }), - reduce((acc: any, value: any) => [...acc, ...value], []), - startWith([]) - ); - - const searchTerm$ = this.searchField.valueChanges.pipe( - debounceTime(200), - distinctUntilChanged(), - startWith('') - ); - - this.searchListCollection$ = combineLatest(searchTerm$, listCollection$).pipe( - map(([searchTerm, listCollection]) => { - this.disabled$.next(isEmpty(listCollection)); - if (isEmpty(searchTerm)) { - return listCollection; - } else { - return listCollection.filter((v) => v.collection.name.toLowerCase().indexOf(searchTerm.toLowerCase()) > -1).slice(0, 5); - } - })); - } } } @@ -273,7 +196,6 @@ export class SubmissionFormCollectionComponent implements OnChanges, OnInit { * the selected [CollectionListEntryItem] */ onSelect(event) { - this.searchField.reset(); this.processingChange$.next(true); this.operationsBuilder.replace(this.pathCombiner.getPath(), event.collection.id, true); this.subs.push(this.operationsService.jsonPatchByResourceID( @@ -296,7 +218,7 @@ export class SubmissionFormCollectionComponent implements OnChanges, OnInit { * Reset search form control on dropdown menu close */ onClose() { - this.searchField.reset(); + this.collectionDropdown.reset(); } /** @@ -307,7 +229,7 @@ export class SubmissionFormCollectionComponent implements OnChanges, OnInit { */ toggled(isOpen: boolean) { if (!isOpen) { - this.searchField.reset(); + this.collectionDropdown.reset(); } } }