[CST-4591] Change import from external source page in order to work with entity type

This commit is contained in:
Giuseppe Digilio
2021-09-22 18:02:52 +02:00
parent afdbf3541b
commit 6bf3b8e7cd
12 changed files with 204 additions and 61 deletions

View File

@@ -53,6 +53,7 @@ export const externalSourceMyStaffDb: ExternalSource = {
export function getMockExternalSourceService(): ExternalSourceService {
return jasmine.createSpyObj('ExternalSourceService', {
findAll: jasmine.createSpy('findAll'),
searchBy: jasmine.createSpy('searchBy'),
getExternalSourceEntries: jasmine.createSpy('getExternalSourceEntries'),
});
}

View File

@@ -5,7 +5,12 @@
</button>
</div>
<div class="modal-body">
<ds-collection-dropdown (selectionChange)="selectObject($event)">
<ds-loading *ngIf="isLoading()"></ds-loading>
<ds-collection-dropdown [ngClass]="{'d-none': isLoading()}"
(selectionChange)="selectObject($event)"
(hasChoice)="onHasChoice($event)"
(theOnlySelectable)="theOnlySelectable($event)"
[entityType]="entityType">
</ds-collection-dropdown>
</div>
</div>

View File

@@ -1,10 +1,11 @@
import { Component, NO_ERRORS_SCHEMA, EventEmitter } from '@angular/core';
import { waitForAsync, TestBed, ComponentFixture, inject } from '@angular/core/testing';
import { Component, EventEmitter, NO_ERRORS_SCHEMA } from '@angular/core';
import { ComponentFixture, fakeAsync, inject, TestBed, waitForAsync } 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';
import { By } from '@angular/platform-browser';
describe('SubmissionImportExternalCollectionComponent test suite', () => {
let comp: SubmissionImportExternalCollectionComponent;
@@ -76,6 +77,48 @@ describe('SubmissionImportExternalCollectionComponent test suite', () => {
expect(compAsAny.activeModal.dismiss).toHaveBeenCalled();
});
it('should be in loading state when hasChoice variable is different to true', () => {
comp.hasChoice = null;
expect(comp.isLoading()).toBeTrue();
comp.hasChoice = false;
expect(comp.isLoading()).toBeTrue();
comp.hasChoice = true;
expect(comp.isLoading()).toBeFalse();
});
it('should set hasChoice variable on hasChoice event', () => {
comp.hasChoice = null;
comp.onHasChoice(true);
expect(comp.hasChoice).toBe(true);
comp.onHasChoice(false);
expect(comp.hasChoice).toBe(false);
});
it('should emit theOnlySelectable', () => {
spyOn(comp.selectedEvent, 'emit').and.callThrough();
const selected: any = {};
comp.theOnlySelectable(selected);
expect(comp.selectedEvent.emit).toHaveBeenCalledWith(selected);
});
it('dropdown should be invisible when the component is loading', fakeAsync(() => {
spyOn(comp, 'isLoading').and.returnValue(true);
fixture.detectChanges();
fixture.whenStable().then(() => {
const dropdownMenu = fixture.debugElement.query(By.css('ds-collection-dropdown')).nativeElement;
expect(dropdownMenu.classList).toContain('d-none');
});
}));
});
});

View File

@@ -1,4 +1,4 @@
import { Component, Output, EventEmitter } from '@angular/core';
import { Component, EventEmitter, Output } from '@angular/core';
import { CollectionListEntry } from '../../../shared/collection-dropdown/collection-dropdown.component';
import { NgbActiveModal } from '@ng-bootstrap/ng-bootstrap';
@@ -16,6 +16,16 @@ export class SubmissionImportExternalCollectionComponent {
*/
@Output() public selectedEvent = new EventEmitter<CollectionListEntry>();
/**
* If present this value is used to filter collection list by entity type
*/
public entityType: string;
/**
* If a collection choice is available
*/
public hasChoice: boolean = null;
/**
* Initialize the component variables.
* @param {NgbActiveModal} activeModal
@@ -37,4 +47,27 @@ export class SubmissionImportExternalCollectionComponent {
public closeCollectionModal(): void {
this.activeModal.dismiss(false);
}
/**
* Propagate the onlySelectable collection
* @param theOnlySelectable
*/
public theOnlySelectable(theOnlySelectable: CollectionListEntry) {
this.selectedEvent.emit(theOnlySelectable);
}
/**
* Set the hasChoice state
* @param hasChoice
*/
public onHasChoice(hasChoice: boolean) {
this.hasChoice = hasChoice;
}
/**
* If the component is in loading state.
*/
public isLoading(): boolean {
return this.hasChoice !== true;
}
}

View File

@@ -1,5 +1,5 @@
<div class="modal-header">
<h2>{{'submission.import-external.preview.title' | translate}}</h2>
<h2>{{'submission.import-external.preview.title.' + labelPrefix | translate}}</h2>
<button type="button" class="close"
(click)="closeMetadataModal()" aria-label="Close">
<span aria-hidden="true">×</span>

View File

@@ -1,6 +1,6 @@
import { Component, Input, OnInit } from '@angular/core';
import { Router } from '@angular/router';
import { NgbActiveModal, NgbModalRef, NgbModal } from '@ng-bootstrap/ng-bootstrap';
import { NgbActiveModal, NgbModal, NgbModalRef } 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';
@@ -28,6 +28,10 @@ export class SubmissionImportExternalPreviewComponent implements OnInit {
* The entry metadata list
*/
public metadataList: { key: string, value: MetadataValue }[];
/**
* The label prefix to use to generate the translation label
*/
public labelPrefix: string;
/**
* The modal for the entry preview
*/
@@ -77,6 +81,7 @@ export class SubmissionImportExternalPreviewComponent implements OnInit {
this.modalRef = this.modalService.open(SubmissionImportExternalCollectionComponent, {
size: 'lg',
});
this.modalRef.componentInstance.entityType = this.labelPrefix;
this.closeMetadataModal();
this.modalRef.componentInstance.selectedEvent.pipe(

View File

@@ -15,7 +15,7 @@ import {
getMockExternalSourceService
} from '../../../shared/mocks/external-source.service.mock';
import { PageInfo } from '../../../core/shared/page-info.model';
import { PaginatedList, buildPaginatedList } from '../../../core/data/paginated-list.model';
import { buildPaginatedList, PaginatedList } from '../../../core/data/paginated-list.model';
import { createSuccessfulRemoteDataObject } from '../../../shared/remote-data.utils';
import { ExternalSource } from '../../../core/shared/external-source.model';
import { FindListOptions } from '../../../core/data/request.models';
@@ -23,6 +23,7 @@ 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';
import { RequestParam } from '../../../core/cache/models/request-param.model';
describe('SubmissionImportExternalSearchbarComponent test suite', () => {
let comp: SubmissionImportExternalSearchbarComponent;
@@ -63,9 +64,9 @@ describe('SubmissionImportExternalSearchbarComponent test suite', () => {
// synchronous beforeEach
beforeEach(() => {
mockExternalSourceService.findAll.and.returnValue(observableOf(paginatedListRD));
mockExternalSourceService.searchBy.and.returnValue(observableOf(paginatedListRD));
const html = `
<ds-submission-import-external-searchbar></ds-submission-import-external-searchbar>`;
<ds-submission-import-external-searchbar [initExternalSourceData]="initExternalSourceData"></ds-submission-import-external-searchbar>`;
testFixture = createTestComponent(html, TestComponent) as ComponentFixture<TestComponent>;
testComp = testFixture.componentInstance;
});
@@ -88,7 +89,7 @@ describe('SubmissionImportExternalSearchbarComponent test suite', () => {
const pageInfo = new PageInfo();
paginatedList = buildPaginatedList(pageInfo, [externalSourceOrcid, externalSourceCiencia, externalSourceMyStaffDb]);
paginatedListRD = createSuccessfulRemoteDataObject(paginatedList);
compAsAny.externalService.findAll.and.returnValue(observableOf(paginatedListRD));
compAsAny.externalService.searchBy.and.returnValue(observableOf(paginatedListRD));
sourceList = [
{id: 'orcid', name: 'orcid'},
{id: 'ciencia', name: 'ciencia'},
@@ -103,7 +104,7 @@ describe('SubmissionImportExternalSearchbarComponent test suite', () => {
});
it('Should init component properly (without initExternalSourceData)', () => {
comp.initExternalSourceData = { sourceId: '', query: '' };
comp.initExternalSourceData = { entity: 'Publication', sourceId: '', query: '' };
scheduler.schedule(() => fixture.detectChanges());
scheduler.flush();
@@ -113,7 +114,7 @@ describe('SubmissionImportExternalSearchbarComponent test suite', () => {
});
it('Should init component properly (with initExternalSourceData populated)', () => {
comp.initExternalSourceData = { query: 'dummy', sourceId: 'ciencia' };
comp.initExternalSourceData = { entity: 'Publication', query: 'dummy', sourceId: 'ciencia' };
scheduler.schedule(() => fixture.detectChanges());
scheduler.flush();
@@ -129,6 +130,7 @@ describe('SubmissionImportExternalSearchbarComponent test suite', () => {
});
it('Should load additional external sources', () => {
comp.initExternalSourceData = { entity: 'Publication', query: 'dummy', sourceId: 'ciencia' };
comp.sourceListLoading = false;
compAsAny.pageInfo = new PageInfo({
elementsPerPage: 3,
@@ -139,6 +141,9 @@ describe('SubmissionImportExternalSearchbarComponent test suite', () => {
compAsAny.findListOptions = Object.assign({}, new FindListOptions(), {
elementsPerPage: 3,
currentPage: 0,
searchParams: [
new RequestParam('entityType', 'Publication')
]
});
comp.sourceList = sourceList;
const expected = sourceList.concat(sourceList);
@@ -150,9 +155,10 @@ describe('SubmissionImportExternalSearchbarComponent test suite', () => {
});
it('The \'search\' method should call \'emit\'', () => {
comp.initExternalSourceData = { entity: 'Publication', query: 'dummy', sourceId: 'ciencia' };
comp.selectedElement = { id: 'orcidV2', name: 'orcidV2' };
comp.searchString = 'dummy';
const expected = { sourceId: comp.selectedElement.id, query: comp.searchString };
const expected = { entity: 'Publication', sourceId: comp.selectedElement.id, query: comp.searchString };
spyOn(comp.externalSourceData, 'emit');
comp.search();
@@ -167,5 +173,5 @@ describe('SubmissionImportExternalSearchbarComponent test suite', () => {
template: ``
})
class TestComponent {
initExternalSourceData = { entity: 'Publication', query: 'dummy', sourceId: 'ciencia' };
}

View File

@@ -1,19 +1,12 @@
import {
ChangeDetectorRef,
Component,
EventEmitter,
Input,
OnDestroy,
OnInit,
Output
} from '@angular/core';
import { ChangeDetectorRef, Component, EventEmitter, Input, OnDestroy, OnInit, Output } from '@angular/core';
import { Observable, of as observableOf, Subscription } from 'rxjs';
import { catchError, tap } from 'rxjs/operators';
import { RequestParam } from '../../../core/cache/models/request-param.model';
import { ExternalSourceService } from '../../../core/data/external-source.service';
import { ExternalSource } from '../../../core/shared/external-source.model';
import { PaginatedList, buildPaginatedList } from '../../../core/data/paginated-list.model';
import { buildPaginatedList, PaginatedList } from '../../../core/data/paginated-list.model';
import { RemoteData } from '../../../core/data/remote-data';
import { PageInfo } from '../../../core/shared/page-info.model';
import { createSuccessfulRemoteDataObject } from '../../../shared/remote-data.utils';
@@ -34,6 +27,7 @@ export interface SourceElement {
* Interface for the external source data to export.
*/
export interface ExternalSourceData {
entity: string;
query: string;
sourceId: string;
}
@@ -116,8 +110,11 @@ export class SubmissionImportExternalSearchbarComponent implements OnInit, OnDes
this.findListOptions = Object.assign({}, new FindListOptions(), {
elementsPerPage: 5,
currentPage: 1,
searchParams: [
new RequestParam('entityType', this.initExternalSourceData.entity)
]
});
this.externalService.findAll(this.findListOptions).pipe(
this.externalService.searchBy('findByEntityType', this.findListOptions).pipe(
catchError(() => {
const pageInfo = new PageInfo();
const paginatedList = buildPaginatedList(pageInfo, []);
@@ -158,8 +155,11 @@ export class SubmissionImportExternalSearchbarComponent implements OnInit, OnDes
this.findListOptions = Object.assign({}, new FindListOptions(), {
elementsPerPage: 5,
currentPage: this.findListOptions.currentPage + 1,
searchParams: [
new RequestParam('entityType', this.initExternalSourceData.entity)
]
});
this.sub = this.externalService.findAll(this.findListOptions).pipe(
this.externalService.searchBy('findByEntityType', this.findListOptions).pipe(
catchError(() => {
const pageInfo = new PageInfo();
const paginatedList = buildPaginatedList(pageInfo, []);
@@ -182,7 +182,13 @@ export class SubmissionImportExternalSearchbarComponent implements OnInit, OnDes
* Passes the search parameters to the parent component.
*/
public search(): void {
this.externalSourceData.emit({ sourceId: this.selectedElement.id, query: this.searchString });
this.externalSourceData.emit(
{
entity: this.initExternalSourceData.entity,
sourceId: this.selectedElement.id,
query: this.searchString
}
);
}
/**
@@ -193,4 +199,5 @@ export class SubmissionImportExternalSearchbarComponent implements OnInit, OnDes
this.sub.unsubscribe();
}
}
}

View File

@@ -1,17 +1,18 @@
<div class="container">
<div class="row">
<div class="col-md-12">
<h2 id="header" class="pb-2">{{'submission.import-external.title' | translate}}</h2>
<h2 id="header" class="pb-2">{{'submission.import-external.title' + ((label) ? '.' + label : '') | translate}}</h2>
<ds-submission-import-external-searchbar
[initExternalSourceData]="routeData"
*ngIf="reload$.value.entity"
[initExternalSourceData]="reload$.value"
(externalSourceData) = "getExternalSourceData($event)">
</ds-submission-import-external-searchbar>
</div>
</div>
<div class="row">
<div *ngIf="routeData.sourceId !== ''" class="col-md-12">
<div class="row" *ngIf="reload$.value.entity">
<div *ngIf="reload$.value.sourceId !== ''" class="col-md-12">
<ng-container *ngVar="(entriesRD$ | async) as entriesRD">
<h3 *ngIf="entriesRD && entriesRD?.payload?.page?.length !== 0">{{ 'submission.sections.describe.relationship-lookup.selection-tab.title.' + routeData.sourceId | translate}}</h3>
<h3 *ngIf="entriesRD && entriesRD?.payload?.page?.length !== 0">{{ 'submission.sections.describe.relationship-lookup.selection-tab.title' | translate}}</h3>
<ds-viewable-collection *ngIf="entriesRD?.hasSucceeded && !(isLoading$ | async) && entriesRD?.payload?.page?.length > 0" @fadeIn
[objects]="entriesRD"
[selectionConfig]="{ repeatable: repeatable, listId: listId }"
@@ -29,12 +30,19 @@
</div>
</ng-container>
</div>
<div *ngIf="routeData.sourceId === ''" class="col-md-12">
<div *ngIf="reload$.value.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" *ngIf="!reload$.value.entity">
<div class="col-md-12">
<ds-alert [type]="'alert-warning'">
<p class="lead mb-0">{{'submission.import-external.page.noentity' | translate}}</p>
</ds-alert>
</div>
</div>
<div class="row">
<div class="col-md-12">
<hr>

View File

@@ -1,5 +1,5 @@
import { Component, NO_ERRORS_SCHEMA } from '@angular/core';
import { waitForAsync, ComponentFixture, inject, TestBed } from '@angular/core/testing';
import { ComponentFixture, inject, TestBed, waitForAsync } from '@angular/core/testing';
import { getTestScheduler } from 'jasmine-marbles';
import { TranslateModule } from '@ngx-translate/core';
@@ -102,17 +102,19 @@ describe('SubmissionImportExternalComponent test suite', () => {
it('Should init component properly (without route data)', () => {
const expectedEntries = createSuccessfulRemoteDataObject(createPaginatedList([]));
comp.routeData = {entity: '', sourceId: '', query: '' };
spyOn(compAsAny.routeService, 'getQueryParameterValue').and.returnValue(observableOf(''));
fixture.detectChanges();
expect(comp.routeData).toEqual({ sourceId: '', query: '' });
expect(comp.routeData).toEqual({entity: '', sourceId: '', query: '' });
expect(comp.isLoading$.value).toBe(false);
expect(comp.entriesRD$.value).toEqual(expectedEntries);
});
it('Should init component properly (with route data)', () => {
comp.routeData = {entity: '', sourceId: '', query: '' };
spyOn(compAsAny, 'retrieveExternalSources');
spyOn(compAsAny.routeService, 'getQueryParameterValue').and.returnValues(observableOf('source'), observableOf('dummy'));
spyOn(compAsAny.routeService, 'getQueryParameterValue').and.returnValues(observableOf('entity'), observableOf('source'), observableOf('dummy'));
fixture.detectChanges();
expect(compAsAny.retrieveExternalSources).toHaveBeenCalled();
@@ -120,7 +122,7 @@ describe('SubmissionImportExternalComponent test suite', () => {
it('Should call \'getExternalSourceEntries\' properly', () => {
spyOn(routeServiceStub, 'getQueryParameterValue').and.callFake((param) => {
if (param === 'source') {
if (param === 'sourceId') {
return observableOf('orcidV2');
} else if (param === 'query') {
return observableOf('test');
@@ -136,15 +138,15 @@ describe('SubmissionImportExternalComponent test suite', () => {
});
it('Should call \'router.navigate\'', () => {
comp.routeData = { sourceId: '', query: '' };
comp.routeData = {entity: 'Person', sourceId: '', query: '' };
spyOn(compAsAny, 'retrieveExternalSources').and.callFake(() => null);
compAsAny.router.navigate.and.returnValue( new Promise(() => {return;}));
const event = { sourceId: 'orcidV2', query: 'dummy' };
const event = {entity: 'Person', sourceId: 'orcidV2', query: 'dummy' };
scheduler.schedule(() => comp.getExternalSourceData(event));
scheduler.flush();
expect(compAsAny.router.navigate).toHaveBeenCalledWith([], { queryParams: { source: event.sourceId, query: event.query }, replaceUrl: true });
expect(compAsAny.router.navigate).toHaveBeenCalledWith([], { queryParams: { entity: event.entity, sourceId: event.sourceId, query: event.query }, replaceUrl: true });
});
it('Entry should be passed to the component loaded inside the modal', () => {
@@ -166,6 +168,13 @@ describe('SubmissionImportExternalComponent test suite', () => {
expect(compAsAny.modalService.open).toHaveBeenCalledWith(SubmissionImportExternalPreviewComponent, { size: 'lg' });
expect(comp.modalRef.componentInstance.externalSourceEntry).toEqual(entry);
});
it('Should set the correct label', () => {
const label = 'Person';
compAsAny.selectLabel(label);
expect(comp.label).toEqual(label);
});
});
});

View File

@@ -45,9 +45,10 @@ export class SubmissionImportExternalComponent implements OnInit, OnDestroy {
*/
public isLoading$: BehaviorSubject<boolean> = new BehaviorSubject<boolean>(false);
public reload$: BehaviorSubject<{ query: string, source: string }> = new BehaviorSubject<{ query: string; source: string }>({
public reload$: BehaviorSubject<ExternalSourceData> = new BehaviorSubject<ExternalSourceData>({
entity: '',
query: '',
source: ''
sourceId: ''
});
/**
* Configuration to use for the import buttons
@@ -109,11 +110,10 @@ export class SubmissionImportExternalComponent implements OnInit, OnDestroy {
* 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.routeData = {entity: '', sourceId: '', query: ''};
this.importConfig = {
buttonLabel: 'submission.sections.describe.relationship-lookup.external-source.import-button-title.' + this.label
};
@@ -121,12 +121,14 @@ export class SubmissionImportExternalComponent implements OnInit, OnDestroy {
this.isLoading$ = new BehaviorSubject(false);
this.subs.push(combineLatest(
[
this.routeService.getQueryParameterValue('source'),
this.routeService.getQueryParameterValue('entity'),
this.routeService.getQueryParameterValue('sourceId'),
this.routeService.getQueryParameterValue('query')
]).pipe(
take(1)
).subscribe(([source, query]: [string, string]) => {
this.reload$.next({query: query, source: source});
).subscribe(([entity, sourceId, query]: [string, string, string]) => {
this.reload$.next({entity: entity, query: query, sourceId: sourceId});
this.selectLabel(entity);
this.retrieveExternalSources();
}));
}
@@ -138,11 +140,11 @@ export class SubmissionImportExternalComponent implements OnInit, OnDestroy {
this.router.navigate(
[],
{
queryParams: {source: event.sourceId, query: event.query},
queryParams: event,
replaceUrl: true
}
).then(() => {
this.reload$.next({source: event.sourceId, query: event.query});
this.reload$.next(event);
this.retrieveExternalSources();
});
}
@@ -157,6 +159,7 @@ export class SubmissionImportExternalComponent implements OnInit, OnDestroy {
});
const modalComp = this.modalRef.componentInstance;
modalComp.externalSourceEntry = entry;
modalComp.labelPrefix = this.label;
}
/**
@@ -173,22 +176,17 @@ export class SubmissionImportExternalComponent implements OnInit, OnDestroy {
}
/**
* Retrieve external source entries
*
* @param source The source tupe
* @param query The query string to search
* Retrieve external source entries.
*/
private retrieveExternalSources(): void {
if (hasValue(this.retrieveExternalSourcesSub)) {
/* if (hasValue(this.retrieveExternalSourcesSub)) {
this.retrieveExternalSourcesSub.unsubscribe();
}
}*/
this.retrieveExternalSourcesSub = this.reload$.pipe(
filter((sourceQueryObject: { source: string, query: string }) => isNotEmpty(sourceQueryObject.source) && isNotEmpty(sourceQueryObject.query)),
switchMap((sourceQueryObject: { source: string, query: string }) => {
const source = sourceQueryObject.source;
filter((sourceQueryObject: ExternalSourceData) => isNotEmpty(sourceQueryObject.sourceId) && isNotEmpty(sourceQueryObject.query)),
switchMap((sourceQueryObject: ExternalSourceData) => {
const query = sourceQueryObject.query;
this.routeData.sourceId = source;
this.routeData.query = query;
this.routeData = sourceQueryObject;
return this.searchConfigService.paginatedSearchOptions.pipe(
tap((v) => this.isLoading$.next(true)),
filter((searchOptions) => searchOptions.query === query),
@@ -204,4 +202,16 @@ export class SubmissionImportExternalComponent implements OnInit, OnDestroy {
});
}
/**
* Set the correct button label, depending on the entity.
*
* @param entity The entity name
*/
private selectLabel(entity: string): void {
this.label = entity;
this.importConfig = {
buttonLabel: 'submission.sections.describe.relationship-lookup.external-source.import-button-title.' + this.label
};
}
}

View File

@@ -1254,7 +1254,7 @@
"file-section.error.header": "Error obtaining files for this item",
"footer.copyright": "copyright © 2002-{{ year }}",
@@ -3211,8 +3211,24 @@
"submission.import-external.title": "Import metadata from an external source",
"submission.import-external.title.Journal": "Import a journal from an external source",
"submission.import-external.title.JournalIssue": "Import a journal issue from an external source",
"submission.import-external.title.JournalVolume": "Import a journal volume from an external source",
"submission.import-external.title.OrgUnit": "Import a publisher from an external source",
"submission.import-external.title.Person": "Import a person from an external source",
"submission.import-external.title.Project": "Import a project from an external source",
"submission.import-external.title.Publication": "Import a publication 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.page.noentity": "An entity type must be selected in order to use this page. Select it using the 'Import metadata from an external source' button in 'MyDspace' page.",
"submission.import-external.back-to-my-dspace": "Back to MyDSpace",
"submission.import-external.search.placeholder": "Search the external source",