diff --git a/src/app/+my-dspace-page/collection-selector/collection-selector.component.html b/src/app/+my-dspace-page/collection-selector/collection-selector.component.html new file mode 100644 index 00000000000..83cc4151a36 --- /dev/null +++ b/src/app/+my-dspace-page/collection-selector/collection-selector.component.html @@ -0,0 +1,11 @@ +
+ + +
diff --git a/src/app/+my-dspace-page/collection-selector/collection-selector.component.scss b/src/app/+my-dspace-page/collection-selector/collection-selector.component.scss new file mode 100644 index 00000000000..e69de29bb2d diff --git a/src/app/+my-dspace-page/collection-selector/collection-selector.component.spec.ts b/src/app/+my-dspace-page/collection-selector/collection-selector.component.spec.ts new file mode 100644 index 00000000000..982d06aa75b --- /dev/null +++ b/src/app/+my-dspace-page/collection-selector/collection-selector.component.spec.ts @@ -0,0 +1,164 @@ +import { async, ComponentFixture, TestBed, fakeAsync, tick } from '@angular/core/testing'; + +import { CollectionSelectorComponent } from './collection-selector.component'; +import { CollectionDropdownComponent } from 'src/app/shared/collection-dropdown/collection-dropdown.component'; +import { Collection } from 'src/app/core/shared/collection.model'; +import { of, Observable } from 'rxjs'; +import { RemoteData } from 'src/app/core/data/remote-data'; +import { Community } from 'src/app/core/shared/community.model'; +import { FindListOptions } from 'src/app/core/data/request.models'; +import { FollowLinkConfig } from 'src/app/shared/utils/follow-link-config.model'; +import { PaginatedList } from 'src/app/core/data/paginated-list'; +import { createSuccessfulRemoteDataObject } from 'src/app/shared/remote-data.utils'; +import { PageInfo } from 'src/app/core/shared/page-info.model'; +import { TranslateModule, TranslateLoader } from '@ngx-translate/core'; +import { TranslateLoaderMock } from 'src/app/shared/mocks/translate-loader.mock'; +import { CollectionDataService } from 'src/app/core/data/collection-data.service'; +import { ChangeDetectorRef, ElementRef, NO_ERRORS_SCHEMA } from '@angular/core'; +import { NgbActiveModal } from '@ng-bootstrap/ng-bootstrap'; +import { ActivatedRoute } from '@angular/router'; +import { hot } from 'jasmine-marbles'; +import { By } from '@angular/platform-browser'; + +describe('CollectionSelectorComponent', () => { + let component: CollectionSelectorComponent; + let fixture: ComponentFixture; + const modal = jasmine.createSpyObj('modal', ['close', 'dismiss']); + + 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) + ) + }) + ]; + + // tslint:disable-next-line: max-classes-per-file + const collectionDataServiceMock = { + getAuthorizedCollection(query: string, options: FindListOptions = {}, ...linksToFollow: Array>): Observable>> { + return hot( 'a|', { + a: createSuccessfulRemoteDataObject( + new PaginatedList(new PageInfo(), collections) + ) + }); + } + }; + + beforeEach(async(() => { + TestBed.configureTestingModule({ + imports: [ + TranslateModule.forRoot({ + loader: { + provide: TranslateLoader, + useClass: TranslateLoaderMock + } + }) + ], + declarations: [ CollectionSelectorComponent, CollectionDropdownComponent ], + providers: [ + {provide: CollectionDataService, useValue: collectionDataServiceMock}, + {provide: ChangeDetectorRef, useValue: {}}, + {provide: ElementRef, userValue: {}}, + {provide: NgbActiveModal, useValue: modal}, + {provide: ActivatedRoute, useValue: {}} + ], + schemas: [NO_ERRORS_SCHEMA] + }) + .compileComponents(); + })); + + beforeEach(() => { + fixture = TestBed.createComponent(CollectionSelectorComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); + + it('should call selectObject', fakeAsync(() => { + spyOn(component, 'selectObject'); + fixture.detectChanges(); + tick(); + fixture.whenStable().then(() => { + const collectionItem = fixture.debugElement.query(By.css('.collection-item:nth-child(2)')); + collectionItem.triggerEventHandler('click', { + preventDefault: () => {/**/ + } + }); + expect(component.selectObject).toHaveBeenCalled(); + }); + })); + + it('should close the dialog', () => { + component.close(); + expect((component as any).activeModal.close).toHaveBeenCalled(); + }); +}); diff --git a/src/app/+my-dspace-page/collection-selector/collection-selector.component.ts b/src/app/+my-dspace-page/collection-selector/collection-selector.component.ts new file mode 100644 index 00000000000..f930fc3f540 --- /dev/null +++ b/src/app/+my-dspace-page/collection-selector/collection-selector.component.ts @@ -0,0 +1,33 @@ +import { Component } from '@angular/core'; +import { DSpaceObject } from '../../core/shared/dspace-object.model'; +import { NgbActiveModal } from '@ng-bootstrap/ng-bootstrap'; + +/** + * This component displays the dialog that shows the list of selectable collections + * on the MyDSpace page + */ +@Component({ + selector: 'ds-collection-selector', + templateUrl: './collection-selector.component.html', + styleUrls: ['./collection-selector.component.scss'] +}) +export class CollectionSelectorComponent { + + constructor(protected activeModal: NgbActiveModal) {} + + /** + * Method called when an element has been selected from collection list. + * Its close the active modal and send selected value to the component container + * @param dso The selected DSpaceObject + */ + selectObject(dso: DSpaceObject) { + this.activeModal.close(dso); + } + + /** + * Close the modal + */ + close() { + this.activeModal.close(); + } +} 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 4809f206aea..f12cb1ea4a6 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 @@ -3,7 +3,8 @@ + (onUploadError)="onUploadError($event)" + (onFileSelected)="afterFileLoaded($event)">
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 16b50d18f02..b0210b4979f 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,5 +1,5 @@ import { ChangeDetectorRef, Component, NO_ERRORS_SCHEMA } from '@angular/core'; -import { async, ComponentFixture, inject, TestBed, tick, fakeAsync } from '@angular/core/testing'; +import { async, ComponentFixture, inject, TestBed } from '@angular/core/testing'; import { RouterTestingModule } from '@angular/router/testing'; import { Store } from '@ngrx/store'; @@ -15,7 +15,6 @@ import { createTestComponent } from '../../shared/testing/utils.test'; import { MyDSpaceNewSubmissionComponent } from './my-dspace-new-submission.component'; import { AppState } from '../../app.reducer'; import { TranslateLoaderMock } from '../../shared/mocks/translate-loader.mock'; -import { getMockTranslateService } from '../../shared/mocks/translate.service.mock'; import { NotificationsService } from '../../shared/notifications/notifications.service'; import { NotificationsServiceStub } from '../../shared/testing/notifications-service.stub'; import { SharedModule } from '../../shared/shared.module'; @@ -23,10 +22,25 @@ import { getMockScrollToService } from '../../shared/mocks/scroll-to-service.moc import { UploaderService } from '../../shared/uploader/uploader.service'; import { By } from '@angular/platform-browser'; import { NgbModal } from '@ng-bootstrap/ng-bootstrap'; +import { UploaderComponent } from 'src/app/shared/uploader/uploader.component'; describe('MyDSpaceNewSubmissionComponent test', () => { - const translateService: any = getMockTranslateService(); + const translateService: TranslateService = jasmine.createSpyObj('translateService', { + get: (key: string): any => { observableOf(key) }, + instant: jasmine.createSpy('instant') + }); + + const uploader: any = jasmine.createSpyObj('uploader', { + clearQueue: jasmine.createSpy('clearQueue') + }); + + const modalService = { + open: () => { + return { result: new Promise((res, rej) => {/****/}) }; + } + }; + const store: Store = jasmine.createSpyObj('store', { /* tslint:disable:no-empty */ dispatch: {}, @@ -56,11 +70,7 @@ describe('MyDSpaceNewSubmissionComponent test', () => { { provide: ScrollToService, useValue: getMockScrollToService() }, { provide: Store, useValue: store }, { provide: TranslateService, useValue: translateService }, - { - provide: NgbModal, useValue: { - open: () => {/*comment*/} - } - }, + { provide: NgbModal, useValue: modalService }, ChangeDetectorRef, MyDSpaceNewSubmissionComponent, UploaderService @@ -100,6 +110,10 @@ describe('MyDSpaceNewSubmissionComponent test', () => { beforeEach(() => { fixture = TestBed.createComponent(MyDSpaceNewSubmissionComponent); comp = fixture.componentInstance; + comp.uploadFilesOptions.authToken = 'user-auth-token'; + comp.uploadFilesOptions.url = 'https://fake.upload-api.url'; + comp.uploaderComponent = TestBed.createComponent(UploaderComponent).componentInstance; + comp.uploaderComponent.uploader = uploader; }); it('should call app.openDialog', () => { @@ -111,6 +125,12 @@ describe('MyDSpaceNewSubmissionComponent test', () => { }); expect(comp.openDialog).toHaveBeenCalled(); }); + + it('should show a collection selector if only one file are uploaded', () => { + spyOn((comp as any).modalService, 'open').and.returnValue({ result: new Promise((res, rej) => {/****/}) }); + comp.afterFileLoaded(['']); + expect((comp as any).modalService.open).toHaveBeenCalled(); + }); }); }); 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 8d20a5736a3..f12280dafdf 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 @@ -1,11 +1,8 @@ -import { ChangeDetectorRef, Component, EventEmitter, OnDestroy, OnInit, Output } from '@angular/core'; +import { ChangeDetectorRef, Component, EventEmitter, OnDestroy, OnInit, Output, ViewChild } from '@angular/core'; import { Subscription } from 'rxjs'; import { first } from 'rxjs/operators'; -import { Store } from '@ngrx/store'; import { TranslateService } from '@ngx-translate/core'; - -import { SubmissionState } from '../../submission/submission.reducers'; import { AuthService } from '../../core/auth/auth.service'; import { DSpaceObject } from '../../core/shared/dspace-object.model'; import { NotificationsService } from '../../shared/notifications/notifications.service'; @@ -15,9 +12,11 @@ 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'; +import { CollectionSelectorComponent } from '../collection-selector/collection-selector.component'; +import { UploaderComponent } from 'src/app/shared/uploader/uploader.component'; +import { UploaderError } from 'src/app/shared/uploader/uploader-error.model'; /** * This component represents the whole mydspace page header @@ -43,6 +42,11 @@ export class MyDSpaceNewSubmissionComponent implements OnDestroy, OnInit { */ private sub: Subscription; + /** + * Reference to uploaderComponent + */ + @ViewChild(UploaderComponent, { static: false }) uploaderComponent: UploaderComponent; + /** * Initialize instance variables * @@ -57,9 +61,7 @@ export class MyDSpaceNewSubmissionComponent implements OnDestroy, OnInit { private changeDetectorRef: ChangeDetectorRef, private halService: HALEndpointService, private notificationsService: NotificationsService, - private store: Store, private translate: TranslateService, - private router: Router, private modalService: NgbModal) { } @@ -67,6 +69,7 @@ export class MyDSpaceNewSubmissionComponent implements OnDestroy, OnInit { * Initialize url and Bearer token */ ngOnInit() { + this.uploadFilesOptions.autoUpload = false; this.sub = this.halService.getEndpoint('workspaceitems').pipe(first()).subscribe((url) => { this.uploadFilesOptions.url = url; this.uploadFilesOptions.authToken = this.authService.buildAuthHeader(); @@ -106,8 +109,12 @@ export class MyDSpaceNewSubmissionComponent implements OnDestroy, OnInit { /** * Method called on file upload error */ - public onUploadError() { - this.notificationsService.error(null, this.translate.get('mydspace.upload.upload-failed')); + public onUploadError(error: UploaderError) { + let errorMessageKey = 'mydspace.upload.upload-failed'; + if (hasValue(error.status) && error.status === 422) { + errorMessageKey = 'mydspace.upload.upload-failed-manyentries'; + } + this.notificationsService.error(null, this.translate.get(errorMessageKey)); } /** @@ -118,6 +125,28 @@ export class MyDSpaceNewSubmissionComponent implements OnDestroy, OnInit { this.modalService.open(CreateItemParentSelectorComponent); } + /** + * Method invoked after all file are loaded from upload plugin + */ + afterFileLoaded(items) { + const uploader = this.uploaderComponent.uploader; + if (hasValue(items) && items.length > 1) { + this.notificationsService.error(null, this.translate.get('mydspace.upload.upload-failed-moreonefile')); + uploader.clearQueue(); + this.changeDetectorRef.detectChanges(); + } else { + const modalRef = this.modalService.open(CollectionSelectorComponent); + // When the dialog are closes its takes the collection selected and + // uploads choosed file after adds owningCollection parameter + modalRef.result.then( (result) => { + uploader.onBuildItemForm = (fileItem: any, form: any) => { + form.append('owningCollection', result.uuid); + }; + uploader.uploadAll(); + }); + } + } + /** * Unsubscribe from the subscription */ 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 1cf30c4ec95..49570fec6d7 100644 --- a/src/app/+my-dspace-page/my-dspace-page.module.ts +++ b/src/app/+my-dspace-page/my-dspace-page.module.ts @@ -20,6 +20,7 @@ import { SearchResultListElementComponent } from '../shared/object-list/search-r import { ItemSearchResultListElementSubmissionComponent } from '../shared/object-list/my-dspace-result-list-element/item-search-result/item-search-result-list-element-submission.component'; import { WorkflowItemSearchResultListElementComponent } from '../shared/object-list/my-dspace-result-list-element/workflow-item-search-result/workflow-item-search-result-list-element.component'; import { PoolSearchResultDetailElementComponent } from '../shared/object-detail/my-dspace-result-detail-element/pool-search-result/pool-search-result-detail-element.component'; +import { CollectionSelectorComponent } from './collection-selector/collection-selector.component'; @NgModule({ imports: [ @@ -40,7 +41,8 @@ import { PoolSearchResultDetailElementComponent } from '../shared/object-detail/ ClaimedTaskSearchResultDetailElementComponent, PoolSearchResultDetailElementComponent, MyDSpaceNewSubmissionComponent, - ItemSearchResultListElementSubmissionComponent + ItemSearchResultListElementSubmissionComponent, + CollectionSelectorComponent ], providers: [ MyDSpaceGuard, @@ -57,7 +59,8 @@ import { PoolSearchResultDetailElementComponent } from '../shared/object-detail/ WorkflowItemSearchResultDetailElementComponent, ClaimedTaskSearchResultDetailElementComponent, PoolSearchResultDetailElementComponent, - ItemSearchResultListElementSubmissionComponent + ItemSearchResultListElementSubmissionComponent, + CollectionSelectorComponent ] }) diff --git a/src/app/shared/uploader/uploader-error.model.ts b/src/app/shared/uploader/uploader-error.model.ts new file mode 100644 index 00000000000..9238a0df367 --- /dev/null +++ b/src/app/shared/uploader/uploader-error.model.ts @@ -0,0 +1,9 @@ +/** + * An interface that represents the upload error values + */ +export interface UploaderError { + item?: any; + response?: any; + status?: any; + headers?: any; +} diff --git a/src/app/shared/uploader/uploader-options.model.ts b/src/app/shared/uploader/uploader-options.model.ts index f195b0930e5..959e5c32959 100644 --- a/src/app/shared/uploader/uploader-options.model.ts +++ b/src/app/shared/uploader/uploader-options.model.ts @@ -17,6 +17,11 @@ export class UploaderOptions { */ autoUpload = true; + /** + * Set the max number of files that can be loaded + */ + maxFileNumber: number; + /** * The request method to use for the file upload request */ diff --git a/src/app/shared/uploader/uploader.component.ts b/src/app/shared/uploader/uploader.component.ts index 72a38d1eb15..07f4245954b 100644 --- a/src/app/shared/uploader/uploader.component.ts +++ b/src/app/shared/uploader/uploader.component.ts @@ -69,6 +69,11 @@ export class UploaderComponent { */ @Output() onUploadError: EventEmitter = new EventEmitter(); + /** + * The function to call when a file is selected + */ + @Output() onFileSelected: EventEmitter = new EventEmitter(); + public uploader: FileUploader; public uploaderId: string; public isOverBaseDropZone = observableOf(false); @@ -102,7 +107,8 @@ export class UploaderComponent { itemAlias: this.uploadFilesOptions.itemAlias, removeAfterUpload: true, autoUpload: this.uploadFilesOptions.autoUpload, - method: this.uploadFilesOptions.method + method: this.uploadFilesOptions.method, + queueLimit: this.uploadFilesOptions.maxFileNumber }); if (isUndefined(this.enableDragOverDocument)) { @@ -121,6 +127,9 @@ export class UploaderComponent { this.uploader.onAfterAddingFile = ((item) => { item.withCredentials = false; }); + this.uploader.onAfterAddingAll = ((items) => { + this.onFileSelected.emit(items); + }); if (isUndefined(this.onBeforeUpload)) { this.onBeforeUpload = () => {return}; } @@ -149,7 +158,7 @@ export class UploaderComponent { } }; this.uploader.onErrorItem = (item: any, response: any, status: any, headers: any) => { - this.onUploadError.emit(null); + this.onUploadError.emit({ item: item, response: response, status: status, headers: headers }); this.uploader.cancelAll(); }; this.uploader.onProgressAll = () => this.onProgress(); diff --git a/src/assets/i18n/en.json5 b/src/assets/i18n/en.json5 index bfa1c81aa69..b9c03eafc88 100644 --- a/src/assets/i18n/en.json5 +++ b/src/assets/i18n/en.json5 @@ -959,6 +959,8 @@ "dso-selector.create.item.head": "New item", + "dso-selector.create.submission.head": "New submission", + "dso-selector.edit.collection.head": "Edit collection", "dso-selector.edit.community.head": "Edit community", @@ -1955,6 +1957,10 @@ "mydspace.upload.upload-failed": "Error creating new workspace. Please verify the content uploaded before retry.", + "mydspace.upload.upload-failed-manyentries": "Unprocessable file. Detected too many entries but allowed only one for file.", + + "mydspace.upload.upload-failed-moreonefile": "Unprocessable request. Only one file is allowed.", + "mydspace.upload.upload-multiple-successful": "{{qty}} new workspace items created.", "mydspace.upload.upload-successful": "New workspace item created. Click {{here}} for edit it.",