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();
}
}
}