Merge branch 'w2p-72699_Hard-redirect-after-log-in' into Hard-redirect-after-log-in

This commit is contained in:
Kristof De Langhe
2020-09-07 10:30:09 +02:00
36 changed files with 1479 additions and 11 deletions

View File

@@ -0,0 +1 @@
<ds-submission-import-external></ds-submission-import-external>

View File

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

View File

@@ -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 {
}

View File

@@ -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 {
}

View File

@@ -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 {
}

View File

@@ -8,9 +8,14 @@
</div> </div>
<div class="add"> <div class="add">
<button class="btn btn-lg btn-primary mt-1 ml-2" (click)="openDialog()" role="button"> <button class="btn btn-lg btn-primary mt-1 ml-2" (click)="openDialog()" role="button" title="{{'mydspace.new-submission' | translate}}">
<i class="fa fa-plus-circle" aria-hidden="true"></i> {{'mydspace.new-submission' | translate}} <i class="fa fa-plus-circle" aria-hidden="true"></i>
</button> </button>
</div> </div>
<div class="add">
<a class="btn btn-lg btn-primary mt-1 ml-2" [routerLink]="['/import-external']" role="button" title="{{'mydspace.new-submission-external' | translate}}">
<i class="fa fa-file-import" aria-hidden="true"></i>
</a>
</div>
</div> </div>

View File

@@ -22,6 +22,8 @@ import { getMockScrollToService } from '../../shared/mocks/scroll-to-service.moc
import { UploaderService } from '../../shared/uploader/uploader.service'; import { UploaderService } from '../../shared/uploader/uploader.service';
import { By } from '@angular/platform-browser'; import { By } from '@angular/platform-browser';
import { NgbModal } from '@ng-bootstrap/ng-bootstrap'; 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'; import { UploaderComponent } from 'src/app/shared/uploader/uploader.component';
describe('MyDSpaceNewSubmissionComponent test', () => { describe('MyDSpaceNewSubmissionComponent test', () => {
@@ -73,7 +75,8 @@ describe('MyDSpaceNewSubmissionComponent test', () => {
{ provide: NgbModal, useValue: modalService }, { provide: NgbModal, useValue: modalService },
ChangeDetectorRef, ChangeDetectorRef,
MyDSpaceNewSubmissionComponent, MyDSpaceNewSubmissionComponent,
UploaderService UploaderService,
{ provide: HostWindowService, useValue: new HostWindowServiceStub(800) },
], ],
schemas: [NO_ERRORS_SCHEMA] schemas: [NO_ERRORS_SCHEMA]
}).compileComponents(); }).compileComponents();

View File

@@ -3,6 +3,8 @@ import { ChangeDetectorRef, Component, EventEmitter, OnDestroy, OnInit, Output,
import { Subscription } from 'rxjs'; import { Subscription } from 'rxjs';
import { first } from 'rxjs/operators'; import { first } from 'rxjs/operators';
import { TranslateService } from '@ngx-translate/core'; import { TranslateService } from '@ngx-translate/core';
import { NgbModal } from '@ng-bootstrap/ng-bootstrap';
import { AuthService } from '../../core/auth/auth.service'; import { AuthService } from '../../core/auth/auth.service';
import { DSpaceObject } from '../../core/shared/dspace-object.model'; import { DSpaceObject } from '../../core/shared/dspace-object.model';
import { NotificationsService } from '../../shared/notifications/notifications.service'; 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 { NotificationType } from '../../shared/notifications/models/notification-type';
import { hasValue } from '../../shared/empty.util'; import { hasValue } from '../../shared/empty.util';
import { SearchResult } from '../../shared/search/search-result.model'; import { SearchResult } from '../../shared/search/search-result.model';
import { NgbModal } from '@ng-bootstrap/ng-bootstrap'; import { CreateItemParentSelectorComponent } from '../../shared/dso-selector/modal-wrappers/create-item-parent-selector/create-item-parent-selector.component';
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 { CollectionSelectorComponent } from '../collection-selector/collection-selector.component';
import { UploaderComponent } from 'src/app/shared/uploader/uploader.component'; import { UploaderComponent } from '../../shared/uploader/uploader.component';
import { UploaderError } from 'src/app/shared/uploader/uploader-error.model'; import { UploaderError } from '../../shared/uploader/uploader-error.model';
/** /**
* This component represents the whole mydspace page header * This component represents the whole mydspace page header
@@ -56,6 +57,8 @@ export class MyDSpaceNewSubmissionComponent implements OnDestroy, OnInit {
* @param {NotificationsService} notificationsService * @param {NotificationsService} notificationsService
* @param {Store<SubmissionState>} store * @param {Store<SubmissionState>} store
* @param {TranslateService} translate * @param {TranslateService} translate
* @param {Router} router
* @param {NgbModal} modalService
*/ */
constructor(private authService: AuthService, constructor(private authService: AuthService,
private changeDetectorRef: ChangeDetectorRef, private changeDetectorRef: ChangeDetectorRef,

View File

@@ -48,6 +48,7 @@ import { ReloadGuard } from './core/reload/reload.guard';
{ path: 'login', loadChildren: './+login-page/login-page.module#LoginPageModule' }, { path: 'login', loadChildren: './+login-page/login-page.module#LoginPageModule' },
{ path: 'logout', loadChildren: './+logout-page/logout-page.module#LogoutPageModule' }, { path: 'logout', loadChildren: './+logout-page/logout-page.module#LogoutPageModule' },
{ path: 'submit', loadChildren: './+submit-page/submit-page.module#SubmitPageModule' }, { path: 'submit', loadChildren: './+submit-page/submit-page.module#SubmitPageModule' },
{ path: 'import-external', loadChildren: './+import-external-page/import-external-page.module#ImportExternalPageModule' },
{ {
path: 'workspaceitems', path: 'workspaceitems',
loadChildren: './+workspaceitems-edit-page/workspaceitems-edit-page.module#WorkspaceitemsEditPageModule' loadChildren: './+workspaceitems-edit-page/workspaceitems-edit-page.module#WorkspaceitemsEditPageModule'

View File

@@ -19,6 +19,9 @@ export class AuthBlockingGuard implements CanActivate {
constructor(private store: Store<AppState>) { constructor(private store: Store<AppState>) {
} }
/**
* True when the authentication isn't blocking everything
*/
canActivate(): Observable<boolean> { canActivate(): Observable<boolean> {
return this.store.pipe(select(isAuthenticationBlocking)).pipe( return this.store.pipe(select(isAuthenticationBlocking)).pipe(
map((isBlocking: boolean) => isBlocking === false), map((isBlocking: boolean) => isBlocking === false),

View File

@@ -19,6 +19,7 @@ import { RemoteData } from './remote-data';
import { PaginatedList } from './paginated-list'; import { PaginatedList } from './paginated-list';
import { ExternalSourceEntry } from '../shared/external-source-entry.model'; import { ExternalSourceEntry } from '../shared/external-source-entry.model';
import { DefaultChangeAnalyzer } from './default-change-analyzer.service'; import { DefaultChangeAnalyzer } from './default-change-analyzer.service';
import { FollowLinkConfig } from '../../shared/utils/follow-link-config.model';
/** /**
* A service handling all external source requests * A service handling all external source requests

View File

@@ -24,7 +24,7 @@ interface CollectionListEntryItem {
/** /**
* An interface to represent an entry in the collection list * An interface to represent an entry in the collection list
*/ */
interface CollectionListEntry { export interface CollectionListEntry {
communities: CollectionListEntryItem[], communities: CollectionListEntryItem[],
collection: CollectionListEntryItem collection: CollectionListEntryItem
} }

View File

@@ -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'),
});
}

View File

@@ -478,6 +478,7 @@ const ENTRY_COMPONENTS = [
ClaimedTaskActionsRejectComponent, ClaimedTaskActionsRejectComponent,
ClaimedTaskActionsReturnToPoolComponent, ClaimedTaskActionsReturnToPoolComponent,
ClaimedTaskActionsEditMetadataComponent, ClaimedTaskActionsEditMetadataComponent,
CollectionDropdownComponent,
FileDownloadLinkComponent, FileDownloadLinkComponent,
CurationFormComponent, CurationFormComponent,
ExportMetadataSelectorComponent, ExportMetadataSelectorComponent,

View File

@@ -2,6 +2,7 @@ export class SubmissionServiceStub {
changeSubmissionCollection = jasmine.createSpy('changeSubmissionCollection'); changeSubmissionCollection = jasmine.createSpy('changeSubmissionCollection');
createSubmission = jasmine.createSpy('createSubmission'); createSubmission = jasmine.createSpy('createSubmission');
createSubmissionFromExternalSource = jasmine.createSpy('createSubmissionFromExternalSource');
depositSubmission = jasmine.createSpy('depositSubmission'); depositSubmission = jasmine.createSpy('depositSubmission');
discardSubmission = jasmine.createSpy('discardSubmission'); discardSubmission = jasmine.createSpy('discardSubmission');
dispatchInit = jasmine.createSpy('dispatchInit'); dispatchInit = jasmine.createSpy('dispatchInit');

View File

@@ -0,0 +1,11 @@
<div>
<div class="modal-header">{{'dso-selector.create.collection.head' | translate}}
<button type="button" class="close" (click)="closeCollectionModal()" aria-label="Close">
<span aria-hidden="true">×</span>
</button>
</div>
<div class="modal-body">
<ds-collection-dropdown (selectionChange)="selectObject($event)">
</ds-collection-dropdown>
</div>
</div>

View File

@@ -0,0 +1,3 @@
.close:focus {
outline: none !important;
}

View File

@@ -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<SubmissionImportExternalCollectionComponent>;
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<TestComponent>;
// synchronous beforeEach
beforeEach(() => {
const html = `
<ds-submission-import-external-collection></ds-submission-import-external-collection>`;
testFixture = createTestComponent(html, TestComponent) as ComponentFixture<TestComponent>;
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<CollectionListEntry>();
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 {
}

View File

@@ -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<CollectionListEntry>();
/**
* 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);
}
}

View File

@@ -0,0 +1,39 @@
<div class="modal-header">
<h2>{{'submission.import-external.preview.title' | translate}}</h2>
<button type="button" class="close"
(click)="closeMetadataModal()" aria-label="Close">
<span aria-hidden="true">×</span>
</button>
</div>
<div class="modal-body">
<div class="container">
<div class="row">
<div class="col-md-12">
<p>{{'submission.import-external.preview.subtitle' | translate}}</p>
</div>
</div>
<div class="row">
<div class="col-md-12">
<hr>
</div>
</div>
<div *ngFor="let metadata of metadataList" class="row">
<div class="col-md-12">
<strong>{{'item.preview.' + metadata.key | translate}}</strong>
<p>{{metadata.value.value}}</p>
</div>
</div>
<div class="row">
<div class="col-md-12">
<hr>
</div>
</div>
<div class="row">
<div class="col-md-12 text-right">
<button class="btn btn-success" (click)="import()" role="button">
<i class="fa fa-file-import" aria-hidden="true"></i> {{'submission.import-external.preview.button.import' | translate}}
</button>
</div>
</div>
</div>
</div>

View File

@@ -0,0 +1,3 @@
.close:focus {
outline: none !important;
}

View File

@@ -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<SubmissionImportExternalPreviewComponent>;
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<TestComponent>;
// synchronous beforeEach
beforeEach(() => {
const html = `
<ds-submission-import-external-preview></ds-submission-import-external-preview>`;
testFixture = createTestComponent(html, TestComponent) as ComponentFixture<TestComponent>;
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 {
}

View File

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

View File

@@ -0,0 +1,25 @@
<div *ngIf="(isXsOrSm$ | async)" class="input-group mb-2">
<input type="text" class="form-control" (keyup.enter)="(searchString === '')?null:search()" [(ngModel)]="searchString" placeholder="{{'submission.import-external.search.placeholder' |translate}}" aria-label="" aria-describedby="">
</div>
<div class="input-group mb-5">
<input *ngIf="!(isXsOrSm$ | async)" type="text" class="form-control" (keyup.enter)="(searchString === '')?null:search()" [(ngModel)]="searchString" placeholder="{{'submission.import-external.search.placeholder' |translate}}" aria-label="" aria-describedby="">
<div [ngClass]="{'input-group-append': !(isXsOrSm$ | async)}" ngbDropdown role="group" aria-label="">
<button class="btn btn-outline-secondary w-fx" title="{{'submission.import-external.search.source.hint' |translate}}" ngbDropdownToggle>{{'submission.import-external.source.' + selectedElement?.name | translate}}</button>
<div ngbDropdownMenu class="dropdown-menu scrollable-dropdown-menu w-100"
aria-haspopup="true"
aria-expanded="false"
aria-labelledby="scrollableDropdownMenuButton">
<div class="scrollable-menu"
aria-labelledby="scrollableDropdownMenuButton"
infiniteScroll
[infiniteScrollDistance]="2"
[infiniteScrollThrottle]="50"
(scrolled)="onScroll()"
[scrollWindow]="false">
<button ngbDropdownItem class="dropdown-item text-truncate" title="{{'submission.import-external.source.' + source?.name | translate}}" (click)="makeSourceSelection(source)" *ngFor="let source of sourceList">{{'submission.import-external.source.' + source?.name | translate}}</button>
<div ngbDropdownItem class="scrollable-dropdown-loading text-center" *ngIf="sourceListLoading"><p>{{'submission.import-external.source.loading' | translate}}</p></div>
</div>
</div>
<button type="button" class="btn btn-primary" [title]="(searchString === '')?('submission.import-external.search.button.hint' | translate):('submission.import-external.search.button' | translate)" [disabled]="searchString === ''" (click)="search()">{{'submission.import-external.search.button' | translate}}</button>
</div>
</div>

View File

@@ -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;
}

View File

@@ -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<SubmissionImportExternalSearchbarComponent>;
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<TestComponent>;
// synchronous beforeEach
beforeEach(() => {
const html = `
<ds-submission-import-external-searchbar></ds-submission-import-external-searchbar>`;
testFixture = createTestComponent(html, TestComponent) as ComponentFixture<TestComponent>;
testComp = testFixture.componentInstance;
});
afterEach(() => {
testFixture.destroy();
});
it('should create SubmissionImportExternalSearchbarComponent', inject([SubmissionImportExternalSearchbarComponent], (app: SubmissionImportExternalSearchbarComponent) => {
expect(app).toBeDefined();
}));
});
describe('', () => {
let sourceList: SourceElement[];
let paginatedList: PaginatedList<ExternalSource>;
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 {
}

View File

@@ -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<boolean>;
/**
* The external source data to use to perform the search.
*/
@Output() public externalSourceData: EventEmitter<ExternalSourceData> = new EventEmitter<ExternalSourceData>();
/**
* 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>) => {
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<PaginatedList<ExternalSource>>) => {
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 });
}
}

View File

@@ -0,0 +1,46 @@
<div class="container">
<div class="row">
<div class="col-md-12">
<h2 id="header" class="pb-2">{{'submission.import-external.title' | translate}}</h2>
<ds-submission-import-external-searchbar
[initExternalSourceData]="routeData"
(externalSourceData) = "getExternalsourceData($event)">
</ds-submission-import-external-searchbar>
</div>
</div>
<div class="row">
<div *ngIf="routeData.sourceId !== ''" class="col-md-12">
<ng-container *ngVar="(entriesRD$ | async) as entriesRD">
<h3 *ngIf="entriesRD?.payload?.page?.length !== 0">{{ 'submission.sections.describe.relationship-lookup.selection-tab.title.' + routeData.sourceId | translate}}</h3>
<ds-viewable-collection *ngIf="entriesRD?.hasSucceeded && !(isLoading$ | async) && entriesRD?.payload?.page?.length > 0" @fadeIn
[objects]="entriesRD"
[selectionConfig]="{ repeatable: repeatable, listId: listId }"
[config]="initialPagination"
[hideGear]="true"
[context]="context"
[importable]="true"
[importConfig]="importConfig"
(importObject)="import($event)">
</ds-viewable-collection>
<ds-loading *ngIf="(isLoading$ | async)"
message="{{'loading.search-results' | translate}}"></ds-loading>
<div *ngIf="!(isLoading$ | async) && entriesRD?.payload?.page?.length === 0" id="empty-external-entry-list">
{{ 'search.results.empty' | translate }}
</div>
</ng-container>
</div>
<div *ngIf="routeData.sourceId === ''" class="col-md-12">
<ds-alert [type]="'alert-info'">
<p class="lead mb-0">{{'submission.import-external.page.hint' | translate}}</p>
</ds-alert>
</div>
</div>
<div class="row">
<div class="col-md-12">
<hr>
<a class="btn btn-outline-secondary" [routerLink]="['/mydspace']" role="button">
<i class="fa fa-chevron-left" aria-hidden="true"></i> {{'submission.import-external.back-to-my-dspace' | translate}}
</a>
</div>
</div>
</div>

View File

@@ -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<SubmissionImportExternalComponent>;
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<TestComponent>;
// synchronous beforeEach
beforeEach(() => {
const html = `
<ds-submission-import-external></ds-submission-import-external>`;
testFixture = createTestComponent(html, TestComponent) as ComponentFixture<TestComponent>;
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 {
}

View File

@@ -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<RemoteData<PaginatedList<ExternalSourceEntry>>>;
/**
* TRUE if the REST service is called to retrieve the external source items
*/
public isLoading$: BehaviorSubject<boolean>;
/**
* 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;
}
}

View File

@@ -28,6 +28,10 @@ import { SubmissionSectionUploadFileViewComponent } from './sections/upload/file
import { SubmissionSectionUploadAccessConditionsComponent } from './sections/upload/accessConditions/submission-section-upload-access-conditions.component'; import { SubmissionSectionUploadAccessConditionsComponent } from './sections/upload/accessConditions/submission-section-upload-access-conditions.component';
import { SubmissionSubmitComponent } from './submit/submission-submit.component'; import { SubmissionSubmitComponent } from './submit/submission-submit.component';
import { storeModuleConfig } from '../app.reducer'; 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'; import { SubmissionSectionCcLicensesComponent } from './sections/cc-license/submission-section-cc-licenses.component';
@NgModule({ @NgModule({
@@ -56,19 +60,26 @@ import { SubmissionSectionCcLicensesComponent } from './sections/cc-license/subm
SubmissionSectionContainerComponent, SubmissionSectionContainerComponent,
SubmissionSectionUploadFileComponent, SubmissionSectionUploadFileComponent,
SubmissionSectionUploadFileEditComponent, SubmissionSectionUploadFileEditComponent,
SubmissionSectionUploadFileViewComponent SubmissionSectionUploadFileViewComponent,
SubmissionImportExternalComponent,
SubmissionImportExternalSearchbarComponent,
SubmissionImportExternalPreviewComponent,
SubmissionImportExternalCollectionComponent
], ],
entryComponents: [ entryComponents: [
SubmissionSectionUploadComponent, SubmissionSectionUploadComponent,
SubmissionSectionformComponent, SubmissionSectionformComponent,
SubmissionSectionLicenseComponent, SubmissionSectionLicenseComponent,
SubmissionSectionContainerComponent, SubmissionSectionContainerComponent,
SubmissionSectionCcLicensesComponent, SubmissionImportExternalPreviewComponent,
SubmissionImportExternalCollectionComponent,
SubmissionSectionCcLicensesComponent
], ],
exports: [ exports: [
SubmissionEditComponent, SubmissionEditComponent,
SubmissionFormComponent, SubmissionFormComponent,
SubmissionSubmitComponent SubmissionSubmitComponent,
SubmissionImportExternalComponent
], ],
providers: [ providers: [
SectionUploadService, SectionUploadService,

View File

@@ -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', () => { describe('depositSubmission', () => {
it('should deposit submission', () => { it('should deposit submission', () => {
const options: HttpOptions = Object.create({}); const options: HttpOptions = Object.create({});

View File

@@ -114,6 +114,24 @@ export class SubmissionService {
catchError(() => observableOf({} as SubmissionObject))) 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<SubmissionObject>
* observable of SubmissionObject
*/
createSubmissionFromExternalSource(selfUrl: string, collectionId?: string): Observable<SubmissionObject[]> {
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<SubmissionObject[]>;
}
/** /**
* Perform a REST call to deposit a workspaceitem and return response * Perform a REST call to deposit a workspaceitem and return response
* *

View File

@@ -1606,6 +1606,28 @@
"item.page.filesection.license.bundle" : "License bundle", "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", "item.select.confirm": "Confirm selected",
@@ -1960,6 +1982,10 @@
"mydspace.new-submission": "New submission", "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.head": "Your submissions",
"mydspace.results.no-abstract": "No Abstract", "mydspace.results.no-abstract": "No Abstract",
@@ -2748,6 +2774,43 @@
"submission.general.save-later": "Save for later", "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", "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.orcidv2": "Search Results",
"submission.sections.describe.relationship-lookup.selection-tab.title.lcname": "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.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.sections.describe.relationship-lookup.name-variant.notification.confirm": "Save a new name variant",