diff --git a/src/app/+import-external-page/import-external-page.component.html b/src/app/+import-external-page/import-external-page.component.html new file mode 100644 index 0000000000..5edccd55cb --- /dev/null +++ b/src/app/+import-external-page/import-external-page.component.html @@ -0,0 +1 @@ + diff --git a/src/app/+import-external-page/import-external-page.component.scss b/src/app/+import-external-page/import-external-page.component.scss new file mode 100644 index 0000000000..e69de29bb2 diff --git a/src/app/+import-external-page/import-external-page.component.spec.ts b/src/app/+import-external-page/import-external-page.component.spec.ts new file mode 100644 index 0000000000..5a2b7c5f8e --- /dev/null +++ b/src/app/+import-external-page/import-external-page.component.spec.ts @@ -0,0 +1,26 @@ +import { NO_ERRORS_SCHEMA } from '@angular/core'; +import { async, ComponentFixture, TestBed } from '@angular/core/testing'; +import { ImportExternalPageComponent } from './import-external-page.component'; + +describe('ImportExternalPageComponent', () => { + let component: ImportExternalPageComponent; + let fixture: ComponentFixture; + + beforeEach(async(() => { + TestBed.configureTestingModule({ + declarations: [ ImportExternalPageComponent ], + schemas: [NO_ERRORS_SCHEMA] + }) + .compileComponents(); + })); + + beforeEach(() => { + fixture = TestBed.createComponent(ImportExternalPageComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create ImportExternalPageComponent', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/src/app/+import-external-page/import-external-page.component.ts b/src/app/+import-external-page/import-external-page.component.ts new file mode 100644 index 0000000000..00709dad16 --- /dev/null +++ b/src/app/+import-external-page/import-external-page.component.ts @@ -0,0 +1,13 @@ +import { Component } from '@angular/core'; + +/** + * Component representing the external import page of the submission. + */ +@Component({ + selector: 'ds-import-external-page', + templateUrl: './import-external-page.component.html', + styleUrls: ['./import-external-page.component.scss'] +}) +export class ImportExternalPageComponent { + +} diff --git a/src/app/+import-external-page/import-external-page.module.ts b/src/app/+import-external-page/import-external-page.module.ts new file mode 100644 index 0000000000..017c723ad9 --- /dev/null +++ b/src/app/+import-external-page/import-external-page.module.ts @@ -0,0 +1,29 @@ +import { CommonModule } from '@angular/common'; +import { NgModule } from '@angular/core'; + +import { SharedModule } from '../shared/shared.module'; +import { CoreModule } from '../core/core.module'; +import { ImportExternalRoutingModule } from './import-external-routing.module'; +import { SubmissionModule } from '../submission/submission.module'; +import { ImportExternalPageComponent } from './import-external-page.component'; + +@NgModule({ + imports: [ + CommonModule, + SharedModule, + CoreModule.forRoot(), + ImportExternalRoutingModule, + SubmissionModule, + ], + declarations: [ + ImportExternalPageComponent + ], + entryComponents: [ ] +}) + +/** + * This module handles all components that are necessary for the submission external import page + */ +export class ImportExternalPageModule { + +} diff --git a/src/app/+import-external-page/import-external-routing.module.ts b/src/app/+import-external-page/import-external-routing.module.ts new file mode 100644 index 0000000000..91cdbf9877 --- /dev/null +++ b/src/app/+import-external-page/import-external-routing.module.ts @@ -0,0 +1,24 @@ +import { NgModule } from '@angular/core'; +import { RouterModule } from '@angular/router'; +import { AuthenticatedGuard } from '../core/auth/authenticated.guard'; +import { SubmissionImportExternalComponent } from '../submission/import-external/submission-import-external.component'; + +@NgModule({ + imports: [ + RouterModule.forChild([ + { + canActivate: [ AuthenticatedGuard ], + path: '', + component: SubmissionImportExternalComponent, + pathMatch: 'full', + data: { + title: 'submission.import-external.page.title' + } + } + ]) + ], + providers: [ ] +}) +export class ImportExternalRoutingModule { + +} 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 f12cb1ea4a..9ae38a2205 100644 --- a/src/app/+my-dspace-page/my-dspace-new-submission/my-dspace-new-submission.component.html +++ b/src/app/+my-dspace-page/my-dspace-new-submission/my-dspace-new-submission.component.html @@ -8,9 +8,14 @@ - - {{'mydspace.new-submission' | translate}} + + + + + + + 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 b0210b4979..c395408cf5 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 @@ -22,6 +22,8 @@ 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 { HostWindowService } from '../../shared/host-window.service'; +import { HostWindowServiceStub } from '../../shared/testing/host-window-service.stub'; import { UploaderComponent } from 'src/app/shared/uploader/uploader.component'; describe('MyDSpaceNewSubmissionComponent test', () => { @@ -73,7 +75,8 @@ describe('MyDSpaceNewSubmissionComponent test', () => { { provide: NgbModal, useValue: modalService }, ChangeDetectorRef, MyDSpaceNewSubmissionComponent, - UploaderService + UploaderService, + { provide: HostWindowService, useValue: new HostWindowServiceStub(800) }, ], schemas: [NO_ERRORS_SCHEMA] }).compileComponents(); 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 f12280dafd..d14fe46afd 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 @@ -3,6 +3,8 @@ import { ChangeDetectorRef, Component, EventEmitter, OnDestroy, OnInit, Output, import { Subscription } from 'rxjs'; import { first } from 'rxjs/operators'; import { TranslateService } from '@ngx-translate/core'; +import { NgbModal } from '@ng-bootstrap/ng-bootstrap'; + import { AuthService } from '../../core/auth/auth.service'; import { DSpaceObject } from '../../core/shared/dspace-object.model'; import { NotificationsService } from '../../shared/notifications/notifications.service'; @@ -12,11 +14,10 @@ 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 { 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 { CreateItemParentSelectorComponent } from '../../shared/dso-selector/modal-wrappers/create-item-parent-selector/create-item-parent-selector.component'; import { CollectionSelectorComponent } from '../collection-selector/collection-selector.component'; -import { UploaderComponent } from 'src/app/shared/uploader/uploader.component'; -import { UploaderError } from 'src/app/shared/uploader/uploader-error.model'; +import { UploaderComponent } from '../../shared/uploader/uploader.component'; +import { UploaderError } from '../../shared/uploader/uploader-error.model'; /** * This component represents the whole mydspace page header @@ -56,6 +57,8 @@ export class MyDSpaceNewSubmissionComponent implements OnDestroy, OnInit { * @param {NotificationsService} notificationsService * @param {Store} store * @param {TranslateService} translate + * @param {Router} router + * @param {NgbModal} modalService */ constructor(private authService: AuthService, private changeDetectorRef: ChangeDetectorRef, diff --git a/src/app/app-routing.module.ts b/src/app/app-routing.module.ts index 60f9c078dc..d1f87903ac 100644 --- a/src/app/app-routing.module.ts +++ b/src/app/app-routing.module.ts @@ -48,6 +48,7 @@ import { ReloadGuard } from './core/reload/reload.guard'; { path: 'login', loadChildren: './+login-page/login-page.module#LoginPageModule' }, { path: 'logout', loadChildren: './+logout-page/logout-page.module#LogoutPageModule' }, { path: 'submit', loadChildren: './+submit-page/submit-page.module#SubmitPageModule' }, + { path: 'import-external', loadChildren: './+import-external-page/import-external-page.module#ImportExternalPageModule' }, { path: 'workspaceitems', loadChildren: './+workspaceitems-edit-page/workspaceitems-edit-page.module#WorkspaceitemsEditPageModule' diff --git a/src/app/core/data/external-source.service.ts b/src/app/core/data/external-source.service.ts index 0c1a8d255c..edc538a39b 100644 --- a/src/app/core/data/external-source.service.ts +++ b/src/app/core/data/external-source.service.ts @@ -19,6 +19,7 @@ import { RemoteData } from './remote-data'; import { PaginatedList } from './paginated-list'; import { ExternalSourceEntry } from '../shared/external-source-entry.model'; import { DefaultChangeAnalyzer } from './default-change-analyzer.service'; +import { FollowLinkConfig } from '../../shared/utils/follow-link-config.model'; /** * A service handling all external source requests diff --git a/src/app/shared/collection-dropdown/collection-dropdown.component.ts b/src/app/shared/collection-dropdown/collection-dropdown.component.ts index 05105d74a7..cd3ed698d9 100644 --- a/src/app/shared/collection-dropdown/collection-dropdown.component.ts +++ b/src/app/shared/collection-dropdown/collection-dropdown.component.ts @@ -24,7 +24,7 @@ interface CollectionListEntryItem { /** * An interface to represent an entry in the collection list */ -interface CollectionListEntry { +export interface CollectionListEntry { communities: CollectionListEntryItem[], collection: CollectionListEntryItem } diff --git a/src/app/shared/mocks/external-source.service.mock.ts b/src/app/shared/mocks/external-source.service.mock.ts new file mode 100644 index 0000000000..85d63189e5 --- /dev/null +++ b/src/app/shared/mocks/external-source.service.mock.ts @@ -0,0 +1,59 @@ +import { ExternalSourceService } from '../../core/data/external-source.service'; +import { ExternalSource } from '../../core/shared/external-source.model'; +import { ResourceType } from '../../core/shared/resource-type'; + +export const externalSourceOrcid: ExternalSource = { + type: new ResourceType('externalsource'), + id: 'orcid', + name: 'orcid', + hierarchical: false, + _links: { + entries: { + href: 'https://dspace7.4science.cloud/server/api/integration/externalsources/orcid/entries' + }, + self: { + href: 'https://dspace7.4science.cloud/server/api/integration/externalsources/orcid' + } + } +}; + +export const externalSourceCiencia: ExternalSource = { + type: new ResourceType('externalsource'), + id: 'ciencia', + name: 'ciencia', + hierarchical: false, + _links: { + entries: { + href: 'https://dspace7.4science.cloud/server/api/integration/externalsources/ciencia/entries' + }, + self: { + href: 'https://dspace7.4science.cloud/server/api/integration/externalsources/ciencia' + } + } +}; + +export const externalSourceMyStaffDb: ExternalSource = { + type: new ResourceType('externalsource'), + id: 'my_staff_db', + name: 'my_staff_db', + hierarchical: false, + _links: { + entries: { + href: 'https://dspace7.4science.cloud/server/api/integration/externalsources/my_staff_db/entries' + }, + self: { + href: 'https://dspace7.4science.cloud/server/api/integration/externalsources/my_staff_db' + } + } +}; + +/** + * Mock for [[ExternalSourceService]] + */ +export function getMockExternalSourceService(): +ExternalSourceService { + return jasmine.createSpyObj('ExternalSourceService', { + findAll: jasmine.createSpy('findAll'), + getExternalSourceEntries: jasmine.createSpy('getExternalSourceEntries'), + }); +} diff --git a/src/app/shared/shared.module.ts b/src/app/shared/shared.module.ts index 66bdea9217..acd9aea651 100644 --- a/src/app/shared/shared.module.ts +++ b/src/app/shared/shared.module.ts @@ -478,6 +478,7 @@ const ENTRY_COMPONENTS = [ ClaimedTaskActionsRejectComponent, ClaimedTaskActionsReturnToPoolComponent, ClaimedTaskActionsEditMetadataComponent, + CollectionDropdownComponent, FileDownloadLinkComponent, CurationFormComponent, ExportMetadataSelectorComponent, diff --git a/src/app/shared/testing/submission-service.stub.ts b/src/app/shared/testing/submission-service.stub.ts index a330fa3eee..35c3ddfee0 100644 --- a/src/app/shared/testing/submission-service.stub.ts +++ b/src/app/shared/testing/submission-service.stub.ts @@ -2,6 +2,7 @@ export class SubmissionServiceStub { changeSubmissionCollection = jasmine.createSpy('changeSubmissionCollection'); createSubmission = jasmine.createSpy('createSubmission'); + createSubmissionFromExternalSource = jasmine.createSpy('createSubmissionFromExternalSource'); depositSubmission = jasmine.createSpy('depositSubmission'); discardSubmission = jasmine.createSpy('discardSubmission'); dispatchInit = jasmine.createSpy('dispatchInit'); diff --git a/src/app/submission/import-external/import-external-collection/submission-import-external-collection.component.html b/src/app/submission/import-external/import-external-collection/submission-import-external-collection.component.html new file mode 100644 index 0000000000..73b41378c8 --- /dev/null +++ b/src/app/submission/import-external/import-external-collection/submission-import-external-collection.component.html @@ -0,0 +1,11 @@ + + {{'dso-selector.create.collection.head' | translate}} + + × + + + + + + + diff --git a/src/app/submission/import-external/import-external-collection/submission-import-external-collection.component.scss b/src/app/submission/import-external/import-external-collection/submission-import-external-collection.component.scss new file mode 100644 index 0000000000..1a70081367 --- /dev/null +++ b/src/app/submission/import-external/import-external-collection/submission-import-external-collection.component.scss @@ -0,0 +1,3 @@ +.close:focus { + outline: none !important; +} diff --git a/src/app/submission/import-external/import-external-collection/submission-import-external-collection.component.spec.ts b/src/app/submission/import-external/import-external-collection/submission-import-external-collection.component.spec.ts new file mode 100644 index 0000000000..5002c4f94a --- /dev/null +++ b/src/app/submission/import-external/import-external-collection/submission-import-external-collection.component.spec.ts @@ -0,0 +1,89 @@ +import { Component, NO_ERRORS_SCHEMA, EventEmitter } from '@angular/core'; +import { async, TestBed, ComponentFixture, inject } from '@angular/core/testing'; +import { TranslateModule } from '@ngx-translate/core'; +import { createTestComponent } from '../../../shared/testing/utils.test'; +import { SubmissionImportExternalCollectionComponent } from './submission-import-external-collection.component'; +import { CollectionListEntry } from '../../../shared/collection-dropdown/collection-dropdown.component'; +import { NgbActiveModal } from '@ng-bootstrap/ng-bootstrap'; + +describe('SubmissionImportExternalCollectionComponent test suite', () => { + let comp: SubmissionImportExternalCollectionComponent; + let compAsAny: any; + let fixture: ComponentFixture; + + beforeEach(async (() => { + TestBed.configureTestingModule({ + imports: [ + TranslateModule.forRoot(), + ], + declarations: [ + SubmissionImportExternalCollectionComponent, + TestComponent, + ], + providers: [ + NgbActiveModal, + SubmissionImportExternalCollectionComponent + ], + schemas: [NO_ERRORS_SCHEMA] + }).compileComponents().then(); + })); + + // First test to check the correct component creation + describe('', () => { + let testComp: TestComponent; + let testFixture: ComponentFixture; + + // synchronous beforeEach + beforeEach(() => { + const html = ` + `; + testFixture = createTestComponent(html, TestComponent) as ComponentFixture; + testComp = testFixture.componentInstance; + }); + + afterEach(() => { + testFixture.destroy(); + }); + + it('should create SubmissionImportExternalCollectionComponent', inject([SubmissionImportExternalCollectionComponent], (app: SubmissionImportExternalCollectionComponent) => { + expect(app).toBeDefined(); + })); + }); + + describe('', () => { + beforeEach(() => { + fixture = TestBed.createComponent(SubmissionImportExternalCollectionComponent); + comp = fixture.componentInstance; + compAsAny = comp; + }); + + afterEach(() => { + fixture.destroy(); + comp = null; + compAsAny = null; + }); + + it('The variable \'selectedEvent\' should be assigned', () => { + const event = new EventEmitter(); + comp.selectObject(event); + + expect(comp.selectedEvent).toEqual(event); + }); + + it('The variable \'selectedEvent\' should be assigned', () => { + spyOn(compAsAny.activeModal, 'dismiss'); + comp.closeCollectionModal(); + + expect(compAsAny.activeModal.dismiss).toHaveBeenCalled(); + }); + }); +}); + +// declare a test component +@Component({ + selector: 'ds-test-cmp', + template: `` +}) +class TestComponent { + +} diff --git a/src/app/submission/import-external/import-external-collection/submission-import-external-collection.component.ts b/src/app/submission/import-external/import-external-collection/submission-import-external-collection.component.ts new file mode 100644 index 0000000000..cbac0cb710 --- /dev/null +++ b/src/app/submission/import-external/import-external-collection/submission-import-external-collection.component.ts @@ -0,0 +1,40 @@ +import { Component, Output, EventEmitter } from '@angular/core'; +import { CollectionListEntry } from '../../../shared/collection-dropdown/collection-dropdown.component'; +import { NgbActiveModal } from '@ng-bootstrap/ng-bootstrap'; + +/** + * Wrap component for 'ds-collection-dropdown'. + */ +@Component({ + selector: 'ds-submission-import-external-collection', + styleUrls: ['./submission-import-external-collection.component.scss'], + templateUrl: './submission-import-external-collection.component.html' +}) +export class SubmissionImportExternalCollectionComponent { + /** + * The event passed by 'ds-collection-dropdown'. + */ + @Output() public selectedEvent = new EventEmitter(); + + /** + * Initialize the component variables. + * @param {NgbActiveModal} activeModal + */ + constructor( + private activeModal: NgbActiveModal + ) { } + + /** + * This method populates the 'selectedEvent' variable. + */ + public selectObject(event): void { + this.selectedEvent.emit(event); + } + + /** + * This method closes the modal. + */ + public closeCollectionModal(): void { + this.activeModal.dismiss(false); + } +} diff --git a/src/app/submission/import-external/import-external-preview/submission-import-external-preview.component.html b/src/app/submission/import-external/import-external-preview/submission-import-external-preview.component.html new file mode 100644 index 0000000000..83c1ed82b6 --- /dev/null +++ b/src/app/submission/import-external/import-external-preview/submission-import-external-preview.component.html @@ -0,0 +1,39 @@ + + {{'submission.import-external.preview.title' | translate}} + + × + + + + + + + {{'submission.import-external.preview.subtitle' | translate}} + + + + + + + + + + {{'item.preview.' + metadata.key | translate}} + {{metadata.value.value}} + + + + + + + + + + + {{'submission.import-external.preview.button.import' | translate}} + + + + + diff --git a/src/app/submission/import-external/import-external-preview/submission-import-external-preview.component.scss b/src/app/submission/import-external/import-external-preview/submission-import-external-preview.component.scss new file mode 100644 index 0000000000..1a70081367 --- /dev/null +++ b/src/app/submission/import-external/import-external-preview/submission-import-external-preview.component.scss @@ -0,0 +1,3 @@ +.close:focus { + outline: none !important; +} diff --git a/src/app/submission/import-external/import-external-preview/submission-import-external-preview.component.spec.ts b/src/app/submission/import-external/import-external-preview/submission-import-external-preview.component.spec.ts new file mode 100644 index 0000000000..aaa6887870 --- /dev/null +++ b/src/app/submission/import-external/import-external-preview/submission-import-external-preview.component.spec.ts @@ -0,0 +1,165 @@ +import { Component, NO_ERRORS_SCHEMA } from '@angular/core'; +import { async, ComponentFixture, inject, TestBed } from '@angular/core/testing'; +import { TranslateModule } from '@ngx-translate/core'; +import { Router } from '@angular/router'; +import { NgbActiveModal, NgbModal } from '@ng-bootstrap/ng-bootstrap'; +import { TestScheduler } from 'rxjs/testing'; +import { of as observableOf } from 'rxjs/internal/observable/of'; +import { getTestScheduler } from 'jasmine-marbles'; +import { SubmissionImportExternalPreviewComponent } from './submission-import-external-preview.component'; +import { NotificationsService } from '../../../shared/notifications/notifications.service'; +import { RouterStub } from '../../../shared/testing/router.stub'; +import { SubmissionService } from '../../submission.service'; +import { createTestComponent } from '../../../shared/testing/utils.test'; +import { SubmissionServiceStub } from '../../../shared/testing/submission-service.stub'; +import { NotificationsServiceStub } from '../../../shared/testing/notifications-service.stub'; +import { ExternalSourceEntry } from '../../../core/shared/external-source-entry.model'; +import { Metadata } from '../../../core/shared/metadata.utils'; +import { SubmissionImportExternalCollectionComponent } from '../import-external-collection/submission-import-external-collection.component'; +import { CollectionListEntry } from '../../../shared/collection-dropdown/collection-dropdown.component'; + +describe('SubmissionImportExternalPreviewComponent test suite', () => { + let comp: SubmissionImportExternalPreviewComponent; + let compAsAny: any; + let fixture: ComponentFixture; + let submissionServiceStub: SubmissionServiceStub; + let scheduler: TestScheduler; + const ngbActiveModal = jasmine.createSpyObj('modal', ['close', 'dismiss']); + const ngbModal = jasmine.createSpyObj('modal', ['open']); + const externalEntry = Object.assign(new ExternalSourceEntry(), { + id: '0001-0001-0001-0001', + display: 'John Doe', + value: 'John, Doe', + metadata: { + 'dc.identifier.uri': [ + { + value: 'https://orcid.org/0001-0001-0001-0001' + } + ] + }, + _links: { self: { href: 'http://test-rest.com/server/api/integration/externalSources/orcidV2/entryValues/0000-0003-4851-8004' } } + }); + + beforeEach(async(() => { + scheduler = getTestScheduler(); + TestBed.configureTestingModule({ + imports: [ + TranslateModule.forRoot() + ], + declarations: [ + SubmissionImportExternalPreviewComponent, + TestComponent + ], + providers: [ + { provide: Router, useValue: new RouterStub() }, + { provide: SubmissionService, useValue: new SubmissionServiceStub() }, + { provide: NotificationsService, useValue: new NotificationsServiceStub() }, + { provide: NgbModal, useValue: ngbModal }, + { provide: NgbActiveModal, useValue: ngbActiveModal }, + SubmissionImportExternalPreviewComponent + ], + schemas: [NO_ERRORS_SCHEMA] + }).compileComponents().then(); + })); + + // First test to check the correct component creation + describe('', () => { + let testComp: TestComponent; + let testFixture: ComponentFixture; + + // synchronous beforeEach + beforeEach(() => { + const html = ` + `; + testFixture = createTestComponent(html, TestComponent) as ComponentFixture; + testComp = testFixture.componentInstance; + }); + + afterEach(() => { + testFixture.destroy(); + }); + + it('should create SubmissionImportExternalPreviewComponent', inject([SubmissionImportExternalPreviewComponent], (app: SubmissionImportExternalPreviewComponent) => { + expect(app).toBeDefined(); + })); + }); + + describe('', () => { + beforeEach(() => { + fixture = TestBed.createComponent(SubmissionImportExternalPreviewComponent); + comp = fixture.componentInstance; + compAsAny = comp; + submissionServiceStub = TestBed.get(SubmissionService); + }); + + afterEach(() => { + fixture.destroy(); + comp = null; + compAsAny = null; + }); + + it('Should init component properly', () => { + comp.externalSourceEntry = externalEntry; + const expected = [ + { key: 'dc.identifier.uri', value: Metadata.first(comp.externalSourceEntry.metadata, 'dc.identifier.uri') } + ]; + fixture.detectChanges(); + + expect(comp.metadataList).toEqual(expected); + }); + + it('Should close the modal calling \'activeModal.dismiss\'', () => { + comp.modalRef = jasmine.createSpyObj('modal', ['close', 'dismiss']); + comp.closeMetadataModal(); + + expect(compAsAny.activeModal.dismiss).toHaveBeenCalled(); + }); + + it('Should start the import process opening a modal', (done) => { + const emittedEvent: CollectionListEntry = { + communities: [ + { + id: 'dummy', + uuid: 'dummy', + name: 'dummy', + } + ], + collection: { + id: 'ce64f48e-2c9b-411a-ac36-ee429c0e6a88', + uuid: 'ce64f48e-2c9b-411a-ac36-ee429c0e6a88', + name: 'Collection 1', + } + }; + const submissionObjects = [ + { id: 'jk11k13o-9v4z-632i-sr88-wq071n0h1d47' } + ]; + comp.externalSourceEntry = externalEntry; + ngbModal.open.and.returnValue({ + componentInstance: { selectedEvent: observableOf(emittedEvent) }, + close: () => { + return; + } + }); + spyOn(comp, 'closeMetadataModal'); + submissionServiceStub.createSubmissionFromExternalSource.and.returnValue(observableOf(submissionObjects)); + spyOn(compAsAny.router, 'navigateByUrl'); + scheduler.schedule(() => comp.import()); + scheduler.flush(); + + expect(compAsAny.modalService.open).toHaveBeenCalledWith(SubmissionImportExternalCollectionComponent, { size: 'lg' }); + expect(comp.closeMetadataModal).toHaveBeenCalled(); + expect(compAsAny.submissionService.createSubmissionFromExternalSource).toHaveBeenCalledWith(externalEntry._links.self.href, emittedEvent.collection.id); + expect(compAsAny.router.navigateByUrl).toHaveBeenCalledWith('/workspaceitems/' + submissionObjects[0].id + '/edit'); + done(); + }); + }); +}); + +// declare a test component +@Component({ + selector: 'ds-test-cmp', + template: `` +}) +class TestComponent { + +} diff --git a/src/app/submission/import-external/import-external-preview/submission-import-external-preview.component.ts b/src/app/submission/import-external/import-external-preview/submission-import-external-preview.component.ts new file mode 100644 index 0000000000..8294ec66ba --- /dev/null +++ b/src/app/submission/import-external/import-external-preview/submission-import-external-preview.component.ts @@ -0,0 +1,100 @@ +import { Component, OnInit } from '@angular/core'; +import { Router } from '@angular/router'; +import { NgbActiveModal, NgbModalRef, NgbModal } from '@ng-bootstrap/ng-bootstrap'; +import { ExternalSourceEntry } from '../../../core/shared/external-source-entry.model'; +import { MetadataValue } from '../../../core/shared/metadata.models'; +import { Metadata } from '../../../core/shared/metadata.utils'; +import { CollectionListEntry } from '../../../shared/collection-dropdown/collection-dropdown.component'; +import { mergeMap } from 'rxjs/operators'; +import { SubmissionService } from '../../submission.service'; +import { SubmissionObject } from '../../../core/submission/models/submission-object.model'; +import { NotificationsService } from '../../../shared/notifications/notifications.service'; +import { SubmissionImportExternalCollectionComponent } from '../import-external-collection/submission-import-external-collection.component'; + +/** + * This component display a preview of an external source item. + */ +@Component({ + selector: 'ds-submission-import-external-preview', + styleUrls: ['./submission-import-external-preview.component.scss'], + templateUrl: './submission-import-external-preview.component.html' +}) +export class SubmissionImportExternalPreviewComponent implements OnInit { + /** + * The external source entry + */ + public externalSourceEntry: ExternalSourceEntry; + /** + * The entry metadata list + */ + public metadataList: Array<{ key: string, value: MetadataValue }>; + /** + * The modal for the entry preview + */ + modalRef: NgbModalRef; + + /** + * Initialize the component variables. + * @param {NgbActiveModal} activeModal + * @param {SubmissionService} submissionService + * @param {NgbModal} modalService + * @param {Router} router + * @param {NotificationsService} notificationService + */ + constructor( + private activeModal: NgbActiveModal, + private submissionService: SubmissionService, + private modalService: NgbModal, + private router: Router, + private notificationService: NotificationsService + ) { } + + /** + * Metadata initialization for HTML display. + */ + ngOnInit(): void { + this.metadataList = []; + const metadataKeys = Object.keys(this.externalSourceEntry.metadata); + metadataKeys.forEach((key) => { + this.metadataList.push({ + key: key, + value: Metadata.first(this.externalSourceEntry.metadata, key) + }); + }) + } + + /** + * Closes the modal. + */ + public closeMetadataModal(): void { + this.activeModal.dismiss(false); + } + + /** + * Start the import of an entry by opening up a collection choice modal window. + */ + public import(): void { + this.modalRef = this.modalService.open(SubmissionImportExternalCollectionComponent, { + size: 'lg', + }); + this.closeMetadataModal(); + + this.modalRef.componentInstance.selectedEvent.pipe( + mergeMap((collectionListEntry: CollectionListEntry) => { + return this.submissionService.createSubmissionFromExternalSource(this.externalSourceEntry._links.self.href, collectionListEntry.collection.id); + }) + ).subscribe((submissionObjects: SubmissionObject[]) => { + let isValid = false + if (submissionObjects.length === 1) { + if (submissionObjects[0] !== null) { + isValid = true; + this.router.navigateByUrl('/workspaceitems/' + submissionObjects[0].id + '/edit'); + } + } + if (!isValid) { + this.notificationService.error('submission.import-external.preview.error.import.title', 'submission.import-external.preview.error.import.body'); + } + this.modalRef.close(); + }); + } +} diff --git a/src/app/submission/import-external/import-external-searchbar/submission-import-external-searchbar.component.html b/src/app/submission/import-external/import-external-searchbar/submission-import-external-searchbar.component.html new file mode 100644 index 0000000000..535395e534 --- /dev/null +++ b/src/app/submission/import-external/import-external-searchbar/submission-import-external-searchbar.component.html @@ -0,0 +1,25 @@ + + + + + + + {{'submission.import-external.source.' + selectedElement?.name | translate}} + + + {{'submission.import-external.source.' + source?.name | translate}} + {{'submission.import-external.source.loading' | translate}} + + + {{'submission.import-external.search.button' | translate}} + + diff --git a/src/app/submission/import-external/import-external-searchbar/submission-import-external-searchbar.component.scss b/src/app/submission/import-external/import-external-searchbar/submission-import-external-searchbar.component.scss new file mode 100644 index 0000000000..0d2a79b056 --- /dev/null +++ b/src/app/submission/import-external/import-external-searchbar/submission-import-external-searchbar.component.scss @@ -0,0 +1,27 @@ +.input-group-append:last-child > .btn:not(:last-child):not(.dropdown-toggle) { + margin-left: -1px; + border-radius: 0 0.25rem 0.25rem 0; +} + +.input-group-append .dropdown-toggle { + border-radius: 0; +} + +.w-fx { + min-width: 200px; +} + +.scrollable-menu { + height: auto; + max-height: $dropdown-menu-max-height; + overflow-x: hidden; +} + +.scrollable-dropdown-loading { + background-color: map-get($theme-colors, primary); + color: white; + height: $spacer * 2 !important; + line-height: $spacer * 2; + position: sticky; + bottom: 0; +} diff --git a/src/app/submission/import-external/import-external-searchbar/submission-import-external-searchbar.component.spec.ts b/src/app/submission/import-external/import-external-searchbar/submission-import-external-searchbar.component.spec.ts new file mode 100644 index 0000000000..09a1781d1d --- /dev/null +++ b/src/app/submission/import-external/import-external-searchbar/submission-import-external-searchbar.component.spec.ts @@ -0,0 +1,158 @@ +import { Component, NO_ERRORS_SCHEMA, ChangeDetectorRef } from '@angular/core'; +import { async, TestBed, ComponentFixture, inject, fakeAsync, tick } from '@angular/core/testing'; +import { TranslateModule } from '@ngx-translate/core'; +import { of as observableOf } from 'rxjs'; +import { SubmissionImportExternalSearchbarComponent, SourceElement } from './submission-import-external-searchbar.component'; +import { ExternalSourceService } from '../../../core/data/external-source.service'; +import { createTestComponent } from '../../../shared/testing/utils.test'; +import { getMockExternalSourceService, externalSourceOrcid, externalSourceCiencia, externalSourceMyStaffDb } from '../../../shared/mocks/external-source.service.mock'; +import { PageInfo } from '../../../core/shared/page-info.model'; +import { PaginatedList } from '../../../core/data/paginated-list'; +import { createSuccessfulRemoteDataObject } from '../../../shared/remote-data.utils'; +import { ExternalSource } from '../../../core/shared/external-source.model'; +import { FindListOptions } from '../../../core/data/request.models'; +import { HostWindowService } from '../../../shared/host-window.service'; +import { HostWindowServiceStub } from '../../../shared/testing/host-window-service.stub'; +import { getTestScheduler } from 'jasmine-marbles'; +import { TestScheduler } from 'rxjs/testing'; + +describe('SubmissionImportExternalSearchbarComponent test suite', () => { + let comp: SubmissionImportExternalSearchbarComponent; + let compAsAny: any; + let fixture: ComponentFixture; + let scheduler: TestScheduler; + + beforeEach(async (() => { + scheduler = getTestScheduler(); + TestBed.configureTestingModule({ + imports: [ + TranslateModule.forRoot(), + ], + declarations: [ + SubmissionImportExternalSearchbarComponent, + TestComponent, + ], + providers: [ + { provide: ExternalSourceService, useClass: getMockExternalSourceService }, + ChangeDetectorRef, + { provide: HostWindowService, useValue: new HostWindowServiceStub(800) }, + SubmissionImportExternalSearchbarComponent + ], + schemas: [NO_ERRORS_SCHEMA] + }).compileComponents().then(); + })); + + // First test to check the correct component creation + describe('', () => { + let testComp: TestComponent; + let testFixture: ComponentFixture; + + // synchronous beforeEach + beforeEach(() => { + const html = ` + `; + testFixture = createTestComponent(html, TestComponent) as ComponentFixture; + testComp = testFixture.componentInstance; + }); + + afterEach(() => { + testFixture.destroy(); + }); + + it('should create SubmissionImportExternalSearchbarComponent', inject([SubmissionImportExternalSearchbarComponent], (app: SubmissionImportExternalSearchbarComponent) => { + expect(app).toBeDefined(); + })); + }); + + describe('', () => { + let sourceList: SourceElement[]; + let paginatedList: PaginatedList; + + beforeEach(() => { + fixture = TestBed.createComponent(SubmissionImportExternalSearchbarComponent); + comp = fixture.componentInstance; + compAsAny = comp; + const pageInfo = new PageInfo(); + paginatedList = new PaginatedList(pageInfo, [externalSourceOrcid, externalSourceCiencia, externalSourceMyStaffDb]); + const paginatedListRD = createSuccessfulRemoteDataObject(paginatedList); + compAsAny.externalService.findAll.and.returnValue(observableOf(paginatedListRD)); + sourceList = [ + {id: 'orcid', name: 'orcid'}, + {id: 'ciencia', name: 'ciencia'}, + {id: 'my_staff_db', name: 'my_staff_db'}, + ]; + }); + + afterEach(() => { + fixture.destroy(); + comp = null; + compAsAny = null; + }); + + it('Should init component properly (without initExternalSourceData)', () => { + comp.initExternalSourceData = { sourceId: '', query: '' }; + scheduler.schedule(() => fixture.detectChanges()); + scheduler.flush(); + + expect(comp.selectedElement).toEqual(sourceList[0]); + expect(compAsAny.pageInfo).toEqual(paginatedList.pageInfo); + expect(comp.sourceList).toEqual(sourceList); + }); + + it('Should init component properly (with initExternalSourceData populated)', () => { + comp.initExternalSourceData = { query: 'dummy', sourceId: 'ciencia' }; + scheduler.schedule(() => fixture.detectChanges()); + scheduler.flush(); + + expect(comp.selectedElement).toEqual(sourceList[1]); + expect(compAsAny.pageInfo).toEqual(paginatedList.pageInfo); + expect(comp.sourceList).toEqual(sourceList); + }); + + it('Variable \'selectedElement\' should be assigned', () => { + const selectedElement = {id: 'orcid', name: 'orcid'}; + comp.makeSourceSelection(selectedElement); + expect(comp.selectedElement).toEqual(selectedElement); + }); + + it('Should load additional external sources', () => { + comp.sourceListLoading = false; + compAsAny.pageInfo = new PageInfo({ + elementsPerPage: 3, + totalElements: 6, + totalPages: 2, + currentPage: 0 + }); + compAsAny.findListOptions = Object.assign({}, new FindListOptions(), { + elementsPerPage: 3, + currentPage: 0, + }); + comp.sourceList = sourceList; + const expected = sourceList.concat(sourceList); + + scheduler.schedule(() => comp.onScroll()); + scheduler.flush(); + + expect(comp.sourceList).toEqual(expected); + }); + + it('The \'search\' method should call \'emit\'', () => { + comp.selectedElement = { id: 'orcidV2', name: 'orcidV2' }; + comp.searchString = 'dummy'; + const expected = { sourceId: comp.selectedElement.id, query: comp.searchString }; + spyOn(comp.externalSourceData, 'emit'); + comp.search(); + + expect(comp.externalSourceData.emit).toHaveBeenCalledWith(expected); + }); + }); +}); + +// declare a test component +@Component({ + selector: 'ds-test-cmp', + template: `` +}) +class TestComponent { + +} diff --git a/src/app/submission/import-external/import-external-searchbar/submission-import-external-searchbar.component.ts b/src/app/submission/import-external/import-external-searchbar/submission-import-external-searchbar.component.ts new file mode 100644 index 0000000000..bb156e1878 --- /dev/null +++ b/src/app/submission/import-external/import-external-searchbar/submission-import-external-searchbar.component.ts @@ -0,0 +1,172 @@ +import { ChangeDetectorRef, Component, EventEmitter, Input, OnInit, Output } from '@angular/core'; + +import { of as observableOf, Observable } from 'rxjs'; +import { catchError, tap } from 'rxjs/operators'; + +import { ExternalSourceService } from '../../../core/data/external-source.service'; +import { ExternalSource } from '../../../core/shared/external-source.model'; +import { PaginatedList } from '../../../core/data/paginated-list'; +import { RemoteData } from '../../../core/data/remote-data'; +import { PageInfo } from '../../../core/shared/page-info.model'; +import { createSuccessfulRemoteDataObject } from '../../../shared/remote-data.utils'; +import { FindListOptions } from '../../../core/data/request.models'; +import { getFirstSucceededRemoteDataPayload } from '../../../core/shared/operators'; +import { HostWindowService } from '../../../shared/host-window.service'; + +/** + * Interface for the selected external source element. + */ +export interface SourceElement { + id: string; + name: string; +} + +/** + * Interface for the external source data to export. + */ +export interface ExternalSourceData { + query: string; + sourceId: string; +} + +/** + * This component builds the searchbar for the submission external import. + */ +@Component({ + selector: 'ds-submission-import-external-searchbar', + styleUrls: ['./submission-import-external-searchbar.component.scss'], + templateUrl: './submission-import-external-searchbar.component.html' +}) +export class SubmissionImportExternalSearchbarComponent implements OnInit { + /** + * The init external source value. + */ + @Input() public initExternalSourceData: ExternalSourceData; + /** + * The selected external sources. + */ + public selectedElement: SourceElement; + /** + * The list of external sources. + */ + public sourceList: SourceElement[]; + /** + * The string used to search items in the external sources. + */ + public searchString: string; + /** + * The external sources loading status. + */ + public sourceListLoading = false; + /** + * Emits true if were on a small screen + */ + public isXsOrSm$: Observable; + /** + * The external source data to use to perform the search. + */ + @Output() public externalSourceData: EventEmitter = new EventEmitter(); + + /** + * The external sources pagination data. + */ + protected pageInfo: PageInfo; + /** + * The options for REST data retireval. + */ + protected findListOptions: FindListOptions; + + /** + * Initialize the component variables. + * @param {ExternalSourceService} externalService + * @param {ChangeDetectorRef} cdr + * @param {HostWindowService} windowService + */ + constructor( + private externalService: ExternalSourceService, + private cdr: ChangeDetectorRef, + protected windowService: HostWindowService + ) { + } + + /** + * Component initialization and retrieve first page of external sources. + */ + ngOnInit() { + this.selectedElement = { + id: '', + name: 'loading' + }; + this.searchString = ''; + this.sourceList = []; + this.findListOptions = Object.assign({}, new FindListOptions(), { + elementsPerPage: 5, + currentPage: 0, + }); + this.externalService.findAll(this.findListOptions).pipe( + catchError(() => { + const pageInfo = new PageInfo(); + const paginatedList = new PaginatedList(pageInfo, []); + const paginatedListRD = createSuccessfulRemoteDataObject(paginatedList); + return observableOf(paginatedListRD); + }), + getFirstSucceededRemoteDataPayload() + ).subscribe((externalSource: PaginatedList) => { + externalSource.page.forEach((element) => { + this.sourceList.push({ id: element.id, name: element.name }); + if (this.initExternalSourceData.sourceId === element.id) { + this.selectedElement = { id: element.id, name: element.name }; + this.searchString = this.initExternalSourceData.query; + } + }); + if (this.selectedElement.id === '') { + this.selectedElement = this.sourceList[0]; + } + this.pageInfo = externalSource.pageInfo; + this.cdr.detectChanges(); + }); + this.isXsOrSm$ = this.windowService.isXsOrSm(); + } + + /** + * Set the selected external source. + */ + public makeSourceSelection(source): void { + this.selectedElement = source; + } + + /** + * Load the next pages of external sources. + */ + public onScroll(): void { + if (!this.sourceListLoading && this.pageInfo.currentPage <= this.pageInfo.totalPages) { + this.sourceListLoading = true; + this.findListOptions = Object.assign({}, new FindListOptions(), { + elementsPerPage: 5, + currentPage: this.findListOptions.currentPage + 1, + }); + this.externalService.findAll(this.findListOptions).pipe( + catchError(() => { + const pageInfo = new PageInfo(); + const paginatedList = new PaginatedList(pageInfo, []); + const paginatedListRD = createSuccessfulRemoteDataObject(paginatedList); + return observableOf(paginatedListRD); + }), + tap(() => this.sourceListLoading = false) + ).subscribe((externalSource: RemoteData>) => { + externalSource.payload.page.forEach((element) => { + this.sourceList.push({ id: element.id, name: element.name }); + }) + this.pageInfo = externalSource.payload.pageInfo; + this.cdr.detectChanges(); + }) + } + } + + /** + * Passes the search parameters to the parent component. + */ + public search(): void { + this.externalSourceData.emit({ sourceId: this.selectedElement.id, query: this.searchString }); + } +} diff --git a/src/app/submission/import-external/submission-import-external.component.html b/src/app/submission/import-external/submission-import-external.component.html new file mode 100644 index 0000000000..c4de97b934 --- /dev/null +++ b/src/app/submission/import-external/submission-import-external.component.html @@ -0,0 +1,46 @@ + + + + {{'submission.import-external.title' | translate}} + + + + + + + + {{ 'submission.sections.describe.relationship-lookup.selection-tab.title.' + routeData.sourceId | translate}} + 0" @fadeIn + [objects]="entriesRD" + [selectionConfig]="{ repeatable: repeatable, listId: listId }" + [config]="initialPagination" + [hideGear]="true" + [context]="context" + [importable]="true" + [importConfig]="importConfig" + (importObject)="import($event)"> + + + + {{ 'search.results.empty' | translate }} + + + + + + {{'submission.import-external.page.hint' | translate}} + + + + + + + + {{'submission.import-external.back-to-my-dspace' | translate}} + + + + diff --git a/src/app/submission/import-external/submission-import-external.component.scss b/src/app/submission/import-external/submission-import-external.component.scss new file mode 100644 index 0000000000..e69de29bb2 diff --git a/src/app/submission/import-external/submission-import-external.component.spec.ts b/src/app/submission/import-external/submission-import-external.component.spec.ts new file mode 100644 index 0000000000..4cbe204a91 --- /dev/null +++ b/src/app/submission/import-external/submission-import-external.component.spec.ts @@ -0,0 +1,158 @@ +import { Component, NO_ERRORS_SCHEMA } from '@angular/core'; +import { async, TestBed, ComponentFixture, inject } from '@angular/core/testing'; +import { TranslateModule } from '@ngx-translate/core'; +import { Router } from '@angular/router'; +import { NgbModal } from '@ng-bootstrap/ng-bootstrap'; +import { of as observableOf, of } from 'rxjs/internal/observable/of'; +import { SubmissionImportExternalComponent } from './submission-import-external.component'; +import { ExternalSourceService } from '../../core/data/external-source.service'; +import { getMockExternalSourceService } from '../../shared/mocks/external-source.service.mock'; +import { SearchConfigurationService } from '../../core/shared/search/search-configuration.service'; +import { RouteService } from '../../core/services/route.service'; +import { createTestComponent, createPaginatedList } from '../../shared/testing/utils.test'; +import { RouterStub } from '../../shared/testing/router.stub'; +import { VarDirective } from '../../shared/utils/var.directive'; +import { routeServiceStub } from '../../shared/testing/route-service.stub'; +import { PaginatedSearchOptions } from '../../shared/search/paginated-search-options.model'; +import { PaginationComponentOptions } from '../../shared/pagination/pagination-component-options.model'; +import { createSuccessfulRemoteDataObject } from '../../shared/remote-data.utils'; +import { ExternalSourceEntry } from '../../core/shared/external-source-entry.model'; +import { SubmissionImportExternalPreviewComponent } from './import-external-preview/submission-import-external-preview.component'; + +describe('SubmissionImportExternalComponent test suite', () => { + let comp: SubmissionImportExternalComponent; + let compAsAny: any; + let fixture: ComponentFixture; + const ngbModal = jasmine.createSpyObj('modal', ['open']); + const mockSearchOptions = of(new PaginatedSearchOptions({ + pagination: Object.assign(new PaginationComponentOptions(), { + pageSize: 10, + currentPage: 0 + }) + })); + const searchConfigServiceStub = { + paginatedSearchOptions: mockSearchOptions + }; + + beforeEach(async (() => { + TestBed.configureTestingModule({ + imports: [ + TranslateModule.forRoot() + ], + declarations: [ + SubmissionImportExternalComponent, + TestComponent, + VarDirective + ], + providers: [ + { provide: ExternalSourceService, useClass: getMockExternalSourceService }, + { provide: SearchConfigurationService, useValue: searchConfigServiceStub }, + { provide: RouteService, useValue: routeServiceStub }, + { provide: Router, useValue: new RouterStub() }, + { provide: NgbModal, useValue: ngbModal }, + SubmissionImportExternalComponent + ], + schemas: [NO_ERRORS_SCHEMA] + }).compileComponents().then(); + })); + + // First test to check the correct component creation + describe('', () => { + let testComp: TestComponent; + let testFixture: ComponentFixture; + + // synchronous beforeEach + beforeEach(() => { + const html = ` + `; + testFixture = createTestComponent(html, TestComponent) as ComponentFixture; + testComp = testFixture.componentInstance; + }); + + afterEach(() => { + testFixture.destroy(); + }); + + it('should create SubmissionImportExternalComponent', inject([SubmissionImportExternalComponent], (app: SubmissionImportExternalComponent) => { + expect(app).toBeDefined(); + })); + }); + + describe('', () => { + beforeEach(() => { + fixture = TestBed.createComponent(SubmissionImportExternalComponent); + comp = fixture.componentInstance; + compAsAny = comp; + }); + + afterEach(() => { + fixture.destroy(); + comp = null; + compAsAny = null; + }); + + it('Should init component properly (without route data)', () => { + const expectedEntries = createSuccessfulRemoteDataObject(createPaginatedList([])); + spyOn(compAsAny.routeService, 'getQueryParameterValue').and.returnValue(observableOf('')); + fixture.detectChanges(); + + expect(comp.routeData).toEqual({ sourceId: '', query: '' }); + expect(comp.isLoading$.value).toBe(false); + expect(comp.entriesRD$.value).toEqual(expectedEntries); + }); + + it('Should init component properly (with route data)', () => { + const expectedEntries = createSuccessfulRemoteDataObject(createPaginatedList([])); + const searchOptions = new PaginatedSearchOptions({ + pagination: Object.assign(new PaginationComponentOptions(), { + pageSize: 10, + currentPage: 0 + }) + }); + spyOn(compAsAny.routeService, 'getQueryParameterValue').and.returnValue(observableOf('dummy')); + fixture.detectChanges(); + + expect(comp.routeData).toEqual({ sourceId: 'dummy', query: 'dummy' }); + expect(comp.isLoading$.value).toBe(true); + expect(comp.entriesRD$.value).toEqual(expectedEntries); + expect(compAsAny.externalService.getExternalSourceEntries).toHaveBeenCalledWith('dummy', searchOptions); + }); + + it('Should call \'router.navigate\'', () => { + const event = { sourceId: 'orcidV2', query: 'dummy' }; + comp.getExternalsourceData(event); + + expect(compAsAny.router.navigate).toHaveBeenCalledWith([], { queryParams: { source: event.sourceId, query: event.query }, replaceUrl: true }); + }); + + it('Entry should be passed to the component loaded inside the modal', () => { + const entry = Object.assign(new ExternalSourceEntry(), { + id: '0001-0001-0001-0001', + display: 'John Doe', + value: 'John, Doe', + metadata: { + 'dc.identifier.uri': [ + { + value: 'https://orcid.org/0001-0001-0001-0001' + } + ] + } + }); + ngbModal.open.and.returnValue({componentInstance: { externalSourceEntry: null}}); + comp.import(entry); + + expect(compAsAny.modalService.open).toHaveBeenCalledWith(SubmissionImportExternalPreviewComponent, { size: 'lg' }); + expect(comp.modalRef.componentInstance.externalSourceEntry).toEqual(entry); + }); + }); + +}); + +// declare a test component +@Component({ + selector: 'ds-test-cmp', + template: `` +}) +class TestComponent { + +} diff --git a/src/app/submission/import-external/submission-import-external.component.ts b/src/app/submission/import-external/submission-import-external.component.ts new file mode 100644 index 0000000000..a369863a74 --- /dev/null +++ b/src/app/submission/import-external/submission-import-external.component.ts @@ -0,0 +1,153 @@ +import { Component, OnInit } from '@angular/core'; +import { Router } from '@angular/router'; +import { combineLatest, BehaviorSubject } from 'rxjs'; +import { ExternalSourceService } from '../../core/data/external-source.service'; +import { ExternalSourceData } from './import-external-searchbar/submission-import-external-searchbar.component'; +import { RemoteData } from '../../core/data/remote-data'; +import { PaginatedList } from '../../core/data/paginated-list'; +import { ExternalSourceEntry } from '../../core/shared/external-source-entry.model'; +import { SearchConfigurationService } from '../../core/shared/search/search-configuration.service'; +import { switchMap, filter, take } from 'rxjs/operators'; +import { PaginatedSearchOptions } from '../../shared/search/paginated-search-options.model'; +import { Context } from '../../core/shared/context.model'; +import { PaginationComponentOptions } from '../../shared/pagination/pagination-component-options.model'; +import { RouteService } from '../../core/services/route.service'; +import { createSuccessfulRemoteDataObject } from '../../shared/remote-data.utils'; +import { NgbModal, NgbModalRef } from '@ng-bootstrap/ng-bootstrap'; +import { SubmissionImportExternalPreviewComponent } from './import-external-preview/submission-import-external-preview.component'; +import { fadeIn } from '../../shared/animations/fade'; +import { PageInfo } from '../../core/shared/page-info.model'; + +/** + * This component allows to submit a new workspaceitem importing the data from an external source. + */ +@Component({ + selector: 'ds-submission-import-external', + styleUrls: ['./submission-import-external.component.scss'], + templateUrl: './submission-import-external.component.html', + animations: [ fadeIn ] +}) +export class SubmissionImportExternalComponent implements OnInit { + /** + * The external source search data from the routing service. + */ + public routeData: ExternalSourceData; + /** + * The displayed list of entries + */ + public entriesRD$: BehaviorSubject>>; + /** + * TRUE if the REST service is called to retrieve the external source items + */ + public isLoading$: BehaviorSubject; + /** + * Configuration to use for the import buttons + */ + public importConfig: { buttonLabel: string }; + /** + * Suffix for button label + */ + public label: string; + /** + * The ID of the list to add/remove selected items to/from + */ + public listId: string; + /** + * TRUE if the selection is repeatable + */ + public repeatable: boolean; + /** + * The initial pagination options + */ + public initialPagination = Object.assign(new PaginationComponentOptions(), { + id: 'submission-external-source-relation-list', + pageSize: 5 + }); + /** + * The context to displaying lists for + */ + public context: Context; + /** + * The modal for the entry preview + */ + public modalRef: NgbModalRef; + + /** + * Initialize the component variables. + * @param {SearchConfigurationService} searchConfigService + * @param {ExternalSourceService} externalService + * @param {RouteService} routeService + * @param {Router} router + * @param {NgbModal} modalService + */ + constructor( + public searchConfigService: SearchConfigurationService, + private externalService: ExternalSourceService, + private routeService: RouteService, + private router: Router, + private modalService: NgbModal, + ) { } + + /** + * Get the entries for the selected external source and set initial configuration. + */ + ngOnInit(): void { + this.label = 'Journal'; + this.listId = 'list-submission-external-sources'; + this.context = Context.EntitySearchModalWithNameVariants; + this.repeatable = false; + this.routeData = { sourceId: '', query: '' }; + this.importConfig = { + buttonLabel: 'submission.sections.describe.relationship-lookup.external-source.import-button-title.' + this.label + }; + this.entriesRD$ = new BehaviorSubject(createSuccessfulRemoteDataObject(new PaginatedList(new PageInfo(), []))); + this.isLoading$ = new BehaviorSubject(false); + combineLatest( + [ + this.routeService.getQueryParameterValue('source'), + this.routeService.getQueryParameterValue('query') + ]).pipe( + filter(([source, query]) => source && query && source !== '' && query !== ''), + filter(([source, query]) => source !== this.routeData.sourceId || query !== this.routeData.query), + switchMap(([source, query]) => { + this.routeData.sourceId = source; + this.routeData.query = query; + this.isLoading$.next(true); + return this.searchConfigService.paginatedSearchOptions.pipe( + switchMap((searchOptions: PaginatedSearchOptions) => { + return this.externalService.getExternalSourceEntries(this.routeData.sourceId, searchOptions); + }), + take(1) + ) + }), + ).subscribe((rdData) => { + this.entriesRD$.next(rdData); + this.isLoading$.next(false); + }); + } + + /** + * Get the data from the searchbar and changes the router data. + */ + public getExternalsourceData(event: ExternalSourceData): void { + this.router.navigate( + [], + { + queryParams: { source: event.sourceId, query: event.query }, + replaceUrl: true + } + ); + } + + /** + * Display an item preview by opening up an import modal window. + * @param entry The entry to import + */ + public import(entry): void { + this.modalRef = this.modalService.open(SubmissionImportExternalPreviewComponent, { + size: 'lg', + }); + const modalComp = this.modalRef.componentInstance; + modalComp.externalSourceEntry = entry; + } +} diff --git a/src/app/submission/submission.module.ts b/src/app/submission/submission.module.ts index 93bf06b193..d92f0758f0 100644 --- a/src/app/submission/submission.module.ts +++ b/src/app/submission/submission.module.ts @@ -28,6 +28,10 @@ import { SubmissionSectionUploadFileViewComponent } from './sections/upload/file import { SubmissionSectionUploadAccessConditionsComponent } from './sections/upload/accessConditions/submission-section-upload-access-conditions.component'; import { SubmissionSubmitComponent } from './submit/submission-submit.component'; import { storeModuleConfig } from '../app.reducer'; +import { SubmissionImportExternalComponent } from './import-external/submission-import-external.component'; +import { SubmissionImportExternalSearchbarComponent } from './import-external/import-external-searchbar/submission-import-external-searchbar.component'; +import { SubmissionImportExternalPreviewComponent } from './import-external/import-external-preview/submission-import-external-preview.component'; +import { SubmissionImportExternalCollectionComponent } from './import-external/import-external-collection/submission-import-external-collection.component'; import { SubmissionSectionCcLicensesComponent } from './sections/cc-license/submission-section-cc-licenses.component'; @NgModule({ @@ -56,19 +60,26 @@ import { SubmissionSectionCcLicensesComponent } from './sections/cc-license/subm SubmissionSectionContainerComponent, SubmissionSectionUploadFileComponent, SubmissionSectionUploadFileEditComponent, - SubmissionSectionUploadFileViewComponent + SubmissionSectionUploadFileViewComponent, + SubmissionImportExternalComponent, + SubmissionImportExternalSearchbarComponent, + SubmissionImportExternalPreviewComponent, + SubmissionImportExternalCollectionComponent ], entryComponents: [ SubmissionSectionUploadComponent, SubmissionSectionformComponent, SubmissionSectionLicenseComponent, SubmissionSectionContainerComponent, - SubmissionSectionCcLicensesComponent, + SubmissionImportExternalPreviewComponent, + SubmissionImportExternalCollectionComponent, + SubmissionSectionCcLicensesComponent ], exports: [ SubmissionEditComponent, SubmissionFormComponent, - SubmissionSubmitComponent + SubmissionSubmitComponent, + SubmissionImportExternalComponent ], providers: [ SectionUploadService, diff --git a/src/app/submission/submission.service.spec.ts b/src/app/submission/submission.service.spec.ts index 4e4d418b0d..51cfa65562 100644 --- a/src/app/submission/submission.service.spec.ts +++ b/src/app/submission/submission.service.spec.ts @@ -411,6 +411,19 @@ describe('SubmissionService test suite', () => { }); }); + describe('createSubmissionFromExternalSource', () => { + it('should deposit submission', () => { + const options: HttpOptions = Object.create({}); + let headers = new HttpHeaders(); + headers = headers.append('Content-Type', 'text/uri-list'); + options.headers = headers; + + service.createSubmissionFromExternalSource(selfUrl, collectionId); + + expect((service as any).restService.postToEndpoint).toHaveBeenCalledWith('workspaceitems', selfUrl, null, options, collectionId); + }); + }); + describe('depositSubmission', () => { it('should deposit submission', () => { const options: HttpOptions = Object.create({}); diff --git a/src/app/submission/submission.service.ts b/src/app/submission/submission.service.ts index 262612d50c..deec04ee83 100644 --- a/src/app/submission/submission.service.ts +++ b/src/app/submission/submission.service.ts @@ -114,6 +114,24 @@ export class SubmissionService { catchError(() => observableOf({} as SubmissionObject))) } + /** + * Perform a REST call to deposit a workspaceitem and return response + * + * @param selfUrl + * The workspaceitem self url + * @param collectionId + * Optional collection id + * @return Observable + * observable of SubmissionObject + */ + createSubmissionFromExternalSource(selfUrl: string, collectionId?: string): Observable { + const options: HttpOptions = Object.create({}); + let headers = new HttpHeaders(); + headers = headers.append('Content-Type', 'text/uri-list'); + options.headers = headers; + return this.restService.postToEndpoint(this.workspaceLinkPath, selfUrl, null, options, collectionId) as Observable; + } + /** * Perform a REST call to deposit a workspaceitem and return response * diff --git a/src/assets/i18n/en.json5 b/src/assets/i18n/en.json5 index b6a7c7f7aa..e437548ae9 100644 --- a/src/assets/i18n/en.json5 +++ b/src/assets/i18n/en.json5 @@ -1606,6 +1606,28 @@ "item.page.filesection.license.bundle" : "License bundle", + "item.preview.dc.identifier.uri": "Identifier:", + + "item.preview.dc.contributor.author": "Authors:", + + "item.preview.dc.date.issued": "Published date:", + + "item.preview.dc.description.abstract": "Abstract:", + + "item.preview.dc.identifier.other": "Other identifier:", + + "item.preview.dc.language.iso": "Language:", + + "item.preview.dc.subject": "Subjects:", + + "item.preview.dc.title": "Title:", + + "item.preview.person.familyName": "Surname:", + + "item.preview.person.givenName": "Name:", + + "item.preview.person.identifier.orcid": "ORCID:", + "item.select.confirm": "Confirm selected", @@ -1960,6 +1982,10 @@ "mydspace.new-submission": "New submission", + "mydspace.new-submission-external": "Import metadata from external source", + + "mydspace.new-submission-external-short": "Import metadata", + "mydspace.results.head": "Your submissions", "mydspace.results.no-abstract": "No Abstract", @@ -2748,6 +2774,43 @@ "submission.general.save-later": "Save for later", + "submission.import-external.page.title": "Import metadata from an external source", + + "submission.import-external.title": "Import metadata from an external source", + + "submission.import-external.page.hint": "Enter a query above to find items from the web to import in to DSpace.", + + "submission.import-external.back-to-my-dspace": "Back to MyDSpace", + + "submission.import-external.search.placeholder": "Search the external source", + + "submission.import-external.search.button": "Search", + + "submission.import-external.search.button.hint": "Write some words to search", + + "submission.import-external.search.source.hint": "Pick an external source", + + "submission.import-external.source.loading": "Loading ...", + + "submission.import-external.source.sherpaJournal": "SHERPA Journals", + + "submission.import-external.source.sherpaPublisher": "SHERPA Publishers", + + "submission.import-external.source.orcidV2": "ORCID", + + "submission.import-external.source.pubmed": "Pubmed", + + "submission.import-external.source.lcname": "Library of Congress Names", + + "submission.import-external.preview.title": "Item Preview", + + "submission.import-external.preview.subtitle": "The metadata below was imported from an external source. It will be pre-filled when you start the submission.", + + "submission.import-external.preview.button.import": "Start submission", + + "submission.import-external.preview.error.import.title": "Submission error", + + "submission.import-external.preview.error.import.body": "An error occurs during the external source entry import process.", "submission.sections.describe.relationship-lookup.close": "Close", @@ -2889,8 +2952,12 @@ "submission.sections.describe.relationship-lookup.selection-tab.title.orcidV2": "Search Results", + "submission.sections.describe.relationship-lookup.selection-tab.title.orcidv2": "Search Results", + "submission.sections.describe.relationship-lookup.selection-tab.title.lcname": "Search Results", + "submission.sections.describe.relationship-lookup.selection-tab.title.pubmed": "Search Results", + "submission.sections.describe.relationship-lookup.name-variant.notification.content": "Would you like to save \"{{ value }}\" as a name variant for this person so you and others can reuse it for future submissions? If you don\'t you can still use it for this submission.", "submission.sections.describe.relationship-lookup.name-variant.notification.confirm": "Save a new name variant",
{{'submission.import-external.preview.subtitle' | translate}}
{{metadata.value.value}}
{{'submission.import-external.source.loading' | translate}}
{{'submission.import-external.page.hint' | translate}}