Merge pull request #1332 from 4Science/CST-4510-entity-selection-porting

Assign an entity type to a collection and map external providers
This commit is contained in:
Tim Donohue
2021-10-21 10:05:52 -05:00
committed by GitHub
50 changed files with 1883 additions and 282 deletions

View File

@@ -1,18 +1,27 @@
import { Component, Input } from '@angular/core'; import { Component, Input, OnInit } from '@angular/core';
import { Observable } from 'rxjs';
import { TranslateService } from '@ngx-translate/core';
import { import {
DynamicFormControlModel, DynamicFormControlModel,
DynamicFormOptionConfig,
DynamicFormService, DynamicFormService,
DynamicInputModel, DynamicSelectModel
DynamicTextAreaModel
} from '@ng-dynamic-forms/core'; } from '@ng-dynamic-forms/core';
import { Collection } from '../../core/shared/collection.model'; import { Collection } from '../../core/shared/collection.model';
import { ComColFormComponent } from '../../shared/comcol-forms/comcol-form/comcol-form.component'; import { ComColFormComponent } from '../../shared/comcol-forms/comcol-form/comcol-form.component';
import { TranslateService } from '@ngx-translate/core';
import { NotificationsService } from '../../shared/notifications/notifications.service'; import { NotificationsService } from '../../shared/notifications/notifications.service';
import { CommunityDataService } from '../../core/data/community-data.service'; import { CommunityDataService } from '../../core/data/community-data.service';
import { AuthService } from '../../core/auth/auth.service'; import { AuthService } from '../../core/auth/auth.service';
import { RequestService } from '../../core/data/request.service'; import { RequestService } from '../../core/data/request.service';
import { ObjectCacheService } from '../../core/cache/object-cache.service'; import { ObjectCacheService } from '../../core/cache/object-cache.service';
import { EntityTypeService } from '../../core/data/entity-type.service';
import { ItemType } from '../../core/shared/item-relationships/item-type.model';
import { MetadataValue } from '../../core/shared/metadata.models';
import { getFirstSucceededRemoteListPayload } from '../../core/shared/operators';
import { collectionFormEntityTypeSelectionConfig, collectionFormModels, } from './collection-form.models';
import { NONE_ENTITY_TYPE } from '../../core/shared/item-relationships/item-type.resource-type';
/** /**
* Form used for creating and editing collections * Form used for creating and editing collections
@@ -22,7 +31,7 @@ import { ObjectCacheService } from '../../core/cache/object-cache.service';
styleUrls: ['../../shared/comcol-forms/comcol-form/comcol-form.component.scss'], styleUrls: ['../../shared/comcol-forms/comcol-form/comcol-form.component.scss'],
templateUrl: '../../shared/comcol-forms/comcol-form/comcol-form.component.html' templateUrl: '../../shared/comcol-forms/comcol-form/comcol-form.component.html'
}) })
export class CollectionFormComponent extends ComColFormComponent<Collection> { export class CollectionFormComponent extends ComColFormComponent<Collection> implements OnInit {
/** /**
* @type {Collection} A new collection when a collection is being created, an existing Input collection when a collection is being edited * @type {Collection} A new collection when a collection is being created, an existing Input collection when a collection is being edited
*/ */
@@ -34,46 +43,16 @@ export class CollectionFormComponent extends ComColFormComponent<Collection> {
type = Collection.type; type = Collection.type;
/** /**
* The dynamic form fields used for creating/editing a collection * The dynamic form field used for entity type selection
* @type {(DynamicInputModel | DynamicTextAreaModel)[]} * @type {DynamicSelectModel<string>}
*/ */
formModel: DynamicFormControlModel[] = [ entityTypeSelection: DynamicSelectModel<string> = new DynamicSelectModel(collectionFormEntityTypeSelectionConfig);
new DynamicInputModel({
id: 'title', /**
name: 'dc.title', * The dynamic form fields used for creating/editing a collection
required: true, * @type {DynamicFormControlModel[]}
validators: { */
required: null formModel: DynamicFormControlModel[];
},
errorMessages: {
required: 'Please enter a name for this title'
},
}),
new DynamicTextAreaModel({
id: 'description',
name: 'dc.description',
}),
new DynamicTextAreaModel({
id: 'abstract',
name: 'dc.description.abstract',
}),
new DynamicTextAreaModel({
id: 'rights',
name: 'dc.rights',
}),
new DynamicTextAreaModel({
id: 'tableofcontents',
name: 'dc.description.tableofcontents',
}),
new DynamicTextAreaModel({
id: 'license',
name: 'dc.rights.license',
}),
new DynamicTextAreaModel({
id: 'provenance',
name: 'dc.description.provenance',
}),
];
public constructor(protected formService: DynamicFormService, public constructor(protected formService: DynamicFormService,
protected translate: TranslateService, protected translate: TranslateService,
@@ -81,7 +60,43 @@ export class CollectionFormComponent extends ComColFormComponent<Collection> {
protected authService: AuthService, protected authService: AuthService,
protected dsoService: CommunityDataService, protected dsoService: CommunityDataService,
protected requestService: RequestService, protected requestService: RequestService,
protected objectCache: ObjectCacheService) { protected objectCache: ObjectCacheService,
protected entityTypeService: EntityTypeService) {
super(formService, translate, notificationsService, authService, requestService, objectCache); super(formService, translate, notificationsService, authService, requestService, objectCache);
} }
ngOnInit() {
let currentRelationshipValue: MetadataValue[];
if (this.dso && this.dso.metadata) {
currentRelationshipValue = this.dso.metadata['dspace.entity.type'];
}
const entities$: Observable<ItemType[]> = this.entityTypeService.findAll({ elementsPerPage: 100, currentPage: 1 }).pipe(
getFirstSucceededRemoteListPayload()
);
// retrieve all entity types to populate the dropdowns selection
entities$.subscribe((entityTypes: ItemType[]) => {
entityTypes
.filter((type: ItemType) => type.label !== NONE_ENTITY_TYPE)
.forEach((type: ItemType, index: number) => {
this.entityTypeSelection.add({
disabled: false,
label: type.label,
value: type.label
} as DynamicFormOptionConfig<string>);
if (currentRelationshipValue && currentRelationshipValue.length > 0 && currentRelationshipValue[0].value === type.label) {
this.entityTypeSelection.select(index);
this.entityTypeSelection.disabled = true;
}
});
this.formModel = [...collectionFormModels, this.entityTypeSelection];
super.ngOnInit();
});
}
} }

View File

@@ -0,0 +1,46 @@
import { DynamicFormControlModel, DynamicInputModel, DynamicTextAreaModel } from '@ng-dynamic-forms/core';
import { DynamicSelectModelConfig } from '@ng-dynamic-forms/core/lib/model/select/dynamic-select.model';
export const collectionFormEntityTypeSelectionConfig: DynamicSelectModelConfig<string> = {
id: 'entityType',
name: 'dspace.entity.type',
disabled: false
};
/**
* The dynamic form fields used for creating/editing a collection
* @type {(DynamicInputModel | DynamicTextAreaModel)[]}
*/
export const collectionFormModels: DynamicFormControlModel[] = [
new DynamicInputModel({
id: 'title',
name: 'dc.title',
required: true,
validators: {
required: null
},
errorMessages: {
required: 'Please enter a name for this title'
},
}),
new DynamicTextAreaModel({
id: 'description',
name: 'dc.description',
}),
new DynamicTextAreaModel({
id: 'abstract',
name: 'dc.description.abstract',
}),
new DynamicTextAreaModel({
id: 'rights',
name: 'dc.rights',
}),
new DynamicTextAreaModel({
id: 'tableofcontents',
name: 'dc.description.tableofcontents',
}),
new DynamicTextAreaModel({
id: 'license',
name: 'dc.rights.license',
})
];

View File

@@ -17,7 +17,7 @@ import {
createFailedRemoteDataObject$, createFailedRemoteDataObject$,
createSuccessfulRemoteDataObject, createSuccessfulRemoteDataObject,
createSuccessfulRemoteDataObject$ createSuccessfulRemoteDataObject$
} from 'src/app/shared/remote-data.utils'; } from '../../shared/remote-data.utils';
import { cold, getTestScheduler, hot } from 'jasmine-marbles'; import { cold, getTestScheduler, hot } from 'jasmine-marbles';
import { TestScheduler } from 'rxjs/testing'; import { TestScheduler } from 'rxjs/testing';
import { Observable } from 'rxjs'; import { Observable } from 'rxjs';

View File

@@ -27,12 +27,7 @@ import { CommunityDataService } from './community-data.service';
import { DSOChangeAnalyzer } from './dso-change-analyzer.service'; import { DSOChangeAnalyzer } from './dso-change-analyzer.service';
import { PaginatedList } from './paginated-list.model'; import { PaginatedList } from './paginated-list.model';
import { RemoteData } from './remote-data'; import { RemoteData } from './remote-data';
import { import { ContentSourceRequest, FindListOptions, RestRequest, UpdateContentSourceRequest } from './request.models';
ContentSourceRequest,
FindListOptions,
UpdateContentSourceRequest,
RestRequest
} from './request.models';
import { RequestService } from './request.service'; import { RequestService } from './request.service';
import { BitstreamDataService } from './bitstream-data.service'; import { BitstreamDataService } from './bitstream-data.service';
@@ -84,16 +79,48 @@ export class CollectionDataService extends ComColDataService<Collection> {
filter((collections: RemoteData<PaginatedList<Collection>>) => !collections.isResponsePending)); filter((collections: RemoteData<PaginatedList<Collection>>) => !collections.isResponsePending));
} }
/**
* Get all collections the user is authorized to submit to
*
* @param query limit the returned collection to those with metadata values matching the query terms.
* @param entityType The entity type used to limit the returned collection
* @param options The [[FindListOptions]] object
* @param reRequestOnStale Whether or not the request should automatically be re-requested after
* the response becomes stale
* @param linksToFollow The array of [[FollowLinkConfig]]
* @return Observable<RemoteData<PaginatedList<Collection>>>
* collection list
*/
getAuthorizedCollectionByEntityType(
query: string,
entityType: string,
options: FindListOptions = {},
reRequestOnStale = true,
...linksToFollow: FollowLinkConfig<Collection>[]): Observable<RemoteData<PaginatedList<Collection>>> {
const searchHref = 'findSubmitAuthorizedByEntityType';
options = Object.assign({}, options, {
searchParams: [
new RequestParam('query', query),
new RequestParam('entityType', entityType)
]
});
return this.searchBy(searchHref, options, true, reRequestOnStale, ...linksToFollow).pipe(
filter((collections: RemoteData<PaginatedList<Collection>>) => !collections.isResponsePending));
}
/** /**
* Get all collections the user is authorized to submit to, by community * Get all collections the user is authorized to submit to, by community
* *
* @param communityId The community id * @param communityId The community id
* @param query limit the returned collection to those with metadata values matching the query terms. * @param query limit the returned collection to those with metadata values matching the query terms.
* @param options The [[FindListOptions]] object * @param options The [[FindListOptions]] object
* @param reRequestOnStale Whether or not the request should automatically be re-
* requested after the response becomes stale
* @return Observable<RemoteData<PaginatedList<Collection>>> * @return Observable<RemoteData<PaginatedList<Collection>>>
* collection list * collection list
*/ */
getAuthorizedCollectionByCommunity(communityId: string, query: string, options: FindListOptions = {}): Observable<RemoteData<PaginatedList<Collection>>> { getAuthorizedCollectionByCommunity(communityId: string, query: string, options: FindListOptions = {}, reRequestOnStale = true,): Observable<RemoteData<PaginatedList<Collection>>> {
const searchHref = 'findSubmitAuthorizedByCommunity'; const searchHref = 'findSubmitAuthorizedByCommunity';
options = Object.assign({}, options, { options = Object.assign({}, options, {
searchParams: [ searchParams: [
@@ -102,7 +129,38 @@ export class CollectionDataService extends ComColDataService<Collection> {
] ]
}); });
return this.searchBy(searchHref, options).pipe( return this.searchBy(searchHref, options, reRequestOnStale).pipe(
filter((collections: RemoteData<PaginatedList<Collection>>) => !collections.isResponsePending));
}
/**
* Get all collections the user is authorized to submit to, by community and has the metadata
*
* @param communityId The community id
* @param entityType The entity type used to limit the returned collection
* @param options The [[FindListOptions]] object
* @param reRequestOnStale Whether or not the request should automatically be re-requested after
* the response becomes stale
* @param linksToFollow The array of [[FollowLinkConfig]]
* @return Observable<RemoteData<PaginatedList<Collection>>>
* collection list
*/
getAuthorizedCollectionByCommunityAndEntityType(
communityId: string,
entityType: string,
options: FindListOptions = {},
reRequestOnStale = true,
...linksToFollow: FollowLinkConfig<Collection>[]): Observable<RemoteData<PaginatedList<Collection>>> {
const searchHref = 'findSubmitAuthorizedByCommunityAndEntityType';
const searchParams = [
new RequestParam('uuid', communityId),
new RequestParam('entityType', entityType)
];
options = Object.assign({}, options, {
searchParams: searchParams
});
return this.searchBy(searchHref, options, true, reRequestOnStale, ...linksToFollow).pipe(
filter((collections: RemoteData<PaginatedList<Collection>>) => !collections.isResponsePending)); filter((collections: RemoteData<PaginatedList<Collection>>) => !collections.isResponsePending));
} }

View File

@@ -10,13 +10,14 @@ import { NotificationsService } from '../../shared/notifications/notifications.s
import { HttpClient } from '@angular/common/http'; import { HttpClient } from '@angular/common/http';
import { DefaultChangeAnalyzer } from './default-change-analyzer.service'; import { DefaultChangeAnalyzer } from './default-change-analyzer.service';
import { Injectable } from '@angular/core'; import { Injectable } from '@angular/core';
import { FindListOptions } from './request.models';
import { Observable } from 'rxjs'; import { Observable } from 'rxjs';
import { switchMap, take, map } from 'rxjs/operators'; import { filter, map, switchMap, take } from 'rxjs/operators';
import { RemoteData } from './remote-data'; import { RemoteData } from './remote-data';
import { RelationshipType } from '../shared/item-relationships/relationship-type.model'; import { RelationshipType } from '../shared/item-relationships/relationship-type.model';
import { PaginatedList } from './paginated-list.model'; import { PaginatedList } from './paginated-list.model';
import { ItemType } from '../shared/item-relationships/item-type.model'; import { ItemType } from '../shared/item-relationships/item-type.model';
import { getRemoteDataPayload, getFirstSucceededRemoteData } from '../shared/operators'; import { getFirstSucceededRemoteData, getRemoteDataPayload } from '../shared/operators';
import { RelationshipTypeService } from './relationship-type.service'; import { RelationshipTypeService } from './relationship-type.service';
/** /**
@@ -56,7 +57,7 @@ export class EntityTypeService extends DataService<ItemType> {
/** /**
* Check whether a given entity type is the left type of a given relationship type, as an observable boolean * Check whether a given entity type is the left type of a given relationship type, as an observable boolean
* @param relationshipType the relationship type for which to check whether the given entity type is the left type * @param relationshipType the relationship type for which to check whether the given entity type is the left type
* @param entityType the entity type for which to check whether it is the left type of the given relationship type * @param itemType the entity type for which to check whether it is the left type of the given relationship type
*/ */
isLeftType(relationshipType: RelationshipType, itemType: ItemType): Observable<boolean> { isLeftType(relationshipType: RelationshipType, itemType: ItemType): Observable<boolean> {
@@ -67,6 +68,73 @@ export class EntityTypeService extends DataService<ItemType> {
); );
} }
/**
* Returns a list of entity types for which there is at least one collection in which the user is authorized to submit
*
* @param {FindListOptions} options
*/
getAllAuthorizedRelationshipType(options: FindListOptions = {}): Observable<RemoteData<PaginatedList<ItemType>>> {
const searchHref = 'findAllByAuthorizedCollection';
return this.searchBy(searchHref, options).pipe(
filter((type: RemoteData<PaginatedList<ItemType>>) => !type.isResponsePending));
}
/**
* Used to verify if there are one or more entities available
*/
hasMoreThanOneAuthorized(): Observable<boolean> {
const findListOptions: FindListOptions = {
elementsPerPage: 2,
currentPage: 1
};
return this.getAllAuthorizedRelationshipType(findListOptions).pipe(
map((result: RemoteData<PaginatedList<ItemType>>) => {
let output: boolean;
if (result.payload) {
output = ( result.payload.page.length > 1 );
} else {
output = false;
}
return output;
})
);
}
/**
* It returns a list of entity types for which there is at least one collection
* in which the user is authorized to submit supported by at least one external data source provider
*
* @param {FindListOptions} options
*/
getAllAuthorizedRelationshipTypeImport(options: FindListOptions = {}): Observable<RemoteData<PaginatedList<ItemType>>> {
const searchHref = 'findAllByAuthorizedExternalSource';
return this.searchBy(searchHref, options).pipe(
filter((type: RemoteData<PaginatedList<ItemType>>) => !type.isResponsePending));
}
/**
* Used to verify if there are one or more entities available. To use with external source import.
*/
hasMoreThanOneAuthorizedImport(): Observable<boolean> {
const findListOptions: FindListOptions = {
elementsPerPage: 2,
currentPage: 1
};
return this.getAllAuthorizedRelationshipTypeImport(findListOptions).pipe(
map((result: RemoteData<PaginatedList<ItemType>>) => {
let output: boolean;
if (result.payload) {
output = ( result.payload.page.length > 1 );
} else {
output = false;
}
return output;
})
);
}
/** /**
* Get the allowed relationship types for an entity type * Get the allowed relationship types for an entity type
* @param entityTypeId * @param entityTypeId

View File

@@ -1,10 +1,15 @@
import { autoserialize, deserialize } from 'cerialize'; import { autoserialize, deserialize } from 'cerialize';
import { typedObject } from '../cache/builders/build-decorators'; import { link, typedObject } from '../cache/builders/build-decorators';
import { CacheableObject } from '../cache/object-cache.reducer'; import { CacheableObject } from '../cache/object-cache.reducer';
import { excludeFromEquals } from '../utilities/equals.decorators'; import { excludeFromEquals } from '../utilities/equals.decorators';
import { EXTERNAL_SOURCE } from './external-source.resource-type'; import { EXTERNAL_SOURCE } from './external-source.resource-type';
import { HALLink } from './hal-link.model'; import { HALLink } from './hal-link.model';
import { ResourceType } from './resource-type'; import { ResourceType } from './resource-type';
import { RemoteData } from '../data/remote-data';
import { PaginatedList } from '../data/paginated-list.model';
import { Observable } from 'rxjs/internal/Observable';
import { ITEM_TYPE } from './item-relationships/item-type.resource-type';
import { ItemType } from './item-relationships/item-type.model';
/** /**
* Model class for an external source * Model class for an external source
@@ -38,6 +43,13 @@ export class ExternalSource extends CacheableObject {
@autoserialize @autoserialize
hierarchical: boolean; hierarchical: boolean;
/**
* The list of entity types that are compatible with this external source
* Will be undefined unless the entityTypes {@link HALLink} has been resolved.
*/
@link(ITEM_TYPE, true)
entityTypes?: Observable<RemoteData<PaginatedList<ItemType>>>;
/** /**
* The {@link HALLink}s for this ExternalSource * The {@link HALLink}s for this ExternalSource
*/ */
@@ -45,5 +57,6 @@ export class ExternalSource extends CacheableObject {
_links: { _links: {
self: HALLink; self: HALLink;
entries: HALLink; entries: HALLink;
entityTypes: HALLink;
}; };
} }

View File

@@ -6,5 +6,9 @@ import { ResourceType } from '../resource-type';
* Needs to be in a separate file to prevent circular * Needs to be in a separate file to prevent circular
* dependencies in webpack. * dependencies in webpack.
*/ */
export const ITEM_TYPE = new ResourceType('entitytype'); export const ITEM_TYPE = new ResourceType('entitytype');
/**
* The unset entity type
*/
export const NONE_ENTITY_TYPE = 'none';

View File

@@ -0,0 +1,24 @@
<div class="add" *ngIf="!(moreThanOne$ | async)">
<button class="btn btn-lg btn-outline-primary mt-1 ml-2" [disabled]="!(initialized$|async)"
(click)="openPage(singleEntity)" role="button"
title="{{'mydspace.new-submission-external' | translate}}">
<i class="fa fa-file-import" aria-hidden="true"></i>
</button>
</div>
<div class="add w-100" display="dynamic" placement="bottom-right"
ngbDropdown
*ngIf="(moreThanOne$ | async)">
<button class="btn btn-lg btn-outline-primary mt-1 ml-2" id="dropdownSubmission" ngbDropdownToggle
type="button" [disabled]="!(initialized$|async)"
attr.aria-label="{{'mydspace.new-submission-external' | translate}}"
title="{{'mydspace.new-submission-external' | translate}}">
<i class="fa fa-file-import" aria-hidden="true"></i>
<span class="caret"></span>
</button>
<div ngbDropdownMenu
class="dropdown-menu"
id="entityControlsDropdownMenu"
aria-labelledby="dropdownSubmission">
<ds-entity-dropdown [isSubmission]="false" (selectionChange)="openPage($event)"></ds-entity-dropdown>
</div>
</div>

View File

@@ -0,0 +1,16 @@
.parent {
display: flex;
}
.upload {
flex: auto;
}
.add {
flex: initial;
}
#entityControlsDropdownMenu {
min-width: 18rem;
box-shadow: $btn-focus-box-shadow;
}

View File

@@ -0,0 +1,189 @@
import { Component, DebugElement, NO_ERRORS_SCHEMA } from '@angular/core';
import { async, ComponentFixture, inject, TestBed } from '@angular/core/testing';
import { Router } from '@angular/router';
import { By } from '@angular/platform-browser';
import { CommonModule } from '@angular/common';
import { TranslateModule } from '@ngx-translate/core';
import { of as observableOf } from 'rxjs';
import { createPaginatedList, createTestComponent } from '../../../shared/testing/utils.test';
import { MyDSpaceNewExternalDropdownComponent } from './my-dspace-new-external-dropdown.component';
import { EntityTypeService } from '../../../core/data/entity-type.service';
import { ItemType } from '../../../core/shared/item-relationships/item-type.model';
import { ResourceType } from '../../../core/shared/resource-type';
import { createSuccessfulRemoteDataObject$ } from '../../../shared/remote-data.utils';
import { PageInfo } from '../../../core/shared/page-info.model';
import { RouterStub } from '../../../shared/testing/router.stub';
export function getMockEntityTypeService(): EntityTypeService {
const pageInfo = { elementsPerPage: 20, totalElements: 4, totalPages: 1, currentPage: 0 } as PageInfo;
const type1: ItemType = {
id: '1',
label: 'Publication',
uuid: '1',
type: new ResourceType('entitytype'),
_links: undefined
};
const type2: ItemType = {
id: '2',
label: 'Journal',
uuid: '2',
type: new ResourceType('entitytype'),
_links: undefined
};
const type3: ItemType = {
id: '2',
label: 'DataPackage',
uuid: '2',
type: new ResourceType('entitytype'),
_links: undefined
};
const rd$ = createSuccessfulRemoteDataObject$(createPaginatedList([type1, type2, type3]));
return jasmine.createSpyObj('entityTypeService', {
getAllAuthorizedRelationshipTypeImport: rd$,
hasMoreThanOneAuthorizedImport: observableOf(true)
});
}
export function getMockEmptyEntityTypeService(): EntityTypeService {
const pageInfo = { elementsPerPage: 20, totalElements: 1, totalPages: 1, currentPage: 0 } as PageInfo;
const type1: ItemType = {
id: '1',
label: 'Publication',
uuid: '1',
type: new ResourceType('entitytype'),
_links: undefined
};
const rd$ = createSuccessfulRemoteDataObject$(createPaginatedList([type1]));
return jasmine.createSpyObj('entityTypeService', {
getAllAuthorizedRelationshipTypeImport: rd$,
hasMoreThanOneAuthorizedImport: observableOf(false)
});
}
describe('MyDSpaceNewExternalDropdownComponent test', () => {
let testComp: TestComponent;
let testFixture: ComponentFixture<TestComponent>;
let submissionComponent: MyDSpaceNewExternalDropdownComponent;
let submissionComponentFixture: ComponentFixture<MyDSpaceNewExternalDropdownComponent>;
const entityType1: ItemType = {
id: '1',
label: 'Publication',
uuid: '1',
type: new ResourceType('entitytype'),
_links: undefined
};
describe('With only one Entity', () => {
beforeEach(async(() => {
TestBed.configureTestingModule({
imports: [
CommonModule,
TranslateModule.forRoot(),
],
declarations: [
MyDSpaceNewExternalDropdownComponent,
TestComponent
],
providers: [
{ provide: EntityTypeService, useValue: getMockEmptyEntityTypeService() },
{ provide: Router, useValue: new RouterStub() },
MyDSpaceNewExternalDropdownComponent
],
schemas: [NO_ERRORS_SCHEMA]
}).compileComponents();
const html = `<ds-my-dspace-new-submission (uploadEnd)="reload($event)"></ds-my-dspace-new-submission>`;
testFixture = createTestComponent(html, TestComponent) as ComponentFixture<TestComponent>;
testComp = testFixture.componentInstance;
submissionComponentFixture = TestBed.createComponent(MyDSpaceNewExternalDropdownComponent);
submissionComponent = submissionComponentFixture.componentInstance;
submissionComponentFixture.detectChanges();
}));
afterEach(() => {
testFixture.destroy();
submissionComponentFixture.destroy();
});
it('should create MyDSpaceNewExternalDropdownComponent', inject([MyDSpaceNewExternalDropdownComponent], (app: MyDSpaceNewExternalDropdownComponent) => {
expect(app).toBeDefined();
}));
it('should be a single button', inject([MyDSpaceNewExternalDropdownComponent], (app: MyDSpaceNewExternalDropdownComponent) => {
submissionComponentFixture.detectChanges();
const addDivElement: DebugElement = submissionComponentFixture.debugElement.query(By.css('.add'));
const addDiv = addDivElement.nativeElement;
expect(addDiv.innerHTML).toBeDefined();
const buttonElement: DebugElement = addDivElement.query(By.css('.btn'));
const button = buttonElement.nativeElement;
expect(button.innerHTML).toBeDefined();
const dropdownElement: DebugElement = submissionComponentFixture.debugElement.query(By.css('.dropdown-menu'));
expect(dropdownElement).toBeNull();
}));
});
describe('With more than one Entity', () => {
beforeEach(async(() => {
TestBed.configureTestingModule({
imports: [
CommonModule,
TranslateModule.forRoot(),
],
declarations: [
MyDSpaceNewExternalDropdownComponent,
TestComponent
],
providers: [
{ provide: EntityTypeService, useValue: getMockEntityTypeService() },
{ provide: Router, useValue: new RouterStub() },
MyDSpaceNewExternalDropdownComponent
],
schemas: [NO_ERRORS_SCHEMA]
}).compileComponents();
const html = `<ds-my-dspace-new-submission (uploadEnd)="reload($event)"></ds-my-dspace-new-submission>`;
testFixture = createTestComponent(html, TestComponent) as ComponentFixture<TestComponent>;
testComp = testFixture.componentInstance;
submissionComponentFixture = TestBed.createComponent(MyDSpaceNewExternalDropdownComponent);
submissionComponent = submissionComponentFixture.componentInstance;
submissionComponentFixture.detectChanges();
}));
afterEach(() => {
testFixture.destroy();
submissionComponentFixture.destroy();
});
it('should create MyDSpaceNewExternalDropdownComponent', inject([MyDSpaceNewExternalDropdownComponent], (app: MyDSpaceNewExternalDropdownComponent) => {
expect(app).toBeDefined();
}));
it('should be a dropdown button', inject([MyDSpaceNewExternalDropdownComponent], (app: MyDSpaceNewExternalDropdownComponent) => {
const dropdownElement: DebugElement = submissionComponentFixture.debugElement.query(By.css('.dropdown-menu'));
const dropdown = dropdownElement.nativeElement;
expect(dropdown.innerHTML).toBeDefined();
}));
it('should invoke modalService.open', () => {
submissionComponent.openPage(entityType1);
expect((submissionComponent as any).router.navigate).toHaveBeenCalled();
});
});
});
// declare a test component
@Component({
selector: 'ds-test-cmp',
template: ``
})
class TestComponent {
reload = (event) => {
return;
}
}

View File

@@ -0,0 +1,110 @@
import { Component, OnDestroy, OnInit } from '@angular/core';
import { Router } from '@angular/router';
import { Observable, of as observableOf, Subscription } from 'rxjs';
import { map, mergeMap, take } from 'rxjs/operators';
import { EntityTypeService } from '../../../core/data/entity-type.service';
import { ItemType } from '../../../core/shared/item-relationships/item-type.model';
import { FindListOptions } from '../../../core/data/request.models';
import { hasValue } from '../../../shared/empty.util';
import { RemoteData } from '../../../core/data/remote-data';
import { PaginatedList } from '../../../core/data/paginated-list.model';
/**
* This component represents the 'Import metadata from external source' dropdown menu
*/
@Component({
selector: 'ds-my-dspace-new-external-dropdown',
styleUrls: ['./my-dspace-new-external-dropdown.component.scss'],
templateUrl: './my-dspace-new-external-dropdown.component.html'
})
export class MyDSpaceNewExternalDropdownComponent implements OnInit, OnDestroy {
/**
* Used to verify if there are one or more entities available
*/
public moreThanOne$: Observable<boolean>;
/**
* The entity observble (only if there is only one entity available)
*/
public singleEntity$: Observable<ItemType>;
/**
* The entity object (only if there is only one entity available)
*/
public singleEntity: ItemType;
/**
* TRUE if the page is initialized
*/
public initialized$: Observable<boolean>;
/**
* Array to track all subscriptions and unsubscribe them onDestroy
* @type {Array}
*/
public subs: Subscription[] = [];
/**
* Initialize instance variables
*
* @param {EntityTypeService} entityTypeService
* @param {Router} router
*/
constructor(private entityTypeService: EntityTypeService,
private router: Router) { }
/**
* Initialize entity type list
*/
ngOnInit() {
this.initialized$ = observableOf(false);
this.moreThanOne$ = this.entityTypeService.hasMoreThanOneAuthorizedImport();
this.singleEntity$ = this.moreThanOne$.pipe(
mergeMap((response: boolean) => {
if (!response) {
const findListOptions: FindListOptions = {
elementsPerPage: 1,
currentPage: 1
};
return this.entityTypeService.getAllAuthorizedRelationshipTypeImport(findListOptions).pipe(
map((entities: RemoteData<PaginatedList<ItemType>>) => {
this.initialized$ = observableOf(true);
return entities.payload.page[0];
}),
take(1)
);
} else {
this.initialized$ = observableOf(true);
return observableOf(null);
}
}),
take(1)
);
this.subs.push(
this.singleEntity$.subscribe((result) => this.singleEntity = result )
);
}
/**
* Method called on clicking the button 'Import metadata from external source'. It opens the page of the external import.
*/
openPage(entity: ItemType) {
const params = Object.create({});
if (entity) {
params.entity = entity.label;
}
this.router.navigate(['/import-external'], { queryParams: params });
}
/**
* Unsubscribe from the subscription
*/
ngOnDestroy(): void {
this.subs
.filter((subscription) => hasValue(subscription))
.forEach((subscription) => subscription.unsubscribe());
}
}

View File

@@ -0,0 +1,20 @@
<div class="add" *ngIf="!(moreThanOne$ | async)">
<button class="btn btn-lg btn-primary mt-1 ml-2" [disabled]="!(initialized$|async)" (click)="openDialog(singleEntity)" role="button">
<i class="fa fa-plus-circle" aria-hidden="true"></i>
</button>
</div>
<div class="add w-100" display="dynamic" placement="bottom-right"
ngbDropdown
*ngIf="(moreThanOne$ | async)">
<button class="btn btn-lg btn-primary mt-1 ml-2" id="dropdownSubmission" ngbDropdownToggle
type="button" [disabled]="!(initialized$|async)">
<i class="fa fa-plus-circle" aria-hidden="true"></i>
<span class="caret"></span>
</button>
<div ngbDropdownMenu
class="dropdown-menu"
id="entityControlsDropdownMenu"
aria-labelledby="dropdownSubmission">
<ds-entity-dropdown [isSubmission]="true" (selectionChange)="openDialog($event)"></ds-entity-dropdown>
</div>
</div>

View File

@@ -0,0 +1,16 @@
.parent {
display: flex;
}
.upload {
flex: auto;
}
.add {
flex: initial;
}
#entityControlsDropdownMenu {
min-width: 18rem;
box-shadow: $btn-focus-box-shadow;
}

View File

@@ -0,0 +1,194 @@
import { Component, DebugElement, NO_ERRORS_SCHEMA } from '@angular/core';
import { async, ComponentFixture, inject, TestBed } from '@angular/core/testing';
import { By } from '@angular/platform-browser';
import { CommonModule } from '@angular/common';
import { TranslateModule } from '@ngx-translate/core';
import { NgbModal } from '@ng-bootstrap/ng-bootstrap';
import { of as observableOf } from 'rxjs';
import { createPaginatedList, createTestComponent } from '../../../shared/testing/utils.test';
import { MyDSpaceNewSubmissionDropdownComponent } from './my-dspace-new-submission-dropdown.component';
import { EntityTypeService } from '../../../core/data/entity-type.service';
import { ItemType } from '../../../core/shared/item-relationships/item-type.model';
import { ResourceType } from '../../../core/shared/resource-type';
import { createSuccessfulRemoteDataObject$ } from '../../../shared/remote-data.utils';
import { PageInfo } from '../../../core/shared/page-info.model';
export function getMockEntityTypeService(): EntityTypeService {
const type1: ItemType = {
id: '1',
label: 'Publication',
uuid: '1',
type: new ResourceType('entitytype'),
_links: undefined
};
const type2: ItemType = {
id: '2',
label: 'Journal',
uuid: '2',
type: new ResourceType('entitytype'),
_links: undefined
};
const type3: ItemType = {
id: '2',
label: 'DataPackage',
uuid: '2',
type: new ResourceType('entitytype'),
_links: undefined
};
const rd$ = createSuccessfulRemoteDataObject$(createPaginatedList([type1, type2, type3]));
return jasmine.createSpyObj('entityTypeService', {
getAllAuthorizedRelationshipType: rd$,
hasMoreThanOneAuthorized: observableOf(true)
});
}
export function getMockEmptyEntityTypeService(): EntityTypeService {
const pageInfo = { elementsPerPage: 20, totalElements: 1, totalPages: 1, currentPage: 0 } as PageInfo;
const type1: ItemType = {
id: '1',
label: 'Publication',
uuid: '1',
type: new ResourceType('entitytype'),
_links: undefined
};
const rd$ = createSuccessfulRemoteDataObject$(createPaginatedList([type1]));
return jasmine.createSpyObj('entityTypeService', {
getAllAuthorizedRelationshipType: rd$,
hasMoreThanOneAuthorized: observableOf(false)
});
}
describe('MyDSpaceNewSubmissionDropdownComponent test', () => {
let testComp: TestComponent;
let testFixture: ComponentFixture<TestComponent>;
let submissionComponent: MyDSpaceNewSubmissionDropdownComponent;
let submissionComponentFixture: ComponentFixture<MyDSpaceNewSubmissionDropdownComponent>;
const entityType1: ItemType = {
id: '1',
label: 'Publication',
uuid: '1',
type: new ResourceType('entitytype'),
_links: undefined
};
const modalStub = {
open: () => null,
close: () => null,
dismiss: () => null
};
describe('With only one Entity', () => {
beforeEach(async(() => {
TestBed.configureTestingModule({
imports: [
CommonModule,
TranslateModule.forRoot(),
],
declarations: [
MyDSpaceNewSubmissionDropdownComponent,
TestComponent
],
providers: [
{ provide: EntityTypeService, useValue: getMockEmptyEntityTypeService() },
{ provide: NgbModal, useValue: modalStub },
MyDSpaceNewSubmissionDropdownComponent
],
schemas: [NO_ERRORS_SCHEMA]
}).compileComponents();
const html = `<ds-my-dspace-new-submission (uploadEnd)="reload($event)"></ds-my-dspace-new-submission>`;
testFixture = createTestComponent(html, TestComponent) as ComponentFixture<TestComponent>;
testComp = testFixture.componentInstance;
submissionComponentFixture = TestBed.createComponent(MyDSpaceNewSubmissionDropdownComponent);
submissionComponent = submissionComponentFixture.componentInstance;
submissionComponentFixture.detectChanges();
}));
afterEach(() => {
testFixture.destroy();
submissionComponentFixture.destroy();
});
it('should create MyDSpaceNewSubmissionDropdownComponent', inject([MyDSpaceNewSubmissionDropdownComponent], (app: MyDSpaceNewSubmissionDropdownComponent) => {
expect(app).toBeDefined();
}));
it('should be a single button', inject([MyDSpaceNewSubmissionDropdownComponent], (app: MyDSpaceNewSubmissionDropdownComponent) => {
submissionComponentFixture.detectChanges();
const addDivElement: DebugElement = submissionComponentFixture.debugElement.query(By.css('.add'));
const addDiv = addDivElement.nativeElement;
expect(addDiv.innerHTML).toBeDefined();
const buttonElement: DebugElement = addDivElement.query(By.css('.btn'));
const button = buttonElement.nativeElement;
expect(button.innerHTML).toBeDefined();
const dropdownElement: DebugElement = submissionComponentFixture.debugElement.query(By.css('.dropdown-menu'));
expect(dropdownElement).toBeNull();
}));
});
describe('With more than one Entity', () => {
beforeEach(async(() => {
TestBed.configureTestingModule({
imports: [
CommonModule,
TranslateModule.forRoot(),
],
declarations: [
MyDSpaceNewSubmissionDropdownComponent,
TestComponent
],
providers: [
{ provide: EntityTypeService, useValue: getMockEntityTypeService() },
{ provide: NgbModal, useValue: modalStub },
MyDSpaceNewSubmissionDropdownComponent
],
schemas: [NO_ERRORS_SCHEMA]
}).compileComponents();
const html = `<ds-my-dspace-new-submission (uploadEnd)="reload($event)"></ds-my-dspace-new-submission>`;
testFixture = createTestComponent(html, TestComponent) as ComponentFixture<TestComponent>;
testComp = testFixture.componentInstance;
submissionComponentFixture = TestBed.createComponent(MyDSpaceNewSubmissionDropdownComponent);
submissionComponent = submissionComponentFixture.componentInstance;
submissionComponentFixture.detectChanges();
}));
afterEach(() => {
testFixture.destroy();
submissionComponentFixture.destroy();
});
it('should create MyDSpaceNewSubmissionDropdownComponent', inject([MyDSpaceNewSubmissionDropdownComponent], (app: MyDSpaceNewSubmissionDropdownComponent) => {
expect(app).toBeDefined();
}));
it('should be a dropdown button', inject([MyDSpaceNewSubmissionDropdownComponent], (app: MyDSpaceNewSubmissionDropdownComponent) => {
const dropdownElement: DebugElement = submissionComponentFixture.debugElement.query(By.css('.dropdown-menu'));
const dropdown = dropdownElement.nativeElement;
expect(dropdown.innerHTML).toBeDefined();
}));
it('should invoke modalService.open', () => {
spyOn((submissionComponent as any).modalService, 'open').and.returnValue({ componentInstance: { } });
submissionComponent.openDialog(entityType1);
expect((submissionComponent as any).modalService.open).toHaveBeenCalled();
});
});
});
// declare a test component
@Component({
selector: 'ds-test-cmp',
template: ``
})
class TestComponent {
reload = (event) => {
return;
}
}

View File

@@ -0,0 +1,109 @@
import { Component, OnDestroy, OnInit } from '@angular/core';
import { Observable, of as observableOf, Subscription } from 'rxjs';
import { map, mergeMap, take } from 'rxjs/operators';
import { NgbModal } from '@ng-bootstrap/ng-bootstrap';
import { EntityTypeService } from '../../../core/data/entity-type.service';
import { ItemType } from '../../../core/shared/item-relationships/item-type.model';
import { FindListOptions } from '../../../core/data/request.models';
import { hasValue } from '../../../shared/empty.util';
import { CreateItemParentSelectorComponent } from '../../../shared/dso-selector/modal-wrappers/create-item-parent-selector/create-item-parent-selector.component';
import { RemoteData } from '../../../core/data/remote-data';
import { PaginatedList } from '../../../core/data/paginated-list.model';
/**
* This component represents the new submission dropdown
*/
@Component({
selector: 'ds-my-dspace-new-submission-dropdown',
styleUrls: ['./my-dspace-new-submission-dropdown.component.scss'],
templateUrl: './my-dspace-new-submission-dropdown.component.html'
})
export class MyDSpaceNewSubmissionDropdownComponent implements OnInit, OnDestroy {
/**
* Used to verify if there are one or more entities available
*/
public moreThanOne$: Observable<boolean>;
/**
* The entity observble (only if there is only one entity available)
*/
public singleEntity$: Observable<ItemType>;
/**
* The entity object (only if there is only one entity available)
*/
public singleEntity: ItemType;
/**
* TRUE if the page is initialized
*/
public initialized$: Observable<boolean>;
/**
* Array to track all subscriptions and unsubscribe them onDestroy
* @type {Array}
*/
public subs: Subscription[] = [];
/**
* Initialize instance variables
*
* @param {EntityTypeService} entityTypeService
* @param {NgbModal} modalService
*/
constructor(private entityTypeService: EntityTypeService,
private modalService: NgbModal) { }
/**
* Initialize entity type list
*/
ngOnInit() {
this.initialized$ = observableOf(false);
this.moreThanOne$ = this.entityTypeService.hasMoreThanOneAuthorized();
this.singleEntity$ = this.moreThanOne$.pipe(
mergeMap((response: boolean) => {
if (!response) {
const findListOptions: FindListOptions = {
elementsPerPage: 1,
currentPage: 1
};
return this.entityTypeService.getAllAuthorizedRelationshipType(findListOptions).pipe(
map((entities: RemoteData<PaginatedList<ItemType>>) => {
this.initialized$ = observableOf(true);
return entities.payload.page[0];
}),
take(1)
);
} else {
this.initialized$ = observableOf(true);
return observableOf(null);
}
}),
take(1)
);
this.subs.push(
this.singleEntity$.subscribe((result) => this.singleEntity = result )
);
}
/**
* Method called on clicking the button "New Submition", It opens a dialog for
* select a collection.
*/
openDialog(entity: ItemType) {
const modalRef = this.modalService.open(CreateItemParentSelectorComponent);
modalRef.componentInstance.entityType = entity.label;
}
/**
* Unsubscribe from the subscription
*/
ngOnDestroy(): void {
this.subs
.filter((subscription) => hasValue(subscription))
.forEach((subscription) => subscription.unsubscribe());
}
}

View File

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

View File

@@ -1,7 +1,6 @@
import { ChangeDetectorRef, Component, NO_ERRORS_SCHEMA } from '@angular/core'; import { ChangeDetectorRef, Component, NO_ERRORS_SCHEMA } from '@angular/core';
import { ComponentFixture, inject, TestBed, waitForAsync } from '@angular/core/testing'; import { ComponentFixture, inject, TestBed, waitForAsync } from '@angular/core/testing';
import { RouterTestingModule } from '@angular/router/testing'; import { RouterTestingModule } from '@angular/router/testing';
import { By } from '@angular/platform-browser';
import { TranslateLoader, TranslateModule } from '@ngx-translate/core'; import { TranslateLoader, TranslateModule } from '@ngx-translate/core';
import { ScrollToService } from '@nicky-lenaers/ngx-scroll-to'; import { ScrollToService } from '@nicky-lenaers/ngx-scroll-to';
@@ -25,6 +24,8 @@ import { HttpXsrfTokenExtractor } from '@angular/common/http';
import { CookieService } from '../../core/services/cookie.service'; import { CookieService } from '../../core/services/cookie.service';
import { CookieServiceMock } from '../../shared/mocks/cookie.service.mock'; import { CookieServiceMock } from '../../shared/mocks/cookie.service.mock';
import { HttpXsrfTokenExtractorMock } from '../../shared/mocks/http-xsrf-token-extractor.mock'; import { HttpXsrfTokenExtractorMock } from '../../shared/mocks/http-xsrf-token-extractor.mock';
import { getMockEntityTypeService } from './my-dspace-new-submission-dropdown/my-dspace-new-submission-dropdown.component.spec';
import { EntityTypeService } from '../../core/data/entity-type.service';
describe('MyDSpaceNewSubmissionComponent test', () => { describe('MyDSpaceNewSubmissionComponent test', () => {
@@ -62,6 +63,7 @@ describe('MyDSpaceNewSubmissionComponent test', () => {
{ provide: HttpXsrfTokenExtractor, useValue: new HttpXsrfTokenExtractorMock('mock-token') }, { provide: HttpXsrfTokenExtractor, useValue: new HttpXsrfTokenExtractorMock('mock-token') },
{ provide: CookieService, useValue: new CookieServiceMock() }, { provide: CookieService, useValue: new CookieServiceMock() },
{ provide: HostWindowService, useValue: new HostWindowServiceStub(800) }, { provide: HostWindowService, useValue: new HostWindowServiceStub(800) },
{ provide: EntityTypeService, useValue: getMockEntityTypeService() },
], ],
schemas: [NO_ERRORS_SCHEMA] schemas: [NO_ERRORS_SCHEMA]
}).compileComponents(); }).compileComponents();
@@ -104,20 +106,6 @@ describe('MyDSpaceNewSubmissionComponent test', () => {
comp.uploaderComponent.uploader = uploader; comp.uploaderComponent.uploader = uploader;
}); });
it('should call app.openDialog', (done) => {
spyOn(comp, 'openDialog');
const submissionButton = fixture.debugElement.query(By.css('button.btn-primary'));
submissionButton.triggerEventHandler('click', null);
fixture.detectChanges();
fixture.whenStable().then(() => {
expect(comp.openDialog).toHaveBeenCalled();
done();
});
});
it('should show a collection selector if only one file are uploaded', (done) => { it('should show a collection selector if only one file are uploaded', (done) => {
spyOn((comp as any).modalService, 'open').and.returnValue({ result: new Promise((res, rej) => {/****/}) }); spyOn((comp as any).modalService, 'open').and.returnValue({ result: new Promise((res, rej) => {/****/}) });
comp.afterFileLoaded(['']); comp.afterFileLoaded(['']);

View File

@@ -14,7 +14,6 @@ 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 { 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 { CollectionSelectorComponent } from '../collection-selector/collection-selector.component';
import { UploaderComponent } from '../../shared/uploader/uploader.component'; import { UploaderComponent } from '../../shared/uploader/uploader.component';
import { UploaderError } from '../../shared/uploader/uploader-error.model'; import { UploaderError } from '../../shared/uploader/uploader-error.model';
@@ -118,14 +117,6 @@ export class MyDSpaceNewSubmissionComponent implements OnDestroy, OnInit {
this.notificationsService.error(null, this.translate.get(errorMessageKey)); this.notificationsService.error(null, this.translate.get(errorMessageKey));
} }
/**
* Method called on clicking the button "New Submission", It opens a dialog for
* select a collection.
*/
openDialog() {
this.modalService.open(CreateItemParentSelectorComponent);
}
/** /**
* Method invoked after all file are loaded from upload plugin * Method invoked after all file are loaded from upload plugin
*/ */

View File

@@ -11,6 +11,8 @@ import { MyDSpaceGuard } from './my-dspace.guard';
import { MyDSpaceConfigurationService } from './my-dspace-configuration.service'; import { MyDSpaceConfigurationService } from './my-dspace-configuration.service';
import { CollectionSelectorComponent } from './collection-selector/collection-selector.component'; import { CollectionSelectorComponent } from './collection-selector/collection-selector.component';
import { MyDspaceSearchModule } from './my-dspace-search.module'; import { MyDspaceSearchModule } from './my-dspace-search.module';
import { MyDSpaceNewSubmissionDropdownComponent } from './my-dspace-new-submission/my-dspace-new-submission-dropdown/my-dspace-new-submission-dropdown.component';
import { MyDSpaceNewExternalDropdownComponent } from './my-dspace-new-submission/my-dspace-new-external-dropdown/my-dspace-new-external-dropdown.component';
import { ThemedMyDSpacePageComponent } from './themed-my-dspace-page.component'; import { ThemedMyDSpacePageComponent } from './themed-my-dspace-page.component';
const DECLARATIONS = [ const DECLARATIONS = [
@@ -18,7 +20,9 @@ const DECLARATIONS = [
ThemedMyDSpacePageComponent, ThemedMyDSpacePageComponent,
MyDSpaceResultsComponent, MyDSpaceResultsComponent,
MyDSpaceNewSubmissionComponent, MyDSpaceNewSubmissionComponent,
CollectionSelectorComponent CollectionSelectorComponent,
MyDSpaceNewSubmissionDropdownComponent,
MyDSpaceNewExternalDropdownComponent
]; ];
@NgModule({ @NgModule({

View File

@@ -1,33 +1,31 @@
<div class="form-group w-100 pr-2 pl-2"> <div *ngIf="searchField" class="form-group w-100 pr-2 pl-2">
<input *ngIf="searchField" <input type="search"
type="search" class="form-control w-100"
class="form-control w-100" (click)="$event.stopPropagation();"
(click)="$event.stopPropagation();" placeholder="{{ 'submission.sections.general.search-collection' | translate }}"
placeholder="{{ 'submission.sections.general.search-collection' | translate }}" [formControl]="searchField"
[formControl]="searchField" #searchFieldEl>
#searchFieldEl>
</div> </div>
<div class="dropdown-divider"></div> <div class="dropdown-divider"></div>
<div <div class="scrollable-menu"
class="scrollable-menu" aria-labelledby="dropdownMenuButton"
aria-labelledby="dropdownMenuButton" (scroll)="onScroll($event)"
(scroll)="onScroll($event)"> infiniteScroll
<div [infiniteScrollDistance]="5"
infiniteScroll [infiniteScrollThrottle]="300"
[infiniteScrollDistance]="2" [infiniteScrollUpDistance]="1.5"
[infiniteScrollThrottle]="300" [fromRoot]="true"
[infiniteScrollUpDistance]="1.5" [scrollWindow]="false"
[infiniteScrollContainer]="'.scrollable-menu'" (scrolled)="onScrollDown()">
[fromRoot]="true"
(scrolled)="onScrollDown()"> <button class="dropdown-item disabled" *ngIf="searchListCollection?.length == 0 && !(isLoading | async)">
<button class="dropdown-item disabled" *ngIf="searchListCollection?.length == 0 && !(isLoadingList | async)"> {{'submission.sections.general.no-collection' | translate}}
{{'submission.sections.general.no-collection' | translate}} </button>
</button> <ng-container *ngIf="searchListCollection?.length > 0 && !(isLoading | async)">
<button <button *ngFor="let listItem of searchListCollection"
*ngFor="let listItem of searchListCollection" class="dropdown-item collection-item"
class="dropdown-item collection-item" title="{{ listItem.collection.name }}"
title="{{ listItem.collection.name }}" (click)="onSelect(listItem)">
(click)="onSelect(listItem)">
<ul class="list-unstyled mb-0"> <ul class="list-unstyled mb-0">
<li class="list-item text-truncate text-secondary" *ngFor="let item of listItem.communities"> <li class="list-item text-truncate text-secondary" *ngFor="let item of listItem.communities">
{{ item.name}} <i class="fa fa-level-down" aria-hidden="true"></i> {{ item.name}} <i class="fa fa-level-down" aria-hidden="true"></i>
@@ -35,9 +33,10 @@
<li class="list-item text-truncate text-primary font-weight-bold">{{ listItem.collection.name}}</li> <li class="list-item text-truncate text-primary font-weight-bold">{{ listItem.collection.name}}</li>
</ul> </ul>
</button> </button>
<button class="dropdown-item disabled" *ngIf="(isLoadingList | async)" > </ng-container>
<ds-loading message="{{'loading.default' | translate}}"> <button class="dropdown-item disabled" *ngIf="(isLoading | async)">
</ds-loading> <ds-loading message="{{'loading.default' | translate}}">
</button> </ds-loading>
</div> </button>
</div>
</div>

View File

@@ -5,21 +5,16 @@ import { By } from '@angular/platform-browser';
import { getTestScheduler } from 'jasmine-marbles'; import { getTestScheduler } from 'jasmine-marbles';
import { TestScheduler } from 'rxjs/testing'; import { TestScheduler } from 'rxjs/testing';
import { TranslateLoader, TranslateModule } from '@ngx-translate/core'; import { TranslateLoader, TranslateModule } from '@ngx-translate/core';
import { of as observableOf } from 'rxjs';
import { CollectionDropdownComponent } from './collection-dropdown.component'; import { CollectionDropdownComponent } from './collection-dropdown.component';
import { RemoteData } from '../../core/data/remote-data'; import { buildPaginatedList } from '../../core/data/paginated-list.model';
import { buildPaginatedList, PaginatedList } from '../../core/data/paginated-list.model'; import { createSuccessfulRemoteDataObject$ } from '../remote-data.utils';
import { createSuccessfulRemoteDataObject, createSuccessfulRemoteDataObject$ } from '../remote-data.utils';
import { PageInfo } from '../../core/shared/page-info.model'; import { PageInfo } from '../../core/shared/page-info.model';
import { Collection } from '../../core/shared/collection.model'; import { Collection } from '../../core/shared/collection.model';
import { CollectionDataService } from '../../core/data/collection-data.service'; import { CollectionDataService } from '../../core/data/collection-data.service';
import { TranslateLoaderMock } from '../mocks/translate-loader.mock'; import { TranslateLoaderMock } from '../mocks/translate-loader.mock';
import { Community } from '../../core/shared/community.model'; import { Community } from '../../core/shared/community.model';
import { MockElementRef } from '../testing/element-ref.mock'; import { MockElementRef } from '../testing/element-ref.mock';
import { FollowLinkConfig } from '../utils/follow-link-config.model';
import { FindListOptions } from '../../core/data/request.models';
import { Observable } from 'rxjs/internal/Observable';
const community: Community = Object.assign(new Community(), { const community: Community = Object.assign(new Community(), {
id: 'ce64f48e-2c9b-411a-ac36-ee429c0e6a88', id: 'ce64f48e-2c9b-411a-ac36-ee429c0e6a88',
@@ -99,17 +94,6 @@ const listElementMock = {
} }
}; };
// tslint:disable-next-line: max-classes-per-file
class CollectionDataServiceMock {
getAuthorizedCollection(query: string, options: FindListOptions = {}, ...linksToFollow: FollowLinkConfig<Collection>[]): Observable<RemoteData<PaginatedList<Collection>>> {
return observableOf(
createSuccessfulRemoteDataObject(
buildPaginatedList(new PageInfo(), collections)
)
);
}
}
describe('CollectionDropdownComponent', () => { describe('CollectionDropdownComponent', () => {
let component: CollectionDropdownComponent; let component: CollectionDropdownComponent;
let componentAsAny: any; let componentAsAny: any;
@@ -117,12 +101,16 @@ describe('CollectionDropdownComponent', () => {
let scheduler: TestScheduler; let scheduler: TestScheduler;
const collectionDataServiceMock: any = jasmine.createSpyObj('CollectionDataService', { const collectionDataServiceMock: any = jasmine.createSpyObj('CollectionDataService', {
getAuthorizedCollection: jasmine.createSpy('getAuthorizedCollection') getAuthorizedCollection: jasmine.createSpy('getAuthorizedCollection'),
getAuthorizedCollectionByEntityType: jasmine.createSpy('getAuthorizedCollectionByEntityType')
}); });
const paginatedCollection = buildPaginatedList(new PageInfo(), collections); const paginatedCollection = buildPaginatedList(new PageInfo(), collections);
const paginatedCollectionRD$ = createSuccessfulRemoteDataObject$(paginatedCollection); const paginatedCollectionRD$ = createSuccessfulRemoteDataObject$(paginatedCollection);
const paginatedOneElementCollection = buildPaginatedList(new PageInfo(), [collections[0]]);
const paginatedOneElementCollectionRD$ = createSuccessfulRemoteDataObject$(paginatedOneElementCollection);
beforeEach(waitForAsync(() => { beforeEach(waitForAsync(() => {
TestBed.configureTestingModule({ TestBed.configureTestingModule({
imports: [ imports: [
@@ -150,6 +138,7 @@ describe('CollectionDropdownComponent', () => {
component = fixture.componentInstance; component = fixture.componentInstance;
componentAsAny = component; componentAsAny = component;
componentAsAny.collectionDataService.getAuthorizedCollection.and.returnValue(paginatedCollectionRD$); componentAsAny.collectionDataService.getAuthorizedCollection.and.returnValue(paginatedCollectionRD$);
componentAsAny.collectionDataService.getAuthorizedCollectionByEntityType.and.returnValue(paginatedCollectionRD$);
}); });
it('should init component with collection list', () => { it('should init component with collection list', () => {
@@ -211,10 +200,10 @@ describe('CollectionDropdownComponent', () => {
}); });
it('should change loader status', () => { it('should change loader status', () => {
spyOn(component.isLoadingList, 'next').and.callThrough(); spyOn(component.isLoading, 'next').and.callThrough();
component.hideShowLoader(true); component.hideShowLoader(true);
expect(component.isLoadingList.next).toHaveBeenCalledWith(true); expect(component.isLoading.next).toHaveBeenCalledWith(true);
}); });
it('reset pagination fields', () => { it('reset pagination fields', () => {
@@ -225,4 +214,36 @@ describe('CollectionDropdownComponent', () => {
expect(component.hasNextPage).toEqual(true); expect(component.hasNextPage).toEqual(true);
expect(component.searchListCollection).toEqual([]); expect(component.searchListCollection).toEqual([]);
}); });
it('should invoke the method getAuthorizedCollectionByEntityType of CollectionDataService when entityType is set',() => {
component.entityType = 'rel';
scheduler.schedule(() => fixture.detectChanges());
scheduler.flush();
expect((component as any).collectionDataService.getAuthorizedCollectionByEntityType).toHaveBeenCalled();
});
it('should emit hasChoice true when totalElements is greater then one', () => {
spyOn(component.searchComplete, 'emit').and.callThrough();
component.ngOnInit();
fixture.detectChanges();
expect(component.searchComplete.emit).toHaveBeenCalledWith();
});
it('should emit theOnlySelectable when totalElements is equal to one', () => {
componentAsAny.collectionDataService.getAuthorizedCollection.and.returnValue(paginatedOneElementCollectionRD$);
componentAsAny.collectionDataService.getAuthorizedCollectionByEntityType.and.returnValue(paginatedOneElementCollectionRD$);
spyOn(component.theOnlySelectable, 'emit').and.callThrough();
component.ngOnInit();
fixture.detectChanges();
const expectedTheOnlySelectable = {
communities: [ { id: 'ce64f48e-2c9b-411a-ac36-ee429c0e6a88', name: 'Community 1', uuid: 'ce64f48e-2c9b-411a-ac36-ee429c0e6a88' } ],
collection: { id: 'ce64f48e-2c9b-411a-ac36-ee429c0e6a88', uuid: 'ce64f48e-2c9b-411a-ac36-ee429c0e6a88', name: 'Collection 1' }
};
expect(component.theOnlySelectable.emit).toHaveBeenCalledWith(expectedTheOnlySelectable);
});
}); });

View File

@@ -4,14 +4,15 @@ import {
ElementRef, ElementRef,
EventEmitter, EventEmitter,
HostListener, HostListener,
Input,
OnDestroy, OnDestroy,
OnInit, OnInit,
Output Output
} from '@angular/core'; } from '@angular/core';
import { FormControl } from '@angular/forms'; import { FormControl } from '@angular/forms';
import { BehaviorSubject, Observable, Subscription } from 'rxjs'; import { BehaviorSubject, from as observableFrom, Observable, of as observableOf, Subscription } from 'rxjs';
import { debounceTime, distinctUntilChanged, map, mergeMap, reduce, startWith, switchMap } from 'rxjs/operators'; import { debounceTime, distinctUntilChanged, map, mergeMap, reduce, startWith, switchMap, take } from 'rxjs/operators';
import { hasValue } from '../empty.util'; import { hasValue } from '../empty.util';
import { RemoteData } from '../../core/data/remote-data'; import { RemoteData } from '../../core/data/remote-data';
@@ -21,10 +22,7 @@ import { Community } from '../../core/shared/community.model';
import { CollectionDataService } from '../../core/data/collection-data.service'; import { CollectionDataService } from '../../core/data/collection-data.service';
import { Collection } from '../../core/shared/collection.model'; import { Collection } from '../../core/shared/collection.model';
import { followLink } from '../utils/follow-link-config.model'; import { followLink } from '../utils/follow-link-config.model';
import { import { getFirstCompletedRemoteData, getFirstSucceededRemoteDataPayload } from '../../core/shared/operators';
getFirstSucceededRemoteDataPayload,
getFirstSucceededRemoteWithNotEmptyData
} from '../../core/shared/operators';
/** /**
* An interface to represent a collection entry * An interface to represent a collection entry
@@ -89,10 +87,10 @@ export class CollectionDropdownComponent implements OnInit, OnDestroy {
/** /**
* A boolean representing if the loader is visible or not * A boolean representing if the loader is visible or not
*/ */
isLoadingList: BehaviorSubject<boolean> = new BehaviorSubject(false); isLoading: BehaviorSubject<boolean> = new BehaviorSubject(false);
/** /**
* A numeric representig current page * A numeric representing current page
*/ */
currentPage: number; currentPage: number;
@@ -102,10 +100,25 @@ export class CollectionDropdownComponent implements OnInit, OnDestroy {
hasNextPage: boolean; hasNextPage: boolean;
/** /**
* Current seach query used to filter collection list * Current search query used to filter collection list
*/ */
currentQuery: string; currentQuery: string;
/**
* If present this value is used to filter collection list by entity type
*/
@Input() entityType: string;
/**
* Emit to notify whether search is complete
*/
@Output() searchComplete = new EventEmitter<any>();
/**
* Emit to notify the only selectable collection.
*/
@Output() theOnlySelectable = new EventEmitter<CollectionListEntry>();
constructor( constructor(
private changeDetectorRef: ChangeDetectorRef, private changeDetectorRef: ChangeDetectorRef,
private collectionDataService: CollectionDataService, private collectionDataService: CollectionDataService,
@@ -132,6 +145,7 @@ export class CollectionDropdownComponent implements OnInit, OnDestroy {
* Initialize collection list * Initialize collection list
*/ */
ngOnInit() { ngOnInit() {
this.isLoading.next(false);
this.subs.push(this.searchField.valueChanges.pipe( this.subs.push(this.searchField.valueChanges.pipe(
debounceTime(500), debounceTime(500),
distinctUntilChanged(), distinctUntilChanged(),
@@ -160,7 +174,7 @@ export class CollectionDropdownComponent implements OnInit, OnDestroy {
} }
/** /**
* Method used from infitity scroll for retrive more data on scroll down * Method used from infinity scroll for retrieve more data on scroll down
*/ */
onScrollDown() { onScrollDown() {
if ( this.hasNextPage ) { if ( this.hasNextPage ) {
@@ -175,6 +189,7 @@ export class CollectionDropdownComponent implements OnInit, OnDestroy {
* the selected [CollectionListEntry] * the selected [CollectionListEntry]
*/ */
onSelect(event: CollectionListEntry) { onSelect(event: CollectionListEntry) {
this.isLoading.next(true);
this.selectionChange.emit(event); this.selectionChange.emit(event);
} }
@@ -184,36 +199,57 @@ export class CollectionDropdownComponent implements OnInit, OnDestroy {
* @param page page number * @param page page number
*/ */
populateCollectionList(query: string, page: number) { populateCollectionList(query: string, page: number) {
this.isLoadingList.next(true); this.isLoading.next(true);
// Set the pagination info // Set the pagination info
const findOptions: FindListOptions = { const findOptions: FindListOptions = {
elementsPerPage: 10, elementsPerPage: 10,
currentPage: page currentPage: page
}; };
this.searchListCollection$ = this.collectionDataService let searchListService$: Observable<RemoteData<PaginatedList<Collection>>>;
.getAuthorizedCollection(query, findOptions, true, false, followLink('parentCommunity')) if (this.entityType) {
.pipe( searchListService$ = this.collectionDataService
getFirstSucceededRemoteWithNotEmptyData(), .getAuthorizedCollectionByEntityType(
switchMap((collections: RemoteData<PaginatedList<Collection>>) => { query,
if ( (this.searchListCollection.length + findOptions.elementsPerPage) >= collections.payload.totalElements ) { this.entityType,
findOptions,
true,
followLink('parentCommunity'));
} else {
searchListService$ = this.collectionDataService
.getAuthorizedCollection(query, findOptions, true, true, followLink('parentCommunity'));
}
this.searchListCollection$ = searchListService$.pipe(
getFirstCompletedRemoteData(),
switchMap((collectionsRD: RemoteData<PaginatedList<Collection>>) => {
this.searchComplete.emit();
if (collectionsRD.hasSucceeded && collectionsRD.payload.totalElements > 0) {
if ( (this.searchListCollection.length + findOptions.elementsPerPage) >= collectionsRD.payload.totalElements ) {
this.hasNextPage = false;
this.emitSelectionEvents(collectionsRD);
return observableFrom(collectionsRD.payload.page).pipe(
mergeMap((collection: Collection) => collection.parentCommunity.pipe(
getFirstSucceededRemoteDataPayload(),
map((community: Community) => ({
communities: [{ id: community.id, name: community.name }],
collection: { id: collection.id, uuid: collection.id, name: collection.name }
})
))),
reduce((acc: any, value: any) => [...acc, value], []),
);
}
} else {
this.hasNextPage = false; this.hasNextPage = false;
return observableOf([]);
} }
return collections.payload.page; })
}),
mergeMap((collection: Collection) => collection.parentCommunity.pipe(
getFirstSucceededRemoteDataPayload(),
map((community: Community) => ({
communities: [{ id: community.id, name: community.name }],
collection: { id: collection.id, uuid: collection.id, name: collection.name }
})
))),
reduce((acc: any, value: any) => [...acc, value], []),
startWith([])
); );
this.subs.push(this.searchListCollection$.subscribe( this.subs.push(
(next) => { this.searchListCollection.push(...next); }, undefined, this.searchListCollection$.subscribe((list: CollectionListEntry[]) => {
() => { this.hideShowLoader(false); this.changeDetectorRef.detectChanges(); } this.searchListCollection.push(...list);
)); this.hideShowLoader(false);
this.changeDetectorRef.detectChanges();
})
);
} }
/** /**
@@ -245,6 +281,29 @@ export class CollectionDropdownComponent implements OnInit, OnDestroy {
* @param hideShow true for show, false otherwise * @param hideShow true for show, false otherwise
*/ */
hideShowLoader(hideShow: boolean) { hideShowLoader(hideShow: boolean) {
this.isLoadingList.next(hideShow); this.isLoading.next(hideShow);
} }
/**
* Emit events related to the number of selectable collections.
* hasChoice containing whether there are more then one selectable collections.
* theOnlySelectable containing the only collection available.
* @param collections
* @private
*/
private emitSelectionEvents(collections: RemoteData<PaginatedList<Collection>>) {
if (collections.payload.totalElements === 1) {
const collection = collections.payload.page[0];
collections.payload.page[0].parentCommunity.pipe(
getFirstSucceededRemoteDataPayload(),
take(1)
).subscribe((community: Community) => {
this.theOnlySelectable.emit({
communities: [{ id: community.id, name: community.name, uuid: community.id }],
collection: { id: collection.id, uuid: collection.id, name: collection.name }
});
});
}
}
} }

View File

@@ -26,7 +26,8 @@ describe('AuthorizedCollectionSelectorComponent', () => {
id: 'authorized-collection' id: 'authorized-collection'
}); });
collectionService = jasmine.createSpyObj('collectionService', { collectionService = jasmine.createSpyObj('collectionService', {
getAuthorizedCollection: createSuccessfulRemoteDataObject$(createPaginatedList([collection])) getAuthorizedCollection: createSuccessfulRemoteDataObject$(createPaginatedList([collection])),
getAuthorizedCollectionByEntityType: createSuccessfulRemoteDataObject$(createPaginatedList([collection]))
}); });
notificationsService = jasmine.createSpyObj('notificationsService', ['error']); notificationsService = jasmine.createSpyObj('notificationsService', ['error']);
TestBed.configureTestingModule({ TestBed.configureTestingModule({
@@ -49,12 +50,27 @@ describe('AuthorizedCollectionSelectorComponent', () => {
}); });
describe('search', () => { describe('search', () => {
it('should call getAuthorizedCollection and return the authorized collection in a SearchResult', (done) => { describe('when has no entity type', () => {
it('should call getAuthorizedCollection and return the authorized collection in a SearchResult', (done) => {
component.search('', 1).subscribe((resultRD) => { component.search('', 1).subscribe((resultRD) => {
expect(collectionService.getAuthorizedCollection).toHaveBeenCalled(); expect(collectionService.getAuthorizedCollection).toHaveBeenCalled();
expect(resultRD.payload.page.length).toEqual(1); expect(resultRD.payload.page.length).toEqual(1);
expect(resultRD.payload.page[0].indexableObject).toEqual(collection); expect(resultRD.payload.page[0].indexableObject).toEqual(collection);
done(); done();
});
});
});
describe('when has entity type', () => {
it('should call getAuthorizedCollectionByEntityType and return the authorized collection in a SearchResult', (done) => {
component.entityType = 'test';
fixture.detectChanges();
component.search('', 1).subscribe((resultRD) => {
expect(collectionService.getAuthorizedCollectionByEntityType).toHaveBeenCalled();
expect(resultRD.payload.page.length).toEqual(1);
expect(resultRD.payload.page[0].indexableObject).toEqual(collection);
done();
});
}); });
}); });
}); });

View File

@@ -1,8 +1,8 @@
import { Component } from '@angular/core'; import { Component, Input } from '@angular/core';
import { DSOSelectorComponent } from '../dso-selector.component'; import { DSOSelectorComponent } from '../dso-selector.component';
import { SearchService } from '../../../../core/shared/search/search.service'; import { SearchService } from '../../../../core/shared/search/search.service';
import { CollectionDataService } from '../../../../core/data/collection-data.service'; import { CollectionDataService } from '../../../../core/data/collection-data.service';
import { Observable } from 'rxjs/internal/Observable'; import { Observable } from 'rxjs';
import { getFirstCompletedRemoteData } from '../../../../core/shared/operators'; import { getFirstCompletedRemoteData } from '../../../../core/shared/operators';
import { map } from 'rxjs/operators'; import { map } from 'rxjs/operators';
import { CollectionSearchResult } from '../../../object-collection/shared/collection-search-result.model'; import { CollectionSearchResult } from '../../../object-collection/shared/collection-search-result.model';
@@ -14,6 +14,8 @@ import { RemoteData } from '../../../../core/data/remote-data';
import { hasValue } from '../../../empty.util'; import { hasValue } from '../../../empty.util';
import { NotificationsService } from '../../../notifications/notifications.service'; import { NotificationsService } from '../../../notifications/notifications.service';
import { TranslateService } from '@ngx-translate/core'; import { TranslateService } from '@ngx-translate/core';
import { Collection } from '../../../../core/shared/collection.model';
import { FindListOptions } from '../../../../core/data/request.models';
@Component({ @Component({
selector: 'ds-authorized-collection-selector', selector: 'ds-authorized-collection-selector',
@@ -24,6 +26,11 @@ import { TranslateService } from '@ngx-translate/core';
* Component rendering a list of collections to select from * Component rendering a list of collections to select from
*/ */
export class AuthorizedCollectionSelectorComponent extends DSOSelectorComponent { export class AuthorizedCollectionSelectorComponent extends DSOSelectorComponent {
/**
* If present this value is used to filter collection list by entity type
*/
@Input() entityType: string;
constructor(protected searchService: SearchService, constructor(protected searchService: SearchService,
protected collectionDataService: CollectionDataService, protected collectionDataService: CollectionDataService,
protected notifcationsService: NotificationsService, protected notifcationsService: NotificationsService,
@@ -44,10 +51,23 @@ export class AuthorizedCollectionSelectorComponent extends DSOSelectorComponent
* @param page Page to retrieve * @param page Page to retrieve
*/ */
search(query: string, page: number): Observable<RemoteData<PaginatedList<SearchResult<DSpaceObject>>>> { search(query: string, page: number): Observable<RemoteData<PaginatedList<SearchResult<DSpaceObject>>>> {
return this.collectionDataService.getAuthorizedCollection(query, Object.assign({ let searchListService$: Observable<RemoteData<PaginatedList<Collection>>> = null;
const findOptions: FindListOptions = {
currentPage: page, currentPage: page,
elementsPerPage: this.defaultPagination.pageSize elementsPerPage: this.defaultPagination.pageSize
}),true, false, followLink('parentCommunity')).pipe( };
if (this.entityType) {
searchListService$ = this.collectionDataService
.getAuthorizedCollectionByEntityType(
query,
this.entityType,
findOptions);
} else {
searchListService$ = this.collectionDataService
.getAuthorizedCollection(query, findOptions, true, false, followLink('parentCommunity'));
}
return searchListService$.pipe(
getFirstCompletedRemoteData(), getFirstCompletedRemoteData(),
map((rd) => Object.assign(new RemoteData(null, null, null, null), rd, { map((rd) => Object.assign(new RemoteData(null, null, null, null), rd, {
payload: hasValue(rd.payload) ? buildPaginatedList(rd.payload.pageInfo, rd.payload.page.map((col) => Object.assign(new CollectionSearchResult(), { indexableObject: col }))) : null, payload: hasValue(rd.payload) ? buildPaginatedList(rd.payload.pageInfo, rd.payload.page.map((col) => Object.assign(new CollectionSearchResult(), { indexableObject: col }))) : null,

View File

@@ -6,6 +6,9 @@
</div> </div>
<div class="modal-body"> <div class="modal-body">
<h5 *ngIf="header" class="px-2">{{header | translate}}</h5> <h5 *ngIf="header" class="px-2">{{header | translate}}</h5>
<ds-authorized-collection-selector [currentDSOId]="dsoRD?.payload.uuid" [types]="selectorTypes" (onSelect)="selectObject($event)"></ds-authorized-collection-selector> <ds-authorized-collection-selector [currentDSOId]="dsoRD?.payload.uuid"
[entityType]="entityType"
[types]="selectorTypes"
(onSelect)="selectObject($event)"></ds-authorized-collection-selector>
</div> </div>
</div> </div>

View File

@@ -69,4 +69,10 @@ describe('CreateItemParentSelectorComponent', () => {
expect(router.navigate).toHaveBeenCalledWith(['/submit'], { queryParams: { collection: collection.uuid } }); expect(router.navigate).toHaveBeenCalledWith(['/submit'], { queryParams: { collection: collection.uuid } });
}); });
it('should call navigate on the router with entityType parameter', () => {
const entityType = 'Person';
component.entityType = entityType;
component.navigate(collection);
expect(router.navigate).toHaveBeenCalledWith(['/submit'], { queryParams: { collection: collection.uuid, entityType: entityType } });
});
}); });

View File

@@ -1,4 +1,4 @@
import { Component, OnInit } from '@angular/core'; import { Component, Input, OnInit } from '@angular/core';
import { ActivatedRoute, NavigationExtras, Router } from '@angular/router'; import { ActivatedRoute, NavigationExtras, Router } from '@angular/router';
import { DSpaceObjectType } from '../../../../core/shared/dspace-object-type.model'; import { DSpaceObjectType } from '../../../../core/shared/dspace-object-type.model';
import { DSpaceObject } from '../../../../core/shared/dspace-object.model'; import { DSpaceObject } from '../../../../core/shared/dspace-object.model';
@@ -22,6 +22,11 @@ export class CreateItemParentSelectorComponent extends DSOSelectorModalWrapperCo
action = SelectorActionType.CREATE; action = SelectorActionType.CREATE;
header = 'dso-selector.create.item.sub-level'; header = 'dso-selector.create.item.sub-level';
/**
* If present this value is used to filter collection list by entity type
*/
@Input() entityType: string;
constructor(protected activeModal: NgbActiveModal, protected route: ActivatedRoute, private router: Router) { constructor(protected activeModal: NgbActiveModal, protected route: ActivatedRoute, private router: Router) {
super(activeModal, route); super(activeModal, route);
} }
@@ -35,6 +40,9 @@ export class CreateItemParentSelectorComponent extends DSOSelectorModalWrapperCo
['collection']: dso.uuid, ['collection']: dso.uuid,
} }
}; };
if (this.entityType) {
navigationExtras.queryParams.entityType = this.entityType;
}
this.router.navigate(['/submit'], navigationExtras); this.router.navigate(['/submit'], navigationExtras);
} }
} }

View File

@@ -0,0 +1,28 @@
<div
class="scrollable-menu"
aria-labelledby="dropdownMenuButton"
(scroll)="onScroll($event)"
infiniteScroll
[infiniteScrollDistance]="5"
[infiniteScrollThrottle]="300"
[infiniteScrollUpDistance]="1.5"
[fromRoot]="true"
[scrollWindow]="false"
(scrolled)="onScrollDown()">
<button class="dropdown-item disabled" *ngIf="searchListEntity?.length == 0 && !(isLoadingList | async)">
{{'submission.sections.general.no-entity' | translate}}
</button>
<button *ngFor="let listItem of searchListEntity"
class="dropdown-item entity-item"
title="{{ listItem.label }}"
(click)="onSelect(listItem)">
<ul class="list-unstyled mb-0">
<li class="list-item text-truncate text-primary font-weight-bold">{{ listItem.label.toLowerCase() + '.listelement.badge' | translate }}</li>
</ul>
</button>
<button class="dropdown-item disabled" *ngIf="(isLoadingList | async)" >
<ds-loading message="{{'loading.default' | translate}}">
</ds-loading>
</button>
</div>

View File

@@ -0,0 +1,19 @@
.list-item:active {
color: white !important;
}
.scrollable-menu {
height: auto;
max-height: var(--ds-dropdown-menu-max-height);
overflow-x: hidden;
}
.entity-item {
border-bottom: var(--bs-dropdown-border-width) solid var(--bs-dropdown-border-color);
}
#entityControlsDropdownMenu {
outline: 0;
left: 0 !important;
box-shadow: var(--bs-btn-focus-box-shadow);
}

View File

@@ -0,0 +1,167 @@
import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing';
import { EntityDropdownComponent } from './entity-dropdown.component';
import { getTestScheduler } from 'jasmine-marbles';
import { createSuccessfulRemoteDataObject$ } from '../remote-data.utils';
import { ItemType } from '../../core/shared/item-relationships/item-type.model';
import { ChangeDetectorRef, NO_ERRORS_SCHEMA, Pipe, PipeTransform } from '@angular/core';
import { EntityTypeService } from '../../core/data/entity-type.service';
import { TestScheduler } from 'rxjs/testing';
import { By } from '@angular/platform-browser';
import { createPaginatedList } from '../testing/utils.test';
// tslint:disable-next-line:pipe-prefix
@Pipe({ name: 'translate' })
class MockTranslatePipe implements PipeTransform {
transform(value: string): string {
return value;
}
}
const entities: ItemType[] = [
Object.assign(new ItemType(), {
id: 'ce64f48e-2c9b-411a-ac36-ee429c0e6a88',
label: 'Entity_1',
uuid: 'UUID-ce64f48e-2c9b-411a-ac36-ee429c0e6a88'
}),
Object.assign(new ItemType(), {
id: '59ee713b-ee53-4220-8c3f-9860dc84fe33',
label: 'Entity_2',
uuid: 'UUID-59ee713b-ee53-4220-8c3f-9860dc84fe33'
}),
Object.assign(new ItemType(), {
id: 'e9dbf393-7127-415f-8919-55be34a6e9ed',
label: 'Entity_3',
uuid: 'UUID-7127-415f-8919-55be34a6e9ed'
}),
Object.assign(new ItemType(), {
id: '59da2ff0-9bf4-45bf-88be-e35abd33f304',
label: 'Entity_4',
uuid: 'UUID-59da2ff0-9bf4-45bf-88be-e35abd33f304'
}),
Object.assign(new ItemType(), {
id: 'a5159760-f362-4659-9e81-e3253ad91ede',
label: 'Entity_5',
uuid: 'UUID-a5159760-f362-4659-9e81-e3253ad91ede'
}),
];
const listElementMock: ItemType = Object.assign(
new ItemType(), {
id: 'ce64f48e-2c9b-411a-ac36-ee429c0e6a88',
label: 'Entity_1',
uuid: 'UUID-ce64f48e-2c9b-411a-ac36-ee429c0e6a88'
}
);
describe('EntityDropdownComponent', () => {
let component: EntityDropdownComponent;
let componentAsAny: any;
let fixture: ComponentFixture<EntityDropdownComponent>;
let scheduler: TestScheduler;
const entityTypeServiceMock: any = jasmine.createSpyObj('EntityTypeService', {
getAllAuthorizedRelationshipType: jasmine.createSpy('getAllAuthorizedRelationshipType'),
getAllAuthorizedRelationshipTypeImport: jasmine.createSpy('getAllAuthorizedRelationshipTypeImport')
});
let translatePipeSpy: jasmine.Spy;
const paginatedEntities = createPaginatedList(entities);
const paginatedEntitiesRD$ = createSuccessfulRemoteDataObject$(paginatedEntities);
beforeEach(waitForAsync(() => {
TestBed.configureTestingModule({
imports: [],
declarations: [EntityDropdownComponent, MockTranslatePipe],
providers: [
{ provide: EntityTypeService, useValue: entityTypeServiceMock },
ChangeDetectorRef
],
schemas: [NO_ERRORS_SCHEMA]
})
.compileComponents();
}));
beforeEach(() => {
scheduler = getTestScheduler();
fixture = TestBed.createComponent(EntityDropdownComponent);
component = fixture.componentInstance;
componentAsAny = fixture.componentInstance;
componentAsAny.entityTypeService.getAllAuthorizedRelationshipType.and.returnValue(paginatedEntitiesRD$);
componentAsAny.entityTypeService.getAllAuthorizedRelationshipTypeImport.and.returnValue(paginatedEntitiesRD$);
component.isSubmission = true;
translatePipeSpy = spyOn(MockTranslatePipe.prototype, 'transform');
});
it('should translate entries', () => {
scheduler.schedule(() => fixture.detectChanges());
scheduler.flush();
expect(translatePipeSpy).toHaveBeenCalledWith('entity_1.listelement.badge');
});
it('should init component with entities list', () => {
spyOn(component.subs, 'push');
spyOn(component, 'resetPagination');
spyOn(component, 'populateEntityList').and.callThrough();
scheduler.schedule(() => fixture.detectChanges());
scheduler.flush();
const elements = fixture.debugElement.queryAll(By.css('.entity-item'));
expect(elements.length).toEqual(5);
expect(component.subs.push).toHaveBeenCalled();
expect(component.resetPagination).toHaveBeenCalled();
expect(component.populateEntityList).toHaveBeenCalled();
expect((component as any).entityTypeService.getAllAuthorizedRelationshipType).toHaveBeenCalled();
});
it('should trigger onSelect method when select a new entity from list', () => {
scheduler.schedule(() => fixture.detectChanges());
scheduler.flush();
spyOn(component, 'onSelect');
const entityItem = fixture.debugElement.query(By.css('.entity-item:nth-child(2)'));
entityItem.triggerEventHandler('click', null);
scheduler.schedule(() => fixture.detectChanges());
scheduler.flush();
expect(component.onSelect).toHaveBeenCalled();
});
it('should emit selectionChange event when selecting a new entity', () => {
spyOn(component.selectionChange, 'emit').and.callThrough();
component.ngOnInit();
component.onSelect(listElementMock as any);
fixture.detectChanges();
expect(component.selectionChange.emit).toHaveBeenCalledWith(listElementMock as any);
});
it('should change loader status', () => {
spyOn(component.isLoadingList, 'next').and.callThrough();
component.hideShowLoader(true);
expect(component.isLoadingList.next).toHaveBeenCalledWith(true);
});
it('reset pagination fields', () => {
component.resetPagination();
expect(component.currentPage).toEqual(1);
expect(component.hasNextPage).toEqual(true);
expect(component.searchListEntity).toEqual([]);
});
it('should invoke the method getAllAuthorizedRelationshipTypeImport of EntityTypeService when isSubmission is false', () => {
component.isSubmission = false;
scheduler.schedule(() => fixture.detectChanges());
scheduler.flush();
expect((component as any).entityTypeService.getAllAuthorizedRelationshipTypeImport).toHaveBeenCalled();
});
});

View File

@@ -0,0 +1,207 @@
import {
ChangeDetectorRef,
Component,
ElementRef,
EventEmitter,
HostListener,
Input,
OnDestroy,
OnInit,
Output
} from '@angular/core';
import { BehaviorSubject, Observable, Subscription } from 'rxjs';
import { hasValue } from '../empty.util';
import { reduce, startWith, switchMap } from 'rxjs/operators';
import { RemoteData } from '../../core/data/remote-data';
import { FindListOptions } from '../../core/data/request.models';
import { PaginatedList } from '../../core/data/paginated-list.model';
import { EntityTypeService } from '../../core/data/entity-type.service';
import { ItemType } from '../../core/shared/item-relationships/item-type.model';
import { getFirstSucceededRemoteWithNotEmptyData } from '../../core/shared/operators';
@Component({
selector: 'ds-entity-dropdown',
templateUrl: './entity-dropdown.component.html',
styleUrls: ['./entity-dropdown.component.scss']
})
export class EntityDropdownComponent implements OnInit, OnDestroy {
/**
* The entity list obtained from a search
* @type {Observable<ItemType[]>}
*/
public searchListEntity$: Observable<ItemType[]>;
/**
* A boolean representing if dropdown list is scrollable to the bottom
* @type {boolean}
*/
private scrollableBottom = false;
/**
* A boolean representing if dropdown list is scrollable to the top
* @type {boolean}
*/
private scrollableTop = false;
/**
* The list of entity to render
*/
public searchListEntity: ItemType[] = [];
/**
* TRUE if the parent operation is a 'new submission' operation, FALSE otherwise (eg.: is an 'Import metadata from an external source' operation).
*/
@Input() isSubmission: boolean;
/**
* The entity to output to the parent component
*/
@Output() selectionChange = new EventEmitter<ItemType>();
/**
* A boolean representing if the loader is visible or not
*/
public isLoadingList: BehaviorSubject<boolean> = new BehaviorSubject(false);
/**
* A numeric representig current page
*/
public currentPage: number;
/**
* A boolean representing if exist another page to render
*/
public hasNextPage: boolean;
/**
* Array to track all subscriptions and unsubscribe them onDestroy
* @type {Array}
*/
public subs: Subscription[] = [];
/**
* Initialize instance variables
*
* @param {ChangeDetectorRef} changeDetectorRef
* @param {EntityTypeService} entityTypeService
* @param {ElementRef} el
*/
constructor(
private changeDetectorRef: ChangeDetectorRef,
private entityTypeService: EntityTypeService,
private el: ElementRef
) { }
/**
* Method called on mousewheel event, it prevent the page scroll
* when arriving at the top/bottom of dropdown menu
*
* @param event
* mousewheel event
*/
@HostListener('mousewheel', ['$event']) onMousewheel(event) {
if (event.wheelDelta > 0 && this.scrollableTop) {
event.preventDefault();
}
if (event.wheelDelta < 0 && this.scrollableBottom) {
event.preventDefault();
}
}
/**
* Initialize entity list
*/
ngOnInit() {
this.resetPagination();
this.populateEntityList(this.currentPage);
}
/**
* Check if dropdown scrollbar is at the top or bottom of the dropdown list
*
* @param event
*/
public onScroll(event) {
this.scrollableBottom = (event.target.scrollTop + event.target.clientHeight === event.target.scrollHeight);
this.scrollableTop = (event.target.scrollTop === 0);
}
/**
* Method used from infitity scroll for retrive more data on scroll down
*/
public onScrollDown() {
if ( this.hasNextPage ) {
this.populateEntityList(++this.currentPage);
}
}
/**
* Emit a [selectionChange] event when a new entity is selected from list
*
* @param event
* the selected [ItemType]
*/
public onSelect(event: ItemType) {
this.selectionChange.emit(event);
}
/**
* Method called for populate the entity list
* @param page page number
*/
public populateEntityList(page: number) {
this.isLoadingList.next(true);
// Set the pagination info
const findOptions: FindListOptions = {
elementsPerPage: 10,
currentPage: page
};
let searchListEntity$;
if (this.isSubmission) {
searchListEntity$ = this.entityTypeService.getAllAuthorizedRelationshipType(findOptions);
} else {
searchListEntity$ = this.entityTypeService.getAllAuthorizedRelationshipTypeImport(findOptions);
}
this.searchListEntity$ = searchListEntity$.pipe(
getFirstSucceededRemoteWithNotEmptyData(),
switchMap((entityType: RemoteData<PaginatedList<ItemType>>) => {
if ( (this.searchListEntity.length + findOptions.elementsPerPage) >= entityType.payload.totalElements ) {
this.hasNextPage = false;
}
return entityType.payload.page;
}),
reduce((acc: any, value: any) => [...acc, value], []),
startWith([])
);
this.subs.push(
this.searchListEntity$.subscribe(
(next) => { this.searchListEntity.push(...next); }, undefined,
() => { this.hideShowLoader(false); this.changeDetectorRef.detectChanges(); }
)
);
}
/**
* Reset pagination values
*/
public resetPagination() {
this.currentPage = 1;
this.hasNextPage = true;
this.searchListEntity = [];
}
/**
* Hide/Show the entity list loader
* @param hideShow true for show, false otherwise
*/
public hideShowLoader(hideShow: boolean) {
this.isLoadingList.next(hideShow);
}
/**
* Unsubscribe from all subscriptions
*/
ngOnDestroy(): void {
this.subs.filter((sub) => hasValue(sub)).forEach((sub) => sub.unsubscribe());
}
}

View File

@@ -12,7 +12,9 @@ import { RelationshipOptions } from '../../models/relationship-options.model';
import { SearchResult } from '../../../../search/search-result.model'; import { SearchResult } from '../../../../search/search-result.model';
import { Item } from '../../../../../core/shared/item.model'; import { Item } from '../../../../../core/shared/item.model';
import { import {
AddRelationshipAction, RemoveRelationshipAction, UpdateRelationshipNameVariantAction, AddRelationshipAction,
RemoveRelationshipAction,
UpdateRelationshipNameVariantAction,
} from './relationship.actions'; } from './relationship.actions';
import { RelationshipService } from '../../../../../core/data/relationship.service'; import { RelationshipService } from '../../../../../core/data/relationship.service';
import { RelationshipTypeService } from '../../../../../core/data/relationship-type.service'; import { RelationshipTypeService } from '../../../../../core/data/relationship-type.service';
@@ -25,6 +27,7 @@ import { ExternalSourceService } from '../../../../../core/data/external-source.
import { Router } from '@angular/router'; import { Router } from '@angular/router';
import { RemoteDataBuildService } from '../../../../../core/cache/builders/remote-data-build.service'; import { RemoteDataBuildService } from '../../../../../core/cache/builders/remote-data-build.service';
import { getAllSucceededRemoteDataPayload } from '../../../../../core/shared/operators'; import { getAllSucceededRemoteDataPayload } from '../../../../../core/shared/operators';
import { followLink } from '../../../../utils/follow-link-config.model';
@Component({ @Component({
selector: 'ds-dynamic-lookup-relation-modal', selector: 'ds-dynamic-lookup-relation-modal',
@@ -146,7 +149,14 @@ export class DsDynamicLookupRelationModalComponent implements OnInit, OnDestroy
if (isNotEmpty(this.relationshipOptions.externalSources)) { if (isNotEmpty(this.relationshipOptions.externalSources)) {
this.externalSourcesRD$ = this.rdbService.aggregate( this.externalSourcesRD$ = this.rdbService.aggregate(
this.relationshipOptions.externalSources.map((source) => this.externalSourceService.findById(source)) this.relationshipOptions.externalSources.map((source) => {
return this.externalSourceService.findById(
source,
true,
true,
followLink('entityTypes')
);
})
).pipe( ).pipe(
getAllSucceededRemoteDataPayload() getAllSucceededRemoteDataPayload()
); );

View File

@@ -26,6 +26,7 @@ import { ExternalSourceEntryImportModalComponent } from './external-source-entry
import { createPaginatedList } from '../../../../../testing/utils.test'; import { createPaginatedList } from '../../../../../testing/utils.test';
import { PaginationService } from '../../../../../../core/pagination/pagination.service'; import { PaginationService } from '../../../../../../core/pagination/pagination.service';
import { PaginationServiceStub } from '../../../../../testing/pagination-service.stub'; import { PaginationServiceStub } from '../../../../../testing/pagination-service.stub';
import { ItemType } from '../../../../../../core/shared/item-relationships/item-type.model';
describe('DsDynamicLookupRelationExternalSourceTabComponent', () => { describe('DsDynamicLookupRelationExternalSourceTabComponent', () => {
let component: DsDynamicLookupRelationExternalSourceTabComponent; let component: DsDynamicLookupRelationExternalSourceTabComponent;
@@ -35,10 +36,12 @@ describe('DsDynamicLookupRelationExternalSourceTabComponent', () => {
let selectableListService; let selectableListService;
let modalService; let modalService;
const itemType = Object.assign(new ItemType(), { label: 'Person' });
const externalSource = { const externalSource = {
id: 'orcidV2', id: 'orcidV2',
name: 'orcidV2', name: 'orcidV2',
hierarchical: false hierarchical: false,
entityTypes: createSuccessfulRemoteDataObject$(createPaginatedList([itemType]))
} as ExternalSource; } as ExternalSource;
const externalEntries = [ const externalEntries = [
Object.assign({ Object.assign({

View File

@@ -7,7 +7,7 @@ import { RemoteData } from '../../../../../../core/data/remote-data';
import { PaginatedList } from '../../../../../../core/data/paginated-list.model'; import { PaginatedList } from '../../../../../../core/data/paginated-list.model';
import { ExternalSourceEntry } from '../../../../../../core/shared/external-source-entry.model'; import { ExternalSourceEntry } from '../../../../../../core/shared/external-source-entry.model';
import { ExternalSource } from '../../../../../../core/shared/external-source.model'; import { ExternalSource } from '../../../../../../core/shared/external-source.model';
import { startWith, switchMap } from 'rxjs/operators'; import { map, startWith, switchMap } from 'rxjs/operators';
import { PaginatedSearchOptions } from '../../../../../search/paginated-search-options.model'; import { PaginatedSearchOptions } from '../../../../../search/paginated-search-options.model';
import { Context } from '../../../../../../core/shared/context.model'; import { Context } from '../../../../../../core/shared/context.model';
import { ListableObject } from '../../../../../object-collection/shared/listable-object.model'; import { ListableObject } from '../../../../../object-collection/shared/listable-object.model';
@@ -22,6 +22,8 @@ import { Item } from '../../../../../../core/shared/item.model';
import { Collection } from '../../../../../../core/shared/collection.model'; import { Collection } from '../../../../../../core/shared/collection.model';
import { PaginationService } from '../../../../../../core/pagination/pagination.service'; import { PaginationService } from '../../../../../../core/pagination/pagination.service';
import { Observable, Subscription } from 'rxjs'; import { Observable, Subscription } from 'rxjs';
import { ItemType } from '../../../../../../core/shared/item-relationships/item-type.model';
import { getFirstCompletedRemoteData } from '../../../../../../core/shared/operators';
@Component({ @Component({
selector: 'ds-dynamic-lookup-relation-external-source-tab', selector: 'ds-dynamic-lookup-relation-external-source-tab',
@@ -116,6 +118,11 @@ export class DsDynamicLookupRelationExternalSourceTabComponent implements OnInit
*/ */
importObjectSub: Subscription; importObjectSub: Subscription;
/**
* The entity types compatible with the given external source
*/
relatedEntityType: ItemType;
constructor(private router: Router, constructor(private router: Router,
public searchConfigService: SearchConfigurationService, public searchConfigService: SearchConfigurationService,
private externalSourceService: ExternalSourceService, private externalSourceService: ExternalSourceService,
@@ -129,6 +136,15 @@ export class DsDynamicLookupRelationExternalSourceTabComponent implements OnInit
* Get the entries for the selected external source * Get the entries for the selected external source
*/ */
ngOnInit(): void { ngOnInit(): void {
this.externalSource.entityTypes.pipe(
getFirstCompletedRemoteData(),
map((entityTypesRD: RemoteData<PaginatedList<ItemType>>) => {
return (entityTypesRD.hasSucceeded && entityTypesRD.payload.totalElements > 0) ? entityTypesRD.payload.page[0] : null;
})
).subscribe((entityType: ItemType) => {
this.relatedEntityType = entityType;
});
this.resetRoute(); this.resetRoute();
this.entriesRD$ = this.searchConfigService.paginatedSearchOptions.pipe( this.entriesRD$ = this.searchConfigService.paginatedSearchOptions.pipe(
switchMap((searchOptions: PaginatedSearchOptions) => switchMap((searchOptions: PaginatedSearchOptions) =>
@@ -155,6 +171,7 @@ export class DsDynamicLookupRelationExternalSourceTabComponent implements OnInit
modalComp.collection = this.collection; modalComp.collection = this.collection;
modalComp.relationship = this.relationship; modalComp.relationship = this.relationship;
modalComp.label = this.label; modalComp.label = this.label;
modalComp.relatedEntityType = this.relatedEntityType;
this.importObjectSub = modalComp.importedObject.subscribe((object) => { this.importObjectSub = modalComp.importedObject.subscribe((object) => {
this.selectableListService.selectSingle(this.listId, object); this.selectableListService.selectSingle(this.listId, object);
this.importedObject.emit(object); this.importedObject.emit(object);

View File

@@ -17,13 +17,6 @@
<div id="external-source-entry-entities" class="mb-3"> <div id="external-source-entry-entities" class="mb-3">
<h5 class="font-weight-bold">{{ (labelPrefix + 'entities' | translate) }}</h5> <h5 class="font-weight-bold">{{ (labelPrefix + 'entities' | translate) }}</h5>
<div id="external-source-entry-collection" class="mb-3">
<div class="form-group">
<label for="collection">{{ (labelPrefix + 'collection' | translate) }}</label>
<input type="text" class="form-control" id="collection" placeholder="Enter collection ID" [(ngModel)]="collectionId">
</div>
</div>
<ds-search-results *ngIf="(localEntitiesRD$ | async)?.payload?.page?.length > 0" <ds-search-results *ngIf="(localEntitiesRD$ | async)?.payload?.page?.length > 0"
[searchResults]="(localEntitiesRD$ | async)" [searchResults]="(localEntitiesRD$ | async)"
[sortConfig]="this.lookupRelationService.searchConfig?.sort" [sortConfig]="this.lookupRelationService.searchConfig?.sort"

View File

@@ -86,7 +86,6 @@ describe('DsDynamicLookupRelationExternalSourceTabComponent', () => {
component.externalSourceEntry = entry; component.externalSourceEntry = entry;
component.label = label; component.label = label;
component.relationship = relationship; component.relationship = relationship;
component.collection = submissionCollection;
component.item = submissionItem; component.item = submissionItem;
fixture.detectChanges(); fixture.detectChanges();
}); });

View File

@@ -1,5 +1,5 @@
import { Component, EventEmitter, OnInit } from '@angular/core'; import { Component, EventEmitter, OnInit } from '@angular/core';
import { NgbActiveModal } from '@ng-bootstrap/ng-bootstrap'; import { NgbActiveModal, NgbModal, NgbModalRef } from '@ng-bootstrap/ng-bootstrap';
import { ExternalSourceEntry } from '../../../../../../../core/shared/external-source-entry.model'; import { ExternalSourceEntry } from '../../../../../../../core/shared/external-source-entry.model';
import { MetadataValue } from '../../../../../../../core/shared/metadata.models'; import { MetadataValue } from '../../../../../../../core/shared/metadata.models';
import { Metadata } from '../../../../../../../core/shared/metadata.utils'; import { Metadata } from '../../../../../../../core/shared/metadata.utils';
@@ -15,14 +15,16 @@ import { CollectionElementLinkType } from '../../../../../../object-collection/c
import { Context } from '../../../../../../../core/shared/context.model'; import { Context } from '../../../../../../../core/shared/context.model';
import { SelectableListService } from '../../../../../../object-list/selectable-list/selectable-list.service'; import { SelectableListService } from '../../../../../../object-list/selectable-list/selectable-list.service';
import { ListableObject } from '../../../../../../object-collection/shared/listable-object.model'; import { ListableObject } from '../../../../../../object-collection/shared/listable-object.model';
import { Collection } from '../../../../../../../core/shared/collection.model';
import { ItemDataService } from '../../../../../../../core/data/item-data.service'; import { ItemDataService } from '../../../../../../../core/data/item-data.service';
import { PaginationComponentOptions } from '../../../../../../pagination/pagination-component-options.model'; import { PaginationComponentOptions } from '../../../../../../pagination/pagination-component-options.model';
import { getRemoteDataPayload, getFirstSucceededRemoteData } from '../../../../../../../core/shared/operators'; import { getFirstSucceededRemoteData, getRemoteDataPayload } from '../../../../../../../core/shared/operators';
import { take } from 'rxjs/operators'; import { switchMap, take } from 'rxjs/operators';
import { ItemSearchResult } from '../../../../../../object-collection/shared/item-search-result.model'; import { ItemSearchResult } from '../../../../../../object-collection/shared/item-search-result.model';
import { NotificationsService } from '../../../../../../notifications/notifications.service'; import { NotificationsService } from '../../../../../../notifications/notifications.service';
import { TranslateService } from '@ngx-translate/core'; import { TranslateService } from '@ngx-translate/core';
import { ItemType } from '../../../../../../../core/shared/item-relationships/item-type.model';
import { SubmissionImportExternalCollectionComponent } from '../../../../../../../submission/import-external/import-external-collection/submission-import-external-collection.component';
import { CollectionListEntry } from '../../../../../../collection-dropdown/collection-dropdown.component';
/** /**
* The possible types of import for the external entry * The possible types of import for the external entry
@@ -67,16 +69,6 @@ export class ExternalSourceEntryImportModalComponent implements OnInit {
*/ */
item: Item; item: Item;
/**
* The collection the user is submitting in
*/
collection: Collection;
/**
* The ID of the collection to import entries to
*/
collectionId: string;
/** /**
* The current relationship-options used for filtering results * The current relationship-options used for filtering results
*/ */
@@ -147,8 +139,19 @@ export class ExternalSourceEntryImportModalComponent implements OnInit {
*/ */
authorityEnabled = false; authorityEnabled = false;
/**
* The entity types compatible with the given external source
*/
relatedEntityType: ItemType;
/**
* The modal for the collection selection
*/
modalRef: NgbModalRef;
constructor(public modal: NgbActiveModal, constructor(public modal: NgbActiveModal,
public lookupRelationService: LookupRelationService, public lookupRelationService: LookupRelationService,
private modalService: NgbModal,
private selectService: SelectableListService, private selectService: SelectableListService,
private itemService: ItemDataService, private itemService: ItemDataService,
private notificationsService: NotificationsService, private notificationsService: NotificationsService,
@@ -160,7 +163,6 @@ export class ExternalSourceEntryImportModalComponent implements OnInit {
const pagination = Object.assign(new PaginationComponentOptions(), { id: 'external-entry-import', pageSize: 5 }); const pagination = Object.assign(new PaginationComponentOptions(), { id: 'external-entry-import', pageSize: 5 });
this.searchOptions = Object.assign(new PaginatedSearchOptions({ query: this.externalSourceEntry.value, pagination: pagination })); this.searchOptions = Object.assign(new PaginatedSearchOptions({ query: this.externalSourceEntry.value, pagination: pagination }));
this.localEntitiesRD$ = this.lookupRelationService.getLocalResults(this.relationship, this.searchOptions); this.localEntitiesRD$ = this.lookupRelationService.getLocalResults(this.relationship, this.searchOptions);
this.collectionId = this.collection.id;
} }
/** /**
@@ -211,16 +213,26 @@ export class ExternalSourceEntryImportModalComponent implements OnInit {
* Create and import a new entity from the external entry * Create and import a new entity from the external entry
*/ */
importNewEntity() { importNewEntity() {
this.itemService.importExternalSourceEntry(this.externalSourceEntry, this.collectionId).pipe( this.modalRef = this.modalService.open(SubmissionImportExternalCollectionComponent, {
getFirstSucceededRemoteData(), size: 'lg',
getRemoteDataPayload(), });
take(1) this.modalRef.componentInstance.entityType = this.relatedEntityType.label;
this.modalRef.componentInstance.selectedEvent.pipe(
switchMap((collectionListEntry: CollectionListEntry) => {
return this.itemService.importExternalSourceEntry(this.externalSourceEntry, collectionListEntry.collection.id).pipe(
getFirstSucceededRemoteData(),
getRemoteDataPayload(),
take(1)
);
})
).subscribe((item: Item) => { ).subscribe((item: Item) => {
this.lookupRelationService.removeLocalResultsCache(); this.lookupRelationService.removeLocalResultsCache();
const searchResult = Object.assign(new ItemSearchResult(), { const searchResult = Object.assign(new ItemSearchResult(), {
indexableObject: item indexableObject: item
}); });
this.notificationsService.success(this.translateService.get(this.labelPrefix + this.label + '.added.new-entity')); this.notificationsService.success(this.translateService.get(this.labelPrefix + this.label + '.added.new-entity'));
this.modalRef.close();
this.importedObject.emit(searchResult); this.importedObject.emit(searchResult);
}); });
} }

View File

@@ -11,6 +11,9 @@ export const externalSourceOrcid: ExternalSource = {
entries: { entries: {
href: 'https://dspace7.4science.cloud/server/api/integration/externalsources/orcid/entries' href: 'https://dspace7.4science.cloud/server/api/integration/externalsources/orcid/entries'
}, },
entityTypes: {
href: 'https://dspace7.4science.cloud/server/api/integration/externalsources/my_staff_db/entityTypes'
},
self: { self: {
href: 'https://dspace7.4science.cloud/server/api/integration/externalsources/orcid' href: 'https://dspace7.4science.cloud/server/api/integration/externalsources/orcid'
} }
@@ -26,6 +29,9 @@ export const externalSourceCiencia: ExternalSource = {
entries: { entries: {
href: 'https://dspace7.4science.cloud/server/api/integration/externalsources/ciencia/entries' href: 'https://dspace7.4science.cloud/server/api/integration/externalsources/ciencia/entries'
}, },
entityTypes: {
href: 'https://dspace7.4science.cloud/server/api/integration/externalsources/my_staff_db/entityTypes'
},
self: { self: {
href: 'https://dspace7.4science.cloud/server/api/integration/externalsources/ciencia' href: 'https://dspace7.4science.cloud/server/api/integration/externalsources/ciencia'
} }
@@ -41,6 +47,9 @@ export const externalSourceMyStaffDb: ExternalSource = {
entries: { entries: {
href: 'https://dspace7.4science.cloud/server/api/integration/externalsources/my_staff_db/entries' href: 'https://dspace7.4science.cloud/server/api/integration/externalsources/my_staff_db/entries'
}, },
entityTypes: {
href: 'https://dspace7.4science.cloud/server/api/integration/externalsources/my_staff_db/entityTypes'
},
self: { self: {
href: 'https://dspace7.4science.cloud/server/api/integration/externalsources/my_staff_db' href: 'https://dspace7.4science.cloud/server/api/integration/externalsources/my_staff_db'
} }
@@ -53,6 +62,7 @@ export const externalSourceMyStaffDb: ExternalSource = {
export function getMockExternalSourceService(): ExternalSourceService { export function getMockExternalSourceService(): ExternalSourceService {
return jasmine.createSpyObj('ExternalSourceService', { return jasmine.createSpyObj('ExternalSourceService', {
findAll: jasmine.createSpy('findAll'), findAll: jasmine.createSpy('findAll'),
searchBy: jasmine.createSpy('searchBy'),
getExternalSourceEntries: jasmine.createSpy('getExternalSourceEntries'), getExternalSourceEntries: jasmine.createSpy('getExternalSourceEntries'),
}); });
} }

View File

@@ -53,7 +53,8 @@ import { FormComponent } from './form/form.component';
import { DsDynamicOneboxComponent } from './form/builder/ds-dynamic-form-ui/models/onebox/dynamic-onebox.component'; import { DsDynamicOneboxComponent } from './form/builder/ds-dynamic-form-ui/models/onebox/dynamic-onebox.component';
import { DsDynamicScrollableDropdownComponent } from './form/builder/ds-dynamic-form-ui/models/scrollable-dropdown/dynamic-scrollable-dropdown.component'; import { DsDynamicScrollableDropdownComponent } from './form/builder/ds-dynamic-form-ui/models/scrollable-dropdown/dynamic-scrollable-dropdown.component';
import { import {
DsDynamicFormControlContainerComponent, dsDynamicFormControlMapFn, DsDynamicFormControlContainerComponent,
dsDynamicFormControlMapFn,
} from './form/builder/ds-dynamic-form-ui/ds-dynamic-form-control-container.component'; } from './form/builder/ds-dynamic-form-ui/ds-dynamic-form-control-container.component';
import { DsDynamicFormComponent } from './form/builder/ds-dynamic-form-ui/ds-dynamic-form.component'; import { DsDynamicFormComponent } from './form/builder/ds-dynamic-form-ui/ds-dynamic-form.component';
import { DragClickDirective } from './utils/drag-click.directive'; import { DragClickDirective } from './utils/drag-click.directive';
@@ -202,6 +203,7 @@ import { EpersonSearchBoxComponent } from './resource-policies/form/eperson-grou
import { GroupSearchBoxComponent } from './resource-policies/form/eperson-group-list/group-search-box/group-search-box.component'; import { GroupSearchBoxComponent } from './resource-policies/form/eperson-group-list/group-search-box/group-search-box.component';
import { FileDownloadLinkComponent } from './file-download-link/file-download-link.component'; import { FileDownloadLinkComponent } from './file-download-link/file-download-link.component';
import { CollectionDropdownComponent } from './collection-dropdown/collection-dropdown.component'; import { CollectionDropdownComponent } from './collection-dropdown/collection-dropdown.component';
import { EntityDropdownComponent } from './entity-dropdown/entity-dropdown.component';
import { DsSelectComponent } from './ds-select/ds-select.component'; import { DsSelectComponent } from './ds-select/ds-select.component';
import { VocabularyTreeviewComponent } from './vocabulary-treeview/vocabulary-treeview.component'; import { VocabularyTreeviewComponent } from './vocabulary-treeview/vocabulary-treeview.component';
import { CurationFormComponent } from '../curation-form/curation-form.component'; import { CurationFormComponent } from '../curation-form/curation-form.component';
@@ -438,6 +440,7 @@ const COMPONENTS = [
BitstreamDownloadPageComponent, BitstreamDownloadPageComponent,
BitstreamRequestACopyPageComponent, BitstreamRequestACopyPageComponent,
CollectionDropdownComponent, CollectionDropdownComponent,
EntityDropdownComponent,
ExportMetadataSelectorComponent, ExportMetadataSelectorComponent,
ConfirmationModalComponent, ConfirmationModalComponent,
VocabularyTreeviewComponent, VocabularyTreeviewComponent,

View File

@@ -1,11 +1,16 @@
<div> <div>
<div class="modal-header">{{'dso-selector.create.collection.head' | translate}} <div class="modal-header">{{'dso-selector.select.collection.head' | translate}}
<button type="button" class="close" (click)="closeCollectionModal()" aria-label="Close"> <button type="button" class="close" (click)="closeCollectionModal()" aria-label="Close">
<span aria-hidden="true">×</span> <span aria-hidden="true">×</span>
</button> </button>
</div> </div>
<div class="modal-body"> <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)"
(searchComplete)="searchComplete()"
(theOnlySelectable)="theOnlySelectable($event)"
[entityType]="entityType">
</ds-collection-dropdown> </ds-collection-dropdown>
</div> </div>
</div> </div>

View File

@@ -1,10 +1,11 @@
import { Component, NO_ERRORS_SCHEMA, EventEmitter } from '@angular/core'; import { Component, EventEmitter, NO_ERRORS_SCHEMA } from '@angular/core';
import { waitForAsync, TestBed, ComponentFixture, inject } from '@angular/core/testing'; import { ComponentFixture, fakeAsync, inject, TestBed, waitForAsync } from '@angular/core/testing';
import { TranslateModule } from '@ngx-translate/core'; import { TranslateModule } from '@ngx-translate/core';
import { createTestComponent } from '../../../shared/testing/utils.test'; import { createTestComponent } from '../../../shared/testing/utils.test';
import { SubmissionImportExternalCollectionComponent } from './submission-import-external-collection.component'; import { SubmissionImportExternalCollectionComponent } from './submission-import-external-collection.component';
import { CollectionListEntry } from '../../../shared/collection-dropdown/collection-dropdown.component'; import { CollectionListEntry } from '../../../shared/collection-dropdown/collection-dropdown.component';
import { NgbActiveModal } from '@ng-bootstrap/ng-bootstrap'; import { NgbActiveModal } from '@ng-bootstrap/ng-bootstrap';
import { By } from '@angular/platform-browser';
describe('SubmissionImportExternalCollectionComponent test suite', () => { describe('SubmissionImportExternalCollectionComponent test suite', () => {
let comp: SubmissionImportExternalCollectionComponent; let comp: SubmissionImportExternalCollectionComponent;
@@ -76,6 +77,46 @@ describe('SubmissionImportExternalCollectionComponent test suite', () => {
expect(compAsAny.activeModal.dismiss).toHaveBeenCalled(); expect(compAsAny.activeModal.dismiss).toHaveBeenCalled();
}); });
it('should be in loading state when search is not completed', () => {
comp.loading = null;
expect(comp.isLoading()).toBeFalse();
comp.loading = true;
expect(comp.isLoading()).toBeTrue();
comp.loading = false;
expect(comp.isLoading()).toBeFalse();
});
it('should set loading variable to false on searchComplete event', () => {
comp.loading = null;
comp.searchComplete();
expect(comp.loading).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 { CollectionListEntry } from '../../../shared/collection-dropdown/collection-dropdown.component';
import { NgbActiveModal } from '@ng-bootstrap/ng-bootstrap'; import { NgbActiveModal } from '@ng-bootstrap/ng-bootstrap';
@@ -16,6 +16,16 @@ export class SubmissionImportExternalCollectionComponent {
*/ */
@Output() public selectedEvent = new EventEmitter<CollectionListEntry>(); @Output() public selectedEvent = new EventEmitter<CollectionListEntry>();
/**
* If present this value is used to filter collection list by entity type
*/
public entityType: string;
/**
* If collection searching is pending or not
*/
public loading = true;
/** /**
* Initialize the component variables. * Initialize the component variables.
* @param {NgbActiveModal} activeModal * @param {NgbActiveModal} activeModal
@@ -37,4 +47,28 @@ export class SubmissionImportExternalCollectionComponent {
public closeCollectionModal(): void { public closeCollectionModal(): void {
this.activeModal.dismiss(false); 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 searchComplete() {
this.loading = false;
}
/**
* If the component is in loading state.
*/
public isLoading(): boolean {
return !!this.loading;
}
} }

View File

@@ -1,5 +1,5 @@
<div class="modal-header"> <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" <button type="button" class="close"
(click)="closeMetadataModal()" aria-label="Close"> (click)="closeMetadataModal()" aria-label="Close">
<span aria-hidden="true">×</span> <span aria-hidden="true">×</span>

View File

@@ -1,6 +1,6 @@
import { Component, Input, OnInit } from '@angular/core'; import { Component, Input, OnInit } from '@angular/core';
import { Router } from '@angular/router'; 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 { ExternalSourceEntry } from '../../../core/shared/external-source-entry.model';
import { MetadataValue } from '../../../core/shared/metadata.models'; import { MetadataValue } from '../../../core/shared/metadata.models';
import { Metadata } from '../../../core/shared/metadata.utils'; import { Metadata } from '../../../core/shared/metadata.utils';
@@ -28,6 +28,10 @@ export class SubmissionImportExternalPreviewComponent implements OnInit {
* The entry metadata list * The entry metadata list
*/ */
public metadataList: { key: string, value: MetadataValue }[]; 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 * The modal for the entry preview
*/ */
@@ -77,6 +81,7 @@ export class SubmissionImportExternalPreviewComponent implements OnInit {
this.modalRef = this.modalService.open(SubmissionImportExternalCollectionComponent, { this.modalRef = this.modalService.open(SubmissionImportExternalCollectionComponent, {
size: 'lg', size: 'lg',
}); });
this.modalRef.componentInstance.entityType = this.labelPrefix;
this.closeMetadataModal(); this.closeMetadataModal();
this.modalRef.componentInstance.selectedEvent.pipe( this.modalRef.componentInstance.selectedEvent.pipe(

View File

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

View File

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

View File

@@ -1,17 +1,17 @@
<div class="container"> <div class="container">
<div class="row"> <div class="row">
<div class="col-md-12"> <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 <ds-submission-import-external-searchbar
[initExternalSourceData]="routeData" [initExternalSourceData]="reload$.value"
(externalSourceData) = "getExternalSourceData($event)"> (externalSourceData) = "getExternalSourceData($event)">
</ds-submission-import-external-searchbar> </ds-submission-import-external-searchbar>
</div> </div>
</div> </div>
<div class="row"> <div class="row" *ngIf="reload$.value.entity">
<div *ngIf="routeData.sourceId !== ''" class="col-md-12"> <div *ngIf="reload$.value.sourceId !== ''" class="col-md-12">
<ng-container *ngVar="(entriesRD$ | async) as entriesRD"> <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 <ds-viewable-collection *ngIf="entriesRD?.hasSucceeded && !(isLoading$ | async) && entriesRD?.payload?.page?.length > 0" @fadeIn
[objects]="entriesRD" [objects]="entriesRD"
[selectionConfig]="{ repeatable: repeatable, listId: listId }" [selectionConfig]="{ repeatable: repeatable, listId: listId }"
@@ -25,16 +25,17 @@
<ds-loading *ngIf="(isLoading$ | async)" <ds-loading *ngIf="(isLoading$ | async)"
message="{{'loading.search-results' | translate}}"></ds-loading> message="{{'loading.search-results' | translate}}"></ds-loading>
<div *ngIf="!(isLoading$ | async) && entriesRD?.payload?.page?.length === 0" id="empty-external-entry-list"> <div *ngIf="!(isLoading$ | async) && entriesRD?.payload?.page?.length === 0" id="empty-external-entry-list">
{{ 'search.results.empty' | translate }} <ds-alert [type]="'alert-info'">{{ 'search.results.empty' | translate }}</ds-alert>
</div> </div>
</ng-container> </ng-container>
</div> </div>
<div *ngIf="routeData.sourceId === ''" class="col-md-12"> <div *ngIf="reload$.value.sourceId === ''" class="col-md-12">
<ds-alert [type]="'alert-info'"> <ds-alert [type]="'alert-info'">
<p class="lead mb-0">{{'submission.import-external.page.hint' | translate}}</p> <p class="lead mb-0">{{'submission.import-external.page.hint' | translate}}</p>
</ds-alert> </ds-alert>
</div> </div>
</div> </div>
<div class="row"> <div class="row">
<div class="col-md-12"> <div class="col-md-12">
<hr> <hr>

View File

@@ -1,5 +1,5 @@
import { Component, NO_ERRORS_SCHEMA } from '@angular/core'; 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 { getTestScheduler } from 'jasmine-marbles';
import { TranslateModule } from '@ngx-translate/core'; import { TranslateModule } from '@ngx-translate/core';
@@ -102,17 +102,19 @@ describe('SubmissionImportExternalComponent test suite', () => {
it('Should init component properly (without route data)', () => { it('Should init component properly (without route data)', () => {
const expectedEntries = createSuccessfulRemoteDataObject(createPaginatedList([])); const expectedEntries = createSuccessfulRemoteDataObject(createPaginatedList([]));
comp.routeData = {entity: '', sourceId: '', query: '' };
spyOn(compAsAny.routeService, 'getQueryParameterValue').and.returnValue(observableOf('')); spyOn(compAsAny.routeService, 'getQueryParameterValue').and.returnValue(observableOf(''));
fixture.detectChanges(); fixture.detectChanges();
expect(comp.routeData).toEqual({ sourceId: '', query: '' }); expect(comp.routeData).toEqual({entity: '', sourceId: '', query: '' });
expect(comp.isLoading$.value).toBe(false); expect(comp.isLoading$.value).toBe(false);
expect(comp.entriesRD$.value).toEqual(expectedEntries); expect(comp.entriesRD$.value).toEqual(expectedEntries);
}); });
it('Should init component properly (with route data)', () => { it('Should init component properly (with route data)', () => {
comp.routeData = {entity: '', sourceId: '', query: '' };
spyOn(compAsAny, 'retrieveExternalSources'); 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(); fixture.detectChanges();
expect(compAsAny.retrieveExternalSources).toHaveBeenCalled(); expect(compAsAny.retrieveExternalSources).toHaveBeenCalled();
@@ -120,7 +122,7 @@ describe('SubmissionImportExternalComponent test suite', () => {
it('Should call \'getExternalSourceEntries\' properly', () => { it('Should call \'getExternalSourceEntries\' properly', () => {
spyOn(routeServiceStub, 'getQueryParameterValue').and.callFake((param) => { spyOn(routeServiceStub, 'getQueryParameterValue').and.callFake((param) => {
if (param === 'source') { if (param === 'sourceId') {
return observableOf('orcidV2'); return observableOf('orcidV2');
} else if (param === 'query') { } else if (param === 'query') {
return observableOf('test'); return observableOf('test');
@@ -136,15 +138,15 @@ describe('SubmissionImportExternalComponent test suite', () => {
}); });
it('Should call \'router.navigate\'', () => { it('Should call \'router.navigate\'', () => {
comp.routeData = { sourceId: '', query: '' }; comp.routeData = {entity: 'Person', sourceId: '', query: '' };
spyOn(compAsAny, 'retrieveExternalSources').and.callFake(() => null); spyOn(compAsAny, 'retrieveExternalSources').and.callFake(() => null);
compAsAny.router.navigate.and.returnValue( new Promise(() => {return;})); 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.schedule(() => comp.getExternalSourceData(event));
scheduler.flush(); 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', () => { 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(compAsAny.modalService.open).toHaveBeenCalledWith(SubmissionImportExternalPreviewComponent, { size: 'lg' });
expect(comp.modalRef.componentInstance.externalSourceEntry).toEqual(entry); 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

@@ -20,6 +20,7 @@ import { fadeIn } from '../../shared/animations/fade';
import { PageInfo } from '../../core/shared/page-info.model'; import { PageInfo } from '../../core/shared/page-info.model';
import { hasValue, isNotEmpty } from '../../shared/empty.util'; import { hasValue, isNotEmpty } from '../../shared/empty.util';
import { getFinishedRemoteData } from '../../core/shared/operators'; import { getFinishedRemoteData } from '../../core/shared/operators';
import { NONE_ENTITY_TYPE } from '../../core/shared/item-relationships/item-type.resource-type';
/** /**
* This component allows to submit a new workspaceitem importing the data from an external source. * This component allows to submit a new workspaceitem importing the data from an external source.
@@ -45,9 +46,10 @@ export class SubmissionImportExternalComponent implements OnInit, OnDestroy {
*/ */
public isLoading$: BehaviorSubject<boolean> = new BehaviorSubject<boolean>(false); 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: '', query: '',
source: '' sourceId: ''
}); });
/** /**
* Configuration to use for the import buttons * Configuration to use for the import buttons
@@ -109,11 +111,10 @@ export class SubmissionImportExternalComponent implements OnInit, OnDestroy {
* Get the entries for the selected external source and set initial configuration. * Get the entries for the selected external source and set initial configuration.
*/ */
ngOnInit(): void { ngOnInit(): void {
this.label = 'Journal';
this.listId = 'list-submission-external-sources'; this.listId = 'list-submission-external-sources';
this.context = Context.EntitySearchModalWithNameVariants; this.context = Context.EntitySearchModalWithNameVariants;
this.repeatable = false; this.repeatable = false;
this.routeData = {sourceId: '', query: ''}; this.routeData = {entity: '', sourceId: '', query: ''};
this.importConfig = { this.importConfig = {
buttonLabel: 'submission.sections.describe.relationship-lookup.external-source.import-button-title.' + this.label buttonLabel: 'submission.sections.describe.relationship-lookup.external-source.import-button-title.' + this.label
}; };
@@ -121,12 +122,14 @@ export class SubmissionImportExternalComponent implements OnInit, OnDestroy {
this.isLoading$ = new BehaviorSubject(false); this.isLoading$ = new BehaviorSubject(false);
this.subs.push(combineLatest( this.subs.push(combineLatest(
[ [
this.routeService.getQueryParameterValue('source'), this.routeService.getQueryParameterValue('entity'),
this.routeService.getQueryParameterValue('sourceId'),
this.routeService.getQueryParameterValue('query') this.routeService.getQueryParameterValue('query')
]).pipe( ]).pipe(
take(1) take(1)
).subscribe(([source, query]: [string, string]) => { ).subscribe(([entity, sourceId, query]: [string, string, string]) => {
this.reload$.next({query: query, source: source}); this.reload$.next({entity: entity || NONE_ENTITY_TYPE, query: query, sourceId: sourceId});
this.selectLabel(entity);
this.retrieveExternalSources(); this.retrieveExternalSources();
})); }));
} }
@@ -138,11 +141,11 @@ export class SubmissionImportExternalComponent implements OnInit, OnDestroy {
this.router.navigate( this.router.navigate(
[], [],
{ {
queryParams: {source: event.sourceId, query: event.query}, queryParams: event,
replaceUrl: true replaceUrl: true
} }
).then(() => { ).then(() => {
this.reload$.next({source: event.sourceId, query: event.query}); this.reload$.next(event);
this.retrieveExternalSources(); this.retrieveExternalSources();
}); });
} }
@@ -157,6 +160,7 @@ export class SubmissionImportExternalComponent implements OnInit, OnDestroy {
}); });
const modalComp = this.modalRef.componentInstance; const modalComp = this.modalRef.componentInstance;
modalComp.externalSourceEntry = entry; modalComp.externalSourceEntry = entry;
modalComp.labelPrefix = this.label;
} }
/** /**
@@ -173,28 +177,23 @@ export class SubmissionImportExternalComponent implements OnInit, OnDestroy {
} }
/** /**
* Retrieve external source entries * Retrieve external source entries.
*
* @param source The source tupe
* @param query The query string to search
*/ */
private retrieveExternalSources(): void { private retrieveExternalSources(): void {
if (hasValue(this.retrieveExternalSourcesSub)) { if (hasValue(this.retrieveExternalSourcesSub)) {
this.retrieveExternalSourcesSub.unsubscribe(); this.retrieveExternalSourcesSub.unsubscribe();
} }
this.retrieveExternalSourcesSub = this.reload$.pipe( this.retrieveExternalSourcesSub = this.reload$.pipe(
filter((sourceQueryObject: { source: string, query: string }) => isNotEmpty(sourceQueryObject.source) && isNotEmpty(sourceQueryObject.query)), filter((sourceQueryObject: ExternalSourceData) => isNotEmpty(sourceQueryObject.sourceId) && isNotEmpty(sourceQueryObject.query)),
switchMap((sourceQueryObject: { source: string, query: string }) => { switchMap((sourceQueryObject: ExternalSourceData) => {
const source = sourceQueryObject.source;
const query = sourceQueryObject.query; const query = sourceQueryObject.query;
this.routeData.sourceId = source; this.routeData = sourceQueryObject;
this.routeData.query = query;
return this.searchConfigService.paginatedSearchOptions.pipe( return this.searchConfigService.paginatedSearchOptions.pipe(
tap((v) => this.isLoading$.next(true)), tap(() => this.isLoading$.next(true)),
filter((searchOptions) => searchOptions.query === query), filter((searchOptions) => searchOptions.query === query),
mergeMap((searchOptions) => this.externalService.getExternalSourceEntries(this.routeData.sourceId, searchOptions).pipe( mergeMap((searchOptions) => this.externalService.getExternalSourceEntries(this.routeData.sourceId, searchOptions).pipe(
getFinishedRemoteData(), getFinishedRemoteData(),
)), ))
); );
} }
), ),
@@ -204,4 +203,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

@@ -905,6 +905,8 @@
"collection.form.title": "Name", "collection.form.title": "Name",
"collection.form.entityType": "Entity Type",
"collection.listelement.badge": "Collection", "collection.listelement.badge": "Collection",
@@ -1278,6 +1280,8 @@
"dso-selector.placeholder": "Search for a {{ type }}", "dso-selector.placeholder": "Search for a {{ type }}",
"dso-selector.select.collection.head": "Select a collection",
"dso-selector.set-scope.community.head": "Select a search scope", "dso-selector.set-scope.community.head": "Select a search scope",
"dso-selector.set-scope.community.button": "Search all of DSpace", "dso-selector.set-scope.community.button": "Search all of DSpace",
@@ -2612,6 +2616,7 @@
"nav.user.description" : "User profile bar", "nav.user.description" : "User profile bar",
"none.listelement.badge": "Item",
"orgunit.listelement.badge": "Organizational Unit", "orgunit.listelement.badge": "Organizational Unit",
@@ -3432,6 +3437,22 @@
"submission.import-external.title": "Import metadata from an external source", "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.title.none": "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.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.back-to-my-dspace": "Back to MyDSpace",