mirror of
https://github.com/DSpace/dspace-angular.git
synced 2025-10-07 01:54:15 +00:00
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:
@@ -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 {
|
||||
DynamicFormControlModel,
|
||||
DynamicFormOptionConfig,
|
||||
DynamicFormService,
|
||||
DynamicInputModel,
|
||||
DynamicTextAreaModel
|
||||
DynamicSelectModel
|
||||
} from '@ng-dynamic-forms/core';
|
||||
|
||||
import { Collection } from '../../core/shared/collection.model';
|
||||
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 { CommunityDataService } from '../../core/data/community-data.service';
|
||||
import { AuthService } from '../../core/auth/auth.service';
|
||||
import { RequestService } from '../../core/data/request.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
|
||||
@@ -22,7 +31,7 @@ import { ObjectCacheService } from '../../core/cache/object-cache.service';
|
||||
styleUrls: ['../../shared/comcol-forms/comcol-form/comcol-form.component.scss'],
|
||||
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
|
||||
*/
|
||||
@@ -34,46 +43,16 @@ export class CollectionFormComponent extends ComColFormComponent<Collection> {
|
||||
type = Collection.type;
|
||||
|
||||
/**
|
||||
* The dynamic form fields used for creating/editing a collection
|
||||
* @type {(DynamicInputModel | DynamicTextAreaModel)[]}
|
||||
* The dynamic form field used for entity type selection
|
||||
* @type {DynamicSelectModel<string>}
|
||||
*/
|
||||
formModel: 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',
|
||||
}),
|
||||
new DynamicTextAreaModel({
|
||||
id: 'provenance',
|
||||
name: 'dc.description.provenance',
|
||||
}),
|
||||
];
|
||||
entityTypeSelection: DynamicSelectModel<string> = new DynamicSelectModel(collectionFormEntityTypeSelectionConfig);
|
||||
|
||||
/**
|
||||
* The dynamic form fields used for creating/editing a collection
|
||||
* @type {DynamicFormControlModel[]}
|
||||
*/
|
||||
formModel: DynamicFormControlModel[];
|
||||
|
||||
public constructor(protected formService: DynamicFormService,
|
||||
protected translate: TranslateService,
|
||||
@@ -81,7 +60,43 @@ export class CollectionFormComponent extends ComColFormComponent<Collection> {
|
||||
protected authService: AuthService,
|
||||
protected dsoService: CommunityDataService,
|
||||
protected requestService: RequestService,
|
||||
protected objectCache: ObjectCacheService) {
|
||||
protected objectCache: ObjectCacheService,
|
||||
protected entityTypeService: EntityTypeService) {
|
||||
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();
|
||||
});
|
||||
|
||||
}
|
||||
}
|
||||
|
@@ -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',
|
||||
})
|
||||
];
|
@@ -17,7 +17,7 @@ import {
|
||||
createFailedRemoteDataObject$,
|
||||
createSuccessfulRemoteDataObject,
|
||||
createSuccessfulRemoteDataObject$
|
||||
} from 'src/app/shared/remote-data.utils';
|
||||
} from '../../shared/remote-data.utils';
|
||||
import { cold, getTestScheduler, hot } from 'jasmine-marbles';
|
||||
import { TestScheduler } from 'rxjs/testing';
|
||||
import { Observable } from 'rxjs';
|
||||
|
@@ -27,12 +27,7 @@ import { CommunityDataService } from './community-data.service';
|
||||
import { DSOChangeAnalyzer } from './dso-change-analyzer.service';
|
||||
import { PaginatedList } from './paginated-list.model';
|
||||
import { RemoteData } from './remote-data';
|
||||
import {
|
||||
ContentSourceRequest,
|
||||
FindListOptions,
|
||||
UpdateContentSourceRequest,
|
||||
RestRequest
|
||||
} from './request.models';
|
||||
import { ContentSourceRequest, FindListOptions, RestRequest, UpdateContentSourceRequest } from './request.models';
|
||||
import { RequestService } from './request.service';
|
||||
import { BitstreamDataService } from './bitstream-data.service';
|
||||
|
||||
@@ -84,16 +79,48 @@ export class CollectionDataService extends ComColDataService<Collection> {
|
||||
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
|
||||
*
|
||||
* @param communityId The community id
|
||||
* @param query limit the returned collection to those with metadata values matching the query terms.
|
||||
* @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>>>
|
||||
* 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';
|
||||
options = Object.assign({}, options, {
|
||||
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));
|
||||
}
|
||||
|
||||
|
@@ -10,13 +10,14 @@ import { NotificationsService } from '../../shared/notifications/notifications.s
|
||||
import { HttpClient } from '@angular/common/http';
|
||||
import { DefaultChangeAnalyzer } from './default-change-analyzer.service';
|
||||
import { Injectable } from '@angular/core';
|
||||
import { FindListOptions } from './request.models';
|
||||
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 { RelationshipType } from '../shared/item-relationships/relationship-type.model';
|
||||
import { PaginatedList } from './paginated-list.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';
|
||||
|
||||
/**
|
||||
@@ -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
|
||||
* @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> {
|
||||
|
||||
@@ -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
|
||||
* @param entityTypeId
|
||||
|
@@ -1,10 +1,15 @@
|
||||
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 { excludeFromEquals } from '../utilities/equals.decorators';
|
||||
import { EXTERNAL_SOURCE } from './external-source.resource-type';
|
||||
import { HALLink } from './hal-link.model';
|
||||
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
|
||||
@@ -38,6 +43,13 @@ export class ExternalSource extends CacheableObject {
|
||||
@autoserialize
|
||||
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
|
||||
*/
|
||||
@@ -45,5 +57,6 @@ export class ExternalSource extends CacheableObject {
|
||||
_links: {
|
||||
self: HALLink;
|
||||
entries: HALLink;
|
||||
entityTypes: HALLink;
|
||||
};
|
||||
}
|
||||
|
@@ -6,5 +6,9 @@ import { ResourceType } from '../resource-type';
|
||||
* Needs to be in a separate file to prevent circular
|
||||
* dependencies in webpack.
|
||||
*/
|
||||
|
||||
export const ITEM_TYPE = new ResourceType('entitytype');
|
||||
|
||||
/**
|
||||
* The unset entity type
|
||||
*/
|
||||
export const NONE_ENTITY_TYPE = 'none';
|
||||
|
@@ -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>
|
@@ -0,0 +1,16 @@
|
||||
.parent {
|
||||
display: flex;
|
||||
}
|
||||
|
||||
.upload {
|
||||
flex: auto;
|
||||
}
|
||||
|
||||
.add {
|
||||
flex: initial;
|
||||
}
|
||||
|
||||
#entityControlsDropdownMenu {
|
||||
min-width: 18rem;
|
||||
box-shadow: $btn-focus-box-shadow;
|
||||
}
|
@@ -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;
|
||||
}
|
||||
}
|
@@ -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());
|
||||
}
|
||||
}
|
@@ -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>
|
@@ -0,0 +1,16 @@
|
||||
.parent {
|
||||
display: flex;
|
||||
}
|
||||
|
||||
.upload {
|
||||
flex: auto;
|
||||
}
|
||||
|
||||
.add {
|
||||
flex: initial;
|
||||
}
|
||||
|
||||
#entityControlsDropdownMenu {
|
||||
min-width: 18rem;
|
||||
box-shadow: $btn-focus-box-shadow;
|
||||
}
|
@@ -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;
|
||||
}
|
||||
}
|
@@ -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());
|
||||
}
|
||||
}
|
@@ -8,14 +8,10 @@
|
||||
|
||||
</div>
|
||||
<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}}">
|
||||
<i class="fa fa-plus" aria-hidden="true"></i>
|
||||
</button>
|
||||
<ds-my-dspace-new-submission-dropdown></ds-my-dspace-new-submission-dropdown>
|
||||
</div>
|
||||
<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}}">
|
||||
<i class="fa fa-file-import" aria-hidden="true"></i>
|
||||
</a>
|
||||
<ds-my-dspace-new-external-dropdown></ds-my-dspace-new-external-dropdown>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
@@ -1,7 +1,6 @@
|
||||
import { ChangeDetectorRef, Component, NO_ERRORS_SCHEMA } from '@angular/core';
|
||||
import { ComponentFixture, inject, TestBed, waitForAsync } from '@angular/core/testing';
|
||||
import { RouterTestingModule } from '@angular/router/testing';
|
||||
import { By } from '@angular/platform-browser';
|
||||
|
||||
import { TranslateLoader, TranslateModule } from '@ngx-translate/core';
|
||||
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 { CookieServiceMock } from '../../shared/mocks/cookie.service.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', () => {
|
||||
|
||||
@@ -62,6 +63,7 @@ describe('MyDSpaceNewSubmissionComponent test', () => {
|
||||
{ provide: HttpXsrfTokenExtractor, useValue: new HttpXsrfTokenExtractorMock('mock-token') },
|
||||
{ provide: CookieService, useValue: new CookieServiceMock() },
|
||||
{ provide: HostWindowService, useValue: new HostWindowServiceStub(800) },
|
||||
{ provide: EntityTypeService, useValue: getMockEntityTypeService() },
|
||||
],
|
||||
schemas: [NO_ERRORS_SCHEMA]
|
||||
}).compileComponents();
|
||||
@@ -104,20 +106,6 @@ describe('MyDSpaceNewSubmissionComponent test', () => {
|
||||
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) => {
|
||||
spyOn((comp as any).modalService, 'open').and.returnValue({ result: new Promise((res, rej) => {/****/}) });
|
||||
comp.afterFileLoaded(['']);
|
||||
|
@@ -14,7 +14,6 @@ import { HALEndpointService } from '../../core/shared/hal-endpoint.service';
|
||||
import { NotificationType } from '../../shared/notifications/models/notification-type';
|
||||
import { hasValue } from '../../shared/empty.util';
|
||||
import { SearchResult } from '../../shared/search/search-result.model';
|
||||
import { CreateItemParentSelectorComponent } from '../../shared/dso-selector/modal-wrappers/create-item-parent-selector/create-item-parent-selector.component';
|
||||
import { CollectionSelectorComponent } from '../collection-selector/collection-selector.component';
|
||||
import { UploaderComponent } from '../../shared/uploader/uploader.component';
|
||||
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));
|
||||
}
|
||||
|
||||
/**
|
||||
* 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
|
||||
*/
|
||||
|
@@ -11,6 +11,8 @@ import { MyDSpaceGuard } from './my-dspace.guard';
|
||||
import { MyDSpaceConfigurationService } from './my-dspace-configuration.service';
|
||||
import { CollectionSelectorComponent } from './collection-selector/collection-selector.component';
|
||||
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';
|
||||
|
||||
const DECLARATIONS = [
|
||||
@@ -18,7 +20,9 @@ const DECLARATIONS = [
|
||||
ThemedMyDSpacePageComponent,
|
||||
MyDSpaceResultsComponent,
|
||||
MyDSpaceNewSubmissionComponent,
|
||||
CollectionSelectorComponent
|
||||
CollectionSelectorComponent,
|
||||
MyDSpaceNewSubmissionDropdownComponent,
|
||||
MyDSpaceNewExternalDropdownComponent
|
||||
];
|
||||
|
||||
@NgModule({
|
||||
|
@@ -1,33 +1,31 @@
|
||||
<div class="form-group w-100 pr-2 pl-2">
|
||||
<input *ngIf="searchField"
|
||||
type="search"
|
||||
class="form-control w-100"
|
||||
(click)="$event.stopPropagation();"
|
||||
placeholder="{{ 'submission.sections.general.search-collection' | translate }}"
|
||||
[formControl]="searchField"
|
||||
#searchFieldEl>
|
||||
<div *ngIf="searchField" class="form-group w-100 pr-2 pl-2">
|
||||
<input type="search"
|
||||
class="form-control w-100"
|
||||
(click)="$event.stopPropagation();"
|
||||
placeholder="{{ 'submission.sections.general.search-collection' | translate }}"
|
||||
[formControl]="searchField"
|
||||
#searchFieldEl>
|
||||
</div>
|
||||
<div class="dropdown-divider"></div>
|
||||
<div
|
||||
class="scrollable-menu"
|
||||
aria-labelledby="dropdownMenuButton"
|
||||
(scroll)="onScroll($event)">
|
||||
<div
|
||||
infiniteScroll
|
||||
[infiniteScrollDistance]="2"
|
||||
[infiniteScrollThrottle]="300"
|
||||
[infiniteScrollUpDistance]="1.5"
|
||||
[infiniteScrollContainer]="'.scrollable-menu'"
|
||||
[fromRoot]="true"
|
||||
(scrolled)="onScrollDown()">
|
||||
<button class="dropdown-item disabled" *ngIf="searchListCollection?.length == 0 && !(isLoadingList | async)">
|
||||
{{'submission.sections.general.no-collection' | translate}}
|
||||
</button>
|
||||
<button
|
||||
*ngFor="let listItem of searchListCollection"
|
||||
class="dropdown-item collection-item"
|
||||
title="{{ listItem.collection.name }}"
|
||||
(click)="onSelect(listItem)">
|
||||
<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="searchListCollection?.length == 0 && !(isLoading | async)">
|
||||
{{'submission.sections.general.no-collection' | translate}}
|
||||
</button>
|
||||
<ng-container *ngIf="searchListCollection?.length > 0 && !(isLoading | async)">
|
||||
<button *ngFor="let listItem of searchListCollection"
|
||||
class="dropdown-item collection-item"
|
||||
title="{{ listItem.collection.name }}"
|
||||
(click)="onSelect(listItem)">
|
||||
<ul class="list-unstyled mb-0">
|
||||
<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>
|
||||
@@ -35,9 +33,10 @@
|
||||
<li class="list-item text-truncate text-primary font-weight-bold">{{ listItem.collection.name}}</li>
|
||||
</ul>
|
||||
</button>
|
||||
<button class="dropdown-item disabled" *ngIf="(isLoadingList | async)" >
|
||||
<ds-loading message="{{'loading.default' | translate}}">
|
||||
</ds-loading>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</ng-container>
|
||||
<button class="dropdown-item disabled" *ngIf="(isLoading | async)">
|
||||
<ds-loading message="{{'loading.default' | translate}}">
|
||||
</ds-loading>
|
||||
</button>
|
||||
|
||||
</div>
|
||||
|
@@ -5,21 +5,16 @@ import { By } from '@angular/platform-browser';
|
||||
import { getTestScheduler } from 'jasmine-marbles';
|
||||
import { TestScheduler } from 'rxjs/testing';
|
||||
import { TranslateLoader, TranslateModule } from '@ngx-translate/core';
|
||||
import { of as observableOf } from 'rxjs';
|
||||
|
||||
import { CollectionDropdownComponent } from './collection-dropdown.component';
|
||||
import { RemoteData } from '../../core/data/remote-data';
|
||||
import { buildPaginatedList, PaginatedList } from '../../core/data/paginated-list.model';
|
||||
import { createSuccessfulRemoteDataObject, createSuccessfulRemoteDataObject$ } from '../remote-data.utils';
|
||||
import { buildPaginatedList } from '../../core/data/paginated-list.model';
|
||||
import { createSuccessfulRemoteDataObject$ } from '../remote-data.utils';
|
||||
import { PageInfo } from '../../core/shared/page-info.model';
|
||||
import { Collection } from '../../core/shared/collection.model';
|
||||
import { CollectionDataService } from '../../core/data/collection-data.service';
|
||||
import { TranslateLoaderMock } from '../mocks/translate-loader.mock';
|
||||
import { Community } from '../../core/shared/community.model';
|
||||
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(), {
|
||||
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', () => {
|
||||
let component: CollectionDropdownComponent;
|
||||
let componentAsAny: any;
|
||||
@@ -117,12 +101,16 @@ describe('CollectionDropdownComponent', () => {
|
||||
let scheduler: TestScheduler;
|
||||
|
||||
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 paginatedCollectionRD$ = createSuccessfulRemoteDataObject$(paginatedCollection);
|
||||
|
||||
const paginatedOneElementCollection = buildPaginatedList(new PageInfo(), [collections[0]]);
|
||||
const paginatedOneElementCollectionRD$ = createSuccessfulRemoteDataObject$(paginatedOneElementCollection);
|
||||
|
||||
beforeEach(waitForAsync(() => {
|
||||
TestBed.configureTestingModule({
|
||||
imports: [
|
||||
@@ -150,6 +138,7 @@ describe('CollectionDropdownComponent', () => {
|
||||
component = fixture.componentInstance;
|
||||
componentAsAny = component;
|
||||
componentAsAny.collectionDataService.getAuthorizedCollection.and.returnValue(paginatedCollectionRD$);
|
||||
componentAsAny.collectionDataService.getAuthorizedCollectionByEntityType.and.returnValue(paginatedCollectionRD$);
|
||||
});
|
||||
|
||||
it('should init component with collection list', () => {
|
||||
@@ -211,10 +200,10 @@ describe('CollectionDropdownComponent', () => {
|
||||
});
|
||||
|
||||
it('should change loader status', () => {
|
||||
spyOn(component.isLoadingList, 'next').and.callThrough();
|
||||
spyOn(component.isLoading, 'next').and.callThrough();
|
||||
component.hideShowLoader(true);
|
||||
|
||||
expect(component.isLoadingList.next).toHaveBeenCalledWith(true);
|
||||
expect(component.isLoading.next).toHaveBeenCalledWith(true);
|
||||
});
|
||||
|
||||
it('reset pagination fields', () => {
|
||||
@@ -225,4 +214,36 @@ describe('CollectionDropdownComponent', () => {
|
||||
expect(component.hasNextPage).toEqual(true);
|
||||
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);
|
||||
});
|
||||
});
|
||||
|
@@ -4,14 +4,15 @@ import {
|
||||
ElementRef,
|
||||
EventEmitter,
|
||||
HostListener,
|
||||
Input,
|
||||
OnDestroy,
|
||||
OnInit,
|
||||
Output
|
||||
} from '@angular/core';
|
||||
import { FormControl } from '@angular/forms';
|
||||
|
||||
import { BehaviorSubject, Observable, Subscription } from 'rxjs';
|
||||
import { debounceTime, distinctUntilChanged, map, mergeMap, reduce, startWith, switchMap } from 'rxjs/operators';
|
||||
import { BehaviorSubject, from as observableFrom, Observable, of as observableOf, Subscription } from 'rxjs';
|
||||
import { debounceTime, distinctUntilChanged, map, mergeMap, reduce, startWith, switchMap, take } from 'rxjs/operators';
|
||||
|
||||
import { hasValue } from '../empty.util';
|
||||
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 { Collection } from '../../core/shared/collection.model';
|
||||
import { followLink } from '../utils/follow-link-config.model';
|
||||
import {
|
||||
getFirstSucceededRemoteDataPayload,
|
||||
getFirstSucceededRemoteWithNotEmptyData
|
||||
} from '../../core/shared/operators';
|
||||
import { getFirstCompletedRemoteData, getFirstSucceededRemoteDataPayload } from '../../core/shared/operators';
|
||||
|
||||
/**
|
||||
* 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
|
||||
*/
|
||||
isLoadingList: BehaviorSubject<boolean> = new BehaviorSubject(false);
|
||||
isLoading: BehaviorSubject<boolean> = new BehaviorSubject(false);
|
||||
|
||||
/**
|
||||
* A numeric representig current page
|
||||
* A numeric representing current page
|
||||
*/
|
||||
currentPage: number;
|
||||
|
||||
@@ -102,10 +100,25 @@ export class CollectionDropdownComponent implements OnInit, OnDestroy {
|
||||
hasNextPage: boolean;
|
||||
|
||||
/**
|
||||
* Current seach query used to filter collection list
|
||||
* Current search query used to filter collection list
|
||||
*/
|
||||
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(
|
||||
private changeDetectorRef: ChangeDetectorRef,
|
||||
private collectionDataService: CollectionDataService,
|
||||
@@ -132,6 +145,7 @@ export class CollectionDropdownComponent implements OnInit, OnDestroy {
|
||||
* Initialize collection list
|
||||
*/
|
||||
ngOnInit() {
|
||||
this.isLoading.next(false);
|
||||
this.subs.push(this.searchField.valueChanges.pipe(
|
||||
debounceTime(500),
|
||||
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() {
|
||||
if ( this.hasNextPage ) {
|
||||
@@ -175,6 +189,7 @@ export class CollectionDropdownComponent implements OnInit, OnDestroy {
|
||||
* the selected [CollectionListEntry]
|
||||
*/
|
||||
onSelect(event: CollectionListEntry) {
|
||||
this.isLoading.next(true);
|
||||
this.selectionChange.emit(event);
|
||||
}
|
||||
|
||||
@@ -184,36 +199,57 @@ export class CollectionDropdownComponent implements OnInit, OnDestroy {
|
||||
* @param page page number
|
||||
*/
|
||||
populateCollectionList(query: string, page: number) {
|
||||
this.isLoadingList.next(true);
|
||||
this.isLoading.next(true);
|
||||
// Set the pagination info
|
||||
const findOptions: FindListOptions = {
|
||||
elementsPerPage: 10,
|
||||
currentPage: page
|
||||
};
|
||||
this.searchListCollection$ = this.collectionDataService
|
||||
.getAuthorizedCollection(query, findOptions, true, false, followLink('parentCommunity'))
|
||||
.pipe(
|
||||
getFirstSucceededRemoteWithNotEmptyData(),
|
||||
switchMap((collections: RemoteData<PaginatedList<Collection>>) => {
|
||||
if ( (this.searchListCollection.length + findOptions.elementsPerPage) >= collections.payload.totalElements ) {
|
||||
let searchListService$: Observable<RemoteData<PaginatedList<Collection>>>;
|
||||
if (this.entityType) {
|
||||
searchListService$ = this.collectionDataService
|
||||
.getAuthorizedCollectionByEntityType(
|
||||
query,
|
||||
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;
|
||||
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(
|
||||
(next) => { this.searchListCollection.push(...next); }, undefined,
|
||||
() => { this.hideShowLoader(false); this.changeDetectorRef.detectChanges(); }
|
||||
));
|
||||
this.subs.push(
|
||||
this.searchListCollection$.subscribe((list: CollectionListEntry[]) => {
|
||||
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
|
||||
*/
|
||||
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 }
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
@@ -26,7 +26,8 @@ describe('AuthorizedCollectionSelectorComponent', () => {
|
||||
id: 'authorized-collection'
|
||||
});
|
||||
collectionService = jasmine.createSpyObj('collectionService', {
|
||||
getAuthorizedCollection: createSuccessfulRemoteDataObject$(createPaginatedList([collection]))
|
||||
getAuthorizedCollection: createSuccessfulRemoteDataObject$(createPaginatedList([collection])),
|
||||
getAuthorizedCollectionByEntityType: createSuccessfulRemoteDataObject$(createPaginatedList([collection]))
|
||||
});
|
||||
notificationsService = jasmine.createSpyObj('notificationsService', ['error']);
|
||||
TestBed.configureTestingModule({
|
||||
@@ -49,12 +50,27 @@ describe('AuthorizedCollectionSelectorComponent', () => {
|
||||
});
|
||||
|
||||
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) => {
|
||||
expect(collectionService.getAuthorizedCollection).toHaveBeenCalled();
|
||||
expect(collectionService.getAuthorizedCollection).toHaveBeenCalled();
|
||||
expect(resultRD.payload.page.length).toEqual(1);
|
||||
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();
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
@@ -1,8 +1,8 @@
|
||||
import { Component } from '@angular/core';
|
||||
import { Component, Input } from '@angular/core';
|
||||
import { DSOSelectorComponent } from '../dso-selector.component';
|
||||
import { SearchService } from '../../../../core/shared/search/search.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 { map } from 'rxjs/operators';
|
||||
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 { NotificationsService } from '../../../notifications/notifications.service';
|
||||
import { TranslateService } from '@ngx-translate/core';
|
||||
import { Collection } from '../../../../core/shared/collection.model';
|
||||
import { FindListOptions } from '../../../../core/data/request.models';
|
||||
|
||||
@Component({
|
||||
selector: 'ds-authorized-collection-selector',
|
||||
@@ -24,6 +26,11 @@ import { TranslateService } from '@ngx-translate/core';
|
||||
* Component rendering a list of collections to select from
|
||||
*/
|
||||
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,
|
||||
protected collectionDataService: CollectionDataService,
|
||||
protected notifcationsService: NotificationsService,
|
||||
@@ -44,10 +51,23 @@ export class AuthorizedCollectionSelectorComponent extends DSOSelectorComponent
|
||||
* @param page Page to retrieve
|
||||
*/
|
||||
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,
|
||||
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(),
|
||||
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,
|
||||
|
@@ -6,6 +6,9 @@
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<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>
|
||||
|
@@ -69,4 +69,10 @@ describe('CreateItemParentSelectorComponent', () => {
|
||||
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 } });
|
||||
});
|
||||
});
|
||||
|
@@ -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 { DSpaceObjectType } from '../../../../core/shared/dspace-object-type.model';
|
||||
import { DSpaceObject } from '../../../../core/shared/dspace-object.model';
|
||||
@@ -22,6 +22,11 @@ export class CreateItemParentSelectorComponent extends DSOSelectorModalWrapperCo
|
||||
action = SelectorActionType.CREATE;
|
||||
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) {
|
||||
super(activeModal, route);
|
||||
}
|
||||
@@ -35,6 +40,9 @@ export class CreateItemParentSelectorComponent extends DSOSelectorModalWrapperCo
|
||||
['collection']: dso.uuid,
|
||||
}
|
||||
};
|
||||
if (this.entityType) {
|
||||
navigationExtras.queryParams.entityType = this.entityType;
|
||||
}
|
||||
this.router.navigate(['/submit'], navigationExtras);
|
||||
}
|
||||
}
|
||||
|
@@ -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>
|
@@ -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);
|
||||
}
|
167
src/app/shared/entity-dropdown/entity-dropdown.component.spec.ts
Normal file
167
src/app/shared/entity-dropdown/entity-dropdown.component.spec.ts
Normal 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();
|
||||
});
|
||||
});
|
207
src/app/shared/entity-dropdown/entity-dropdown.component.ts
Normal file
207
src/app/shared/entity-dropdown/entity-dropdown.component.ts
Normal 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());
|
||||
}
|
||||
}
|
@@ -12,7 +12,9 @@ import { RelationshipOptions } from '../../models/relationship-options.model';
|
||||
import { SearchResult } from '../../../../search/search-result.model';
|
||||
import { Item } from '../../../../../core/shared/item.model';
|
||||
import {
|
||||
AddRelationshipAction, RemoveRelationshipAction, UpdateRelationshipNameVariantAction,
|
||||
AddRelationshipAction,
|
||||
RemoveRelationshipAction,
|
||||
UpdateRelationshipNameVariantAction,
|
||||
} from './relationship.actions';
|
||||
import { RelationshipService } from '../../../../../core/data/relationship.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 { RemoteDataBuildService } from '../../../../../core/cache/builders/remote-data-build.service';
|
||||
import { getAllSucceededRemoteDataPayload } from '../../../../../core/shared/operators';
|
||||
import { followLink } from '../../../../utils/follow-link-config.model';
|
||||
|
||||
@Component({
|
||||
selector: 'ds-dynamic-lookup-relation-modal',
|
||||
@@ -146,7 +149,14 @@ export class DsDynamicLookupRelationModalComponent implements OnInit, OnDestroy
|
||||
|
||||
if (isNotEmpty(this.relationshipOptions.externalSources)) {
|
||||
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(
|
||||
getAllSucceededRemoteDataPayload()
|
||||
);
|
||||
|
@@ -26,6 +26,7 @@ import { ExternalSourceEntryImportModalComponent } from './external-source-entry
|
||||
import { createPaginatedList } from '../../../../../testing/utils.test';
|
||||
import { PaginationService } from '../../../../../../core/pagination/pagination.service';
|
||||
import { PaginationServiceStub } from '../../../../../testing/pagination-service.stub';
|
||||
import { ItemType } from '../../../../../../core/shared/item-relationships/item-type.model';
|
||||
|
||||
describe('DsDynamicLookupRelationExternalSourceTabComponent', () => {
|
||||
let component: DsDynamicLookupRelationExternalSourceTabComponent;
|
||||
@@ -35,10 +36,12 @@ describe('DsDynamicLookupRelationExternalSourceTabComponent', () => {
|
||||
let selectableListService;
|
||||
let modalService;
|
||||
|
||||
const itemType = Object.assign(new ItemType(), { label: 'Person' });
|
||||
const externalSource = {
|
||||
id: 'orcidV2',
|
||||
name: 'orcidV2',
|
||||
hierarchical: false
|
||||
hierarchical: false,
|
||||
entityTypes: createSuccessfulRemoteDataObject$(createPaginatedList([itemType]))
|
||||
} as ExternalSource;
|
||||
const externalEntries = [
|
||||
Object.assign({
|
||||
|
@@ -7,7 +7,7 @@ import { RemoteData } from '../../../../../../core/data/remote-data';
|
||||
import { PaginatedList } from '../../../../../../core/data/paginated-list.model';
|
||||
import { ExternalSourceEntry } from '../../../../../../core/shared/external-source-entry.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 { Context } from '../../../../../../core/shared/context.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 { PaginationService } from '../../../../../../core/pagination/pagination.service';
|
||||
import { Observable, Subscription } from 'rxjs';
|
||||
import { ItemType } from '../../../../../../core/shared/item-relationships/item-type.model';
|
||||
import { getFirstCompletedRemoteData } from '../../../../../../core/shared/operators';
|
||||
|
||||
@Component({
|
||||
selector: 'ds-dynamic-lookup-relation-external-source-tab',
|
||||
@@ -116,6 +118,11 @@ export class DsDynamicLookupRelationExternalSourceTabComponent implements OnInit
|
||||
*/
|
||||
importObjectSub: Subscription;
|
||||
|
||||
/**
|
||||
* The entity types compatible with the given external source
|
||||
*/
|
||||
relatedEntityType: ItemType;
|
||||
|
||||
constructor(private router: Router,
|
||||
public searchConfigService: SearchConfigurationService,
|
||||
private externalSourceService: ExternalSourceService,
|
||||
@@ -129,6 +136,15 @@ export class DsDynamicLookupRelationExternalSourceTabComponent implements OnInit
|
||||
* Get the entries for the selected external source
|
||||
*/
|
||||
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.entriesRD$ = this.searchConfigService.paginatedSearchOptions.pipe(
|
||||
switchMap((searchOptions: PaginatedSearchOptions) =>
|
||||
@@ -155,6 +171,7 @@ export class DsDynamicLookupRelationExternalSourceTabComponent implements OnInit
|
||||
modalComp.collection = this.collection;
|
||||
modalComp.relationship = this.relationship;
|
||||
modalComp.label = this.label;
|
||||
modalComp.relatedEntityType = this.relatedEntityType;
|
||||
this.importObjectSub = modalComp.importedObject.subscribe((object) => {
|
||||
this.selectableListService.selectSingle(this.listId, object);
|
||||
this.importedObject.emit(object);
|
||||
|
@@ -17,13 +17,6 @@
|
||||
<div id="external-source-entry-entities" class="mb-3">
|
||||
<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"
|
||||
[searchResults]="(localEntitiesRD$ | async)"
|
||||
[sortConfig]="this.lookupRelationService.searchConfig?.sort"
|
||||
|
@@ -86,7 +86,6 @@ describe('DsDynamicLookupRelationExternalSourceTabComponent', () => {
|
||||
component.externalSourceEntry = entry;
|
||||
component.label = label;
|
||||
component.relationship = relationship;
|
||||
component.collection = submissionCollection;
|
||||
component.item = submissionItem;
|
||||
fixture.detectChanges();
|
||||
});
|
||||
|
@@ -1,5 +1,5 @@
|
||||
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 { MetadataValue } from '../../../../../../../core/shared/metadata.models';
|
||||
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 { SelectableListService } from '../../../../../../object-list/selectable-list/selectable-list.service';
|
||||
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 { PaginationComponentOptions } from '../../../../../../pagination/pagination-component-options.model';
|
||||
import { getRemoteDataPayload, getFirstSucceededRemoteData } from '../../../../../../../core/shared/operators';
|
||||
import { take } from 'rxjs/operators';
|
||||
import { getFirstSucceededRemoteData, getRemoteDataPayload } from '../../../../../../../core/shared/operators';
|
||||
import { switchMap, take } from 'rxjs/operators';
|
||||
import { ItemSearchResult } from '../../../../../../object-collection/shared/item-search-result.model';
|
||||
import { NotificationsService } from '../../../../../../notifications/notifications.service';
|
||||
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
|
||||
@@ -67,16 +69,6 @@ export class ExternalSourceEntryImportModalComponent implements OnInit {
|
||||
*/
|
||||
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
|
||||
*/
|
||||
@@ -147,8 +139,19 @@ export class ExternalSourceEntryImportModalComponent implements OnInit {
|
||||
*/
|
||||
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,
|
||||
public lookupRelationService: LookupRelationService,
|
||||
private modalService: NgbModal,
|
||||
private selectService: SelectableListService,
|
||||
private itemService: ItemDataService,
|
||||
private notificationsService: NotificationsService,
|
||||
@@ -160,7 +163,6 @@ export class ExternalSourceEntryImportModalComponent implements OnInit {
|
||||
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.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
|
||||
*/
|
||||
importNewEntity() {
|
||||
this.itemService.importExternalSourceEntry(this.externalSourceEntry, this.collectionId).pipe(
|
||||
getFirstSucceededRemoteData(),
|
||||
getRemoteDataPayload(),
|
||||
take(1)
|
||||
this.modalRef = this.modalService.open(SubmissionImportExternalCollectionComponent, {
|
||||
size: 'lg',
|
||||
});
|
||||
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) => {
|
||||
this.lookupRelationService.removeLocalResultsCache();
|
||||
const searchResult = Object.assign(new ItemSearchResult(), {
|
||||
indexableObject: item
|
||||
});
|
||||
this.notificationsService.success(this.translateService.get(this.labelPrefix + this.label + '.added.new-entity'));
|
||||
this.modalRef.close();
|
||||
this.importedObject.emit(searchResult);
|
||||
});
|
||||
}
|
||||
|
@@ -11,6 +11,9 @@ export const externalSourceOrcid: ExternalSource = {
|
||||
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: {
|
||||
href: 'https://dspace7.4science.cloud/server/api/integration/externalsources/orcid'
|
||||
}
|
||||
@@ -26,6 +29,9 @@ export const externalSourceCiencia: ExternalSource = {
|
||||
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: {
|
||||
href: 'https://dspace7.4science.cloud/server/api/integration/externalsources/ciencia'
|
||||
}
|
||||
@@ -41,6 +47,9 @@ export const externalSourceMyStaffDb: ExternalSource = {
|
||||
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: {
|
||||
href: 'https://dspace7.4science.cloud/server/api/integration/externalsources/my_staff_db'
|
||||
}
|
||||
@@ -53,6 +62,7 @@ export const externalSourceMyStaffDb: ExternalSource = {
|
||||
export function getMockExternalSourceService(): ExternalSourceService {
|
||||
return jasmine.createSpyObj('ExternalSourceService', {
|
||||
findAll: jasmine.createSpy('findAll'),
|
||||
searchBy: jasmine.createSpy('searchBy'),
|
||||
getExternalSourceEntries: jasmine.createSpy('getExternalSourceEntries'),
|
||||
});
|
||||
}
|
||||
|
@@ -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 { DsDynamicScrollableDropdownComponent } from './form/builder/ds-dynamic-form-ui/models/scrollable-dropdown/dynamic-scrollable-dropdown.component';
|
||||
import {
|
||||
DsDynamicFormControlContainerComponent, dsDynamicFormControlMapFn,
|
||||
DsDynamicFormControlContainerComponent,
|
||||
dsDynamicFormControlMapFn,
|
||||
} 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 { 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 { FileDownloadLinkComponent } from './file-download-link/file-download-link.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 { VocabularyTreeviewComponent } from './vocabulary-treeview/vocabulary-treeview.component';
|
||||
import { CurationFormComponent } from '../curation-form/curation-form.component';
|
||||
@@ -438,6 +440,7 @@ const COMPONENTS = [
|
||||
BitstreamDownloadPageComponent,
|
||||
BitstreamRequestACopyPageComponent,
|
||||
CollectionDropdownComponent,
|
||||
EntityDropdownComponent,
|
||||
ExportMetadataSelectorComponent,
|
||||
ConfirmationModalComponent,
|
||||
VocabularyTreeviewComponent,
|
||||
|
@@ -1,11 +1,16 @@
|
||||
<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">
|
||||
<span aria-hidden="true">×</span>
|
||||
</button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<ds-collection-dropdown (selectionChange)="selectObject($event)">
|
||||
<ds-loading *ngIf="isLoading()"></ds-loading>
|
||||
<ds-collection-dropdown [ngClass]="{'d-none': isLoading()}"
|
||||
(selectionChange)="selectObject($event)"
|
||||
(searchComplete)="searchComplete()"
|
||||
(theOnlySelectable)="theOnlySelectable($event)"
|
||||
[entityType]="entityType">
|
||||
</ds-collection-dropdown>
|
||||
</div>
|
||||
</div>
|
||||
|
@@ -1,10 +1,11 @@
|
||||
import { Component, NO_ERRORS_SCHEMA, EventEmitter } from '@angular/core';
|
||||
import { waitForAsync, TestBed, ComponentFixture, inject } from '@angular/core/testing';
|
||||
import { Component, EventEmitter, NO_ERRORS_SCHEMA } from '@angular/core';
|
||||
import { ComponentFixture, fakeAsync, inject, TestBed, waitForAsync } from '@angular/core/testing';
|
||||
import { TranslateModule } from '@ngx-translate/core';
|
||||
import { createTestComponent } from '../../../shared/testing/utils.test';
|
||||
import { SubmissionImportExternalCollectionComponent } from './submission-import-external-collection.component';
|
||||
import { CollectionListEntry } from '../../../shared/collection-dropdown/collection-dropdown.component';
|
||||
import { NgbActiveModal } from '@ng-bootstrap/ng-bootstrap';
|
||||
import { By } from '@angular/platform-browser';
|
||||
|
||||
describe('SubmissionImportExternalCollectionComponent test suite', () => {
|
||||
let comp: SubmissionImportExternalCollectionComponent;
|
||||
@@ -76,6 +77,46 @@ describe('SubmissionImportExternalCollectionComponent test suite', () => {
|
||||
|
||||
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');
|
||||
});
|
||||
}));
|
||||
|
||||
});
|
||||
});
|
||||
|
||||
|
@@ -1,4 +1,4 @@
|
||||
import { Component, Output, EventEmitter } from '@angular/core';
|
||||
import { Component, EventEmitter, Output } from '@angular/core';
|
||||
import { CollectionListEntry } from '../../../shared/collection-dropdown/collection-dropdown.component';
|
||||
import { NgbActiveModal } from '@ng-bootstrap/ng-bootstrap';
|
||||
|
||||
@@ -16,6 +16,16 @@ export class SubmissionImportExternalCollectionComponent {
|
||||
*/
|
||||
@Output() public selectedEvent = new EventEmitter<CollectionListEntry>();
|
||||
|
||||
/**
|
||||
* If present this value is used to filter collection list by entity type
|
||||
*/
|
||||
public entityType: string;
|
||||
|
||||
/**
|
||||
* If collection searching is pending or not
|
||||
*/
|
||||
public loading = true;
|
||||
|
||||
/**
|
||||
* Initialize the component variables.
|
||||
* @param {NgbActiveModal} activeModal
|
||||
@@ -37,4 +47,28 @@ export class SubmissionImportExternalCollectionComponent {
|
||||
public closeCollectionModal(): void {
|
||||
this.activeModal.dismiss(false);
|
||||
}
|
||||
|
||||
/**
|
||||
* Propagate the onlySelectable collection
|
||||
* @param theOnlySelectable
|
||||
*/
|
||||
public theOnlySelectable(theOnlySelectable: CollectionListEntry) {
|
||||
this.selectedEvent.emit(theOnlySelectable);
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the hasChoice state
|
||||
* @param hasChoice
|
||||
*/
|
||||
public searchComplete() {
|
||||
this.loading = false;
|
||||
}
|
||||
|
||||
/**
|
||||
* If the component is in loading state.
|
||||
*/
|
||||
public isLoading(): boolean {
|
||||
return !!this.loading;
|
||||
}
|
||||
|
||||
}
|
||||
|
@@ -1,5 +1,5 @@
|
||||
<div class="modal-header">
|
||||
<h2>{{'submission.import-external.preview.title' | translate}}</h2>
|
||||
<h2>{{'submission.import-external.preview.title.' + labelPrefix | translate}}</h2>
|
||||
<button type="button" class="close"
|
||||
(click)="closeMetadataModal()" aria-label="Close">
|
||||
<span aria-hidden="true">×</span>
|
||||
|
@@ -1,6 +1,6 @@
|
||||
import { Component, Input, OnInit } from '@angular/core';
|
||||
import { Router } from '@angular/router';
|
||||
import { NgbActiveModal, NgbModalRef, NgbModal } from '@ng-bootstrap/ng-bootstrap';
|
||||
import { NgbActiveModal, NgbModal, NgbModalRef } from '@ng-bootstrap/ng-bootstrap';
|
||||
import { ExternalSourceEntry } from '../../../core/shared/external-source-entry.model';
|
||||
import { MetadataValue } from '../../../core/shared/metadata.models';
|
||||
import { Metadata } from '../../../core/shared/metadata.utils';
|
||||
@@ -28,6 +28,10 @@ export class SubmissionImportExternalPreviewComponent implements OnInit {
|
||||
* The entry metadata list
|
||||
*/
|
||||
public metadataList: { key: string, value: MetadataValue }[];
|
||||
/**
|
||||
* The label prefix to use to generate the translation label
|
||||
*/
|
||||
public labelPrefix: string;
|
||||
/**
|
||||
* The modal for the entry preview
|
||||
*/
|
||||
@@ -77,6 +81,7 @@ export class SubmissionImportExternalPreviewComponent implements OnInit {
|
||||
this.modalRef = this.modalService.open(SubmissionImportExternalCollectionComponent, {
|
||||
size: 'lg',
|
||||
});
|
||||
this.modalRef.componentInstance.entityType = this.labelPrefix;
|
||||
this.closeMetadataModal();
|
||||
|
||||
this.modalRef.componentInstance.selectedEvent.pipe(
|
||||
|
@@ -15,7 +15,7 @@ import {
|
||||
getMockExternalSourceService
|
||||
} from '../../../shared/mocks/external-source.service.mock';
|
||||
import { PageInfo } from '../../../core/shared/page-info.model';
|
||||
import { PaginatedList, buildPaginatedList } from '../../../core/data/paginated-list.model';
|
||||
import { buildPaginatedList, PaginatedList } from '../../../core/data/paginated-list.model';
|
||||
import { createSuccessfulRemoteDataObject } from '../../../shared/remote-data.utils';
|
||||
import { ExternalSource } from '../../../core/shared/external-source.model';
|
||||
import { FindListOptions } from '../../../core/data/request.models';
|
||||
@@ -23,6 +23,7 @@ import { HostWindowService } from '../../../shared/host-window.service';
|
||||
import { HostWindowServiceStub } from '../../../shared/testing/host-window-service.stub';
|
||||
import { getTestScheduler } from 'jasmine-marbles';
|
||||
import { TestScheduler } from 'rxjs/testing';
|
||||
import { RequestParam } from '../../../core/cache/models/request-param.model';
|
||||
|
||||
describe('SubmissionImportExternalSearchbarComponent test suite', () => {
|
||||
let comp: SubmissionImportExternalSearchbarComponent;
|
||||
@@ -63,9 +64,9 @@ describe('SubmissionImportExternalSearchbarComponent test suite', () => {
|
||||
|
||||
// synchronous beforeEach
|
||||
beforeEach(() => {
|
||||
mockExternalSourceService.findAll.and.returnValue(observableOf(paginatedListRD));
|
||||
mockExternalSourceService.searchBy.and.returnValue(observableOf(paginatedListRD));
|
||||
const html = `
|
||||
<ds-submission-import-external-searchbar></ds-submission-import-external-searchbar>`;
|
||||
<ds-submission-import-external-searchbar [initExternalSourceData]="initExternalSourceData"></ds-submission-import-external-searchbar>`;
|
||||
testFixture = createTestComponent(html, TestComponent) as ComponentFixture<TestComponent>;
|
||||
testComp = testFixture.componentInstance;
|
||||
});
|
||||
@@ -88,7 +89,7 @@ describe('SubmissionImportExternalSearchbarComponent test suite', () => {
|
||||
const pageInfo = new PageInfo();
|
||||
paginatedList = buildPaginatedList(pageInfo, [externalSourceOrcid, externalSourceCiencia, externalSourceMyStaffDb]);
|
||||
paginatedListRD = createSuccessfulRemoteDataObject(paginatedList);
|
||||
compAsAny.externalService.findAll.and.returnValue(observableOf(paginatedListRD));
|
||||
compAsAny.externalService.searchBy.and.returnValue(observableOf(paginatedListRD));
|
||||
sourceList = [
|
||||
{id: 'orcid', name: 'orcid'},
|
||||
{id: 'ciencia', name: 'ciencia'},
|
||||
@@ -103,7 +104,7 @@ describe('SubmissionImportExternalSearchbarComponent test suite', () => {
|
||||
});
|
||||
|
||||
it('Should init component properly (without initExternalSourceData)', () => {
|
||||
comp.initExternalSourceData = { sourceId: '', query: '' };
|
||||
comp.initExternalSourceData = { entity: 'Publication', sourceId: '', query: '' };
|
||||
scheduler.schedule(() => fixture.detectChanges());
|
||||
scheduler.flush();
|
||||
|
||||
@@ -113,7 +114,7 @@ describe('SubmissionImportExternalSearchbarComponent test suite', () => {
|
||||
});
|
||||
|
||||
it('Should init component properly (with initExternalSourceData populated)', () => {
|
||||
comp.initExternalSourceData = { query: 'dummy', sourceId: 'ciencia' };
|
||||
comp.initExternalSourceData = { entity: 'Publication', query: 'dummy', sourceId: 'ciencia' };
|
||||
scheduler.schedule(() => fixture.detectChanges());
|
||||
scheduler.flush();
|
||||
|
||||
@@ -129,6 +130,7 @@ describe('SubmissionImportExternalSearchbarComponent test suite', () => {
|
||||
});
|
||||
|
||||
it('Should load additional external sources', () => {
|
||||
comp.initExternalSourceData = { entity: 'Publication', query: 'dummy', sourceId: 'ciencia' };
|
||||
comp.sourceListLoading = false;
|
||||
compAsAny.pageInfo = new PageInfo({
|
||||
elementsPerPage: 3,
|
||||
@@ -139,6 +141,9 @@ describe('SubmissionImportExternalSearchbarComponent test suite', () => {
|
||||
compAsAny.findListOptions = Object.assign({}, new FindListOptions(), {
|
||||
elementsPerPage: 3,
|
||||
currentPage: 0,
|
||||
searchParams: [
|
||||
new RequestParam('entityType', 'Publication')
|
||||
]
|
||||
});
|
||||
comp.sourceList = sourceList;
|
||||
const expected = sourceList.concat(sourceList);
|
||||
@@ -150,9 +155,10 @@ describe('SubmissionImportExternalSearchbarComponent test suite', () => {
|
||||
});
|
||||
|
||||
it('The \'search\' method should call \'emit\'', () => {
|
||||
comp.initExternalSourceData = { entity: 'Publication', query: 'dummy', sourceId: 'ciencia' };
|
||||
comp.selectedElement = { id: 'orcidV2', name: 'orcidV2' };
|
||||
comp.searchString = 'dummy';
|
||||
const expected = { sourceId: comp.selectedElement.id, query: comp.searchString };
|
||||
const expected = { entity: 'Publication', sourceId: comp.selectedElement.id, query: comp.searchString };
|
||||
spyOn(comp.externalSourceData, 'emit');
|
||||
comp.search();
|
||||
|
||||
@@ -167,5 +173,5 @@ describe('SubmissionImportExternalSearchbarComponent test suite', () => {
|
||||
template: ``
|
||||
})
|
||||
class TestComponent {
|
||||
|
||||
initExternalSourceData = { entity: 'Publication', query: 'dummy', sourceId: 'ciencia' };
|
||||
}
|
||||
|
@@ -1,19 +1,12 @@
|
||||
import {
|
||||
ChangeDetectorRef,
|
||||
Component,
|
||||
EventEmitter,
|
||||
Input,
|
||||
OnDestroy,
|
||||
OnInit,
|
||||
Output
|
||||
} from '@angular/core';
|
||||
import { ChangeDetectorRef, Component, EventEmitter, Input, OnDestroy, OnInit, Output } from '@angular/core';
|
||||
|
||||
import { Observable, of as observableOf, Subscription } from 'rxjs';
|
||||
import { catchError, tap } from 'rxjs/operators';
|
||||
|
||||
import { RequestParam } from '../../../core/cache/models/request-param.model';
|
||||
import { ExternalSourceService } from '../../../core/data/external-source.service';
|
||||
import { ExternalSource } from '../../../core/shared/external-source.model';
|
||||
import { PaginatedList, buildPaginatedList } from '../../../core/data/paginated-list.model';
|
||||
import { buildPaginatedList, PaginatedList } from '../../../core/data/paginated-list.model';
|
||||
import { RemoteData } from '../../../core/data/remote-data';
|
||||
import { PageInfo } from '../../../core/shared/page-info.model';
|
||||
import { createSuccessfulRemoteDataObject } from '../../../shared/remote-data.utils';
|
||||
@@ -34,6 +27,7 @@ export interface SourceElement {
|
||||
* Interface for the external source data to export.
|
||||
*/
|
||||
export interface ExternalSourceData {
|
||||
entity: string;
|
||||
query: string;
|
||||
sourceId: string;
|
||||
}
|
||||
@@ -116,8 +110,11 @@ export class SubmissionImportExternalSearchbarComponent implements OnInit, OnDes
|
||||
this.findListOptions = Object.assign({}, new FindListOptions(), {
|
||||
elementsPerPage: 5,
|
||||
currentPage: 1,
|
||||
searchParams: [
|
||||
new RequestParam('entityType', this.initExternalSourceData.entity)
|
||||
]
|
||||
});
|
||||
this.externalService.findAll(this.findListOptions).pipe(
|
||||
this.externalService.searchBy('findByEntityType', this.findListOptions).pipe(
|
||||
catchError(() => {
|
||||
const pageInfo = new PageInfo();
|
||||
const paginatedList = buildPaginatedList(pageInfo, []);
|
||||
@@ -158,8 +155,11 @@ export class SubmissionImportExternalSearchbarComponent implements OnInit, OnDes
|
||||
this.findListOptions = Object.assign({}, new FindListOptions(), {
|
||||
elementsPerPage: 5,
|
||||
currentPage: this.findListOptions.currentPage + 1,
|
||||
searchParams: [
|
||||
new RequestParam('entityType', this.initExternalSourceData.entity)
|
||||
]
|
||||
});
|
||||
this.sub = this.externalService.findAll(this.findListOptions).pipe(
|
||||
this.externalService.searchBy('findByEntityType', this.findListOptions).pipe(
|
||||
catchError(() => {
|
||||
const pageInfo = new PageInfo();
|
||||
const paginatedList = buildPaginatedList(pageInfo, []);
|
||||
@@ -182,7 +182,13 @@ export class SubmissionImportExternalSearchbarComponent implements OnInit, OnDes
|
||||
* Passes the search parameters to the parent component.
|
||||
*/
|
||||
public search(): void {
|
||||
this.externalSourceData.emit({ sourceId: this.selectedElement.id, query: this.searchString });
|
||||
this.externalSourceData.emit(
|
||||
{
|
||||
entity: this.initExternalSourceData.entity,
|
||||
sourceId: this.selectedElement.id,
|
||||
query: this.searchString
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -193,4 +199,5 @@ export class SubmissionImportExternalSearchbarComponent implements OnInit, OnDes
|
||||
this.sub.unsubscribe();
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
@@ -1,17 +1,17 @@
|
||||
<div class="container">
|
||||
<div class="row">
|
||||
<div class="col-md-12">
|
||||
<h2 id="header" class="pb-2">{{'submission.import-external.title' | translate}}</h2>
|
||||
<h2 id="header" class="pb-2">{{'submission.import-external.title' + ((label) ? '.' + label : '') | translate}}</h2>
|
||||
<ds-submission-import-external-searchbar
|
||||
[initExternalSourceData]="routeData"
|
||||
[initExternalSourceData]="reload$.value"
|
||||
(externalSourceData) = "getExternalSourceData($event)">
|
||||
</ds-submission-import-external-searchbar>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div *ngIf="routeData.sourceId !== ''" class="col-md-12">
|
||||
<div class="row" *ngIf="reload$.value.entity">
|
||||
<div *ngIf="reload$.value.sourceId !== ''" class="col-md-12">
|
||||
<ng-container *ngVar="(entriesRD$ | async) as entriesRD">
|
||||
<h3 *ngIf="entriesRD && entriesRD?.payload?.page?.length !== 0">{{ 'submission.sections.describe.relationship-lookup.selection-tab.title.' + routeData.sourceId | translate}}</h3>
|
||||
<h3 *ngIf="entriesRD && entriesRD?.payload?.page?.length !== 0">{{ 'submission.sections.describe.relationship-lookup.selection-tab.title' | translate}}</h3>
|
||||
<ds-viewable-collection *ngIf="entriesRD?.hasSucceeded && !(isLoading$ | async) && entriesRD?.payload?.page?.length > 0" @fadeIn
|
||||
[objects]="entriesRD"
|
||||
[selectionConfig]="{ repeatable: repeatable, listId: listId }"
|
||||
@@ -25,16 +25,17 @@
|
||||
<ds-loading *ngIf="(isLoading$ | async)"
|
||||
message="{{'loading.search-results' | translate}}"></ds-loading>
|
||||
<div *ngIf="!(isLoading$ | async) && entriesRD?.payload?.page?.length === 0" id="empty-external-entry-list">
|
||||
{{ 'search.results.empty' | translate }}
|
||||
<ds-alert [type]="'alert-info'">{{ 'search.results.empty' | translate }}</ds-alert>
|
||||
</div>
|
||||
</ng-container>
|
||||
</div>
|
||||
<div *ngIf="routeData.sourceId === ''" class="col-md-12">
|
||||
<div *ngIf="reload$.value.sourceId === ''" class="col-md-12">
|
||||
<ds-alert [type]="'alert-info'">
|
||||
<p class="lead mb-0">{{'submission.import-external.page.hint' | translate}}</p>
|
||||
</ds-alert>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row">
|
||||
<div class="col-md-12">
|
||||
<hr>
|
||||
|
@@ -1,5 +1,5 @@
|
||||
import { Component, NO_ERRORS_SCHEMA } from '@angular/core';
|
||||
import { waitForAsync, ComponentFixture, inject, TestBed } from '@angular/core/testing';
|
||||
import { ComponentFixture, inject, TestBed, waitForAsync } from '@angular/core/testing';
|
||||
|
||||
import { getTestScheduler } from 'jasmine-marbles';
|
||||
import { TranslateModule } from '@ngx-translate/core';
|
||||
@@ -102,17 +102,19 @@ describe('SubmissionImportExternalComponent test suite', () => {
|
||||
|
||||
it('Should init component properly (without route data)', () => {
|
||||
const expectedEntries = createSuccessfulRemoteDataObject(createPaginatedList([]));
|
||||
comp.routeData = {entity: '', sourceId: '', query: '' };
|
||||
spyOn(compAsAny.routeService, 'getQueryParameterValue').and.returnValue(observableOf(''));
|
||||
fixture.detectChanges();
|
||||
|
||||
expect(comp.routeData).toEqual({ sourceId: '', query: '' });
|
||||
expect(comp.routeData).toEqual({entity: '', sourceId: '', query: '' });
|
||||
expect(comp.isLoading$.value).toBe(false);
|
||||
expect(comp.entriesRD$.value).toEqual(expectedEntries);
|
||||
});
|
||||
|
||||
it('Should init component properly (with route data)', () => {
|
||||
comp.routeData = {entity: '', sourceId: '', query: '' };
|
||||
spyOn(compAsAny, 'retrieveExternalSources');
|
||||
spyOn(compAsAny.routeService, 'getQueryParameterValue').and.returnValues(observableOf('source'), observableOf('dummy'));
|
||||
spyOn(compAsAny.routeService, 'getQueryParameterValue').and.returnValues(observableOf('entity'), observableOf('source'), observableOf('dummy'));
|
||||
fixture.detectChanges();
|
||||
|
||||
expect(compAsAny.retrieveExternalSources).toHaveBeenCalled();
|
||||
@@ -120,7 +122,7 @@ describe('SubmissionImportExternalComponent test suite', () => {
|
||||
|
||||
it('Should call \'getExternalSourceEntries\' properly', () => {
|
||||
spyOn(routeServiceStub, 'getQueryParameterValue').and.callFake((param) => {
|
||||
if (param === 'source') {
|
||||
if (param === 'sourceId') {
|
||||
return observableOf('orcidV2');
|
||||
} else if (param === 'query') {
|
||||
return observableOf('test');
|
||||
@@ -136,15 +138,15 @@ describe('SubmissionImportExternalComponent test suite', () => {
|
||||
});
|
||||
|
||||
it('Should call \'router.navigate\'', () => {
|
||||
comp.routeData = { sourceId: '', query: '' };
|
||||
comp.routeData = {entity: 'Person', sourceId: '', query: '' };
|
||||
spyOn(compAsAny, 'retrieveExternalSources').and.callFake(() => null);
|
||||
compAsAny.router.navigate.and.returnValue( new Promise(() => {return;}));
|
||||
const event = { sourceId: 'orcidV2', query: 'dummy' };
|
||||
const event = {entity: 'Person', sourceId: 'orcidV2', query: 'dummy' };
|
||||
|
||||
scheduler.schedule(() => comp.getExternalSourceData(event));
|
||||
scheduler.flush();
|
||||
|
||||
expect(compAsAny.router.navigate).toHaveBeenCalledWith([], { queryParams: { source: event.sourceId, query: event.query }, replaceUrl: true });
|
||||
expect(compAsAny.router.navigate).toHaveBeenCalledWith([], { queryParams: { entity: event.entity, sourceId: event.sourceId, query: event.query }, replaceUrl: true });
|
||||
});
|
||||
|
||||
it('Entry should be passed to the component loaded inside the modal', () => {
|
||||
@@ -166,6 +168,13 @@ describe('SubmissionImportExternalComponent test suite', () => {
|
||||
expect(compAsAny.modalService.open).toHaveBeenCalledWith(SubmissionImportExternalPreviewComponent, { size: 'lg' });
|
||||
expect(comp.modalRef.componentInstance.externalSourceEntry).toEqual(entry);
|
||||
});
|
||||
|
||||
it('Should set the correct label', () => {
|
||||
const label = 'Person';
|
||||
compAsAny.selectLabel(label);
|
||||
|
||||
expect(comp.label).toEqual(label);
|
||||
});
|
||||
});
|
||||
|
||||
});
|
||||
|
@@ -20,6 +20,7 @@ import { fadeIn } from '../../shared/animations/fade';
|
||||
import { PageInfo } from '../../core/shared/page-info.model';
|
||||
import { hasValue, isNotEmpty } from '../../shared/empty.util';
|
||||
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.
|
||||
@@ -45,9 +46,10 @@ export class SubmissionImportExternalComponent implements OnInit, OnDestroy {
|
||||
*/
|
||||
public isLoading$: BehaviorSubject<boolean> = new BehaviorSubject<boolean>(false);
|
||||
|
||||
public reload$: BehaviorSubject<{ query: string, source: string }> = new BehaviorSubject<{ query: string; source: string }>({
|
||||
public reload$: BehaviorSubject<ExternalSourceData> = new BehaviorSubject<ExternalSourceData>({
|
||||
entity: '',
|
||||
query: '',
|
||||
source: ''
|
||||
sourceId: ''
|
||||
});
|
||||
/**
|
||||
* Configuration to use for the import buttons
|
||||
@@ -109,11 +111,10 @@ export class SubmissionImportExternalComponent implements OnInit, OnDestroy {
|
||||
* Get the entries for the selected external source and set initial configuration.
|
||||
*/
|
||||
ngOnInit(): void {
|
||||
this.label = 'Journal';
|
||||
this.listId = 'list-submission-external-sources';
|
||||
this.context = Context.EntitySearchModalWithNameVariants;
|
||||
this.repeatable = false;
|
||||
this.routeData = {sourceId: '', query: ''};
|
||||
this.routeData = {entity: '', sourceId: '', query: ''};
|
||||
this.importConfig = {
|
||||
buttonLabel: 'submission.sections.describe.relationship-lookup.external-source.import-button-title.' + this.label
|
||||
};
|
||||
@@ -121,12 +122,14 @@ export class SubmissionImportExternalComponent implements OnInit, OnDestroy {
|
||||
this.isLoading$ = new BehaviorSubject(false);
|
||||
this.subs.push(combineLatest(
|
||||
[
|
||||
this.routeService.getQueryParameterValue('source'),
|
||||
this.routeService.getQueryParameterValue('entity'),
|
||||
this.routeService.getQueryParameterValue('sourceId'),
|
||||
this.routeService.getQueryParameterValue('query')
|
||||
]).pipe(
|
||||
take(1)
|
||||
).subscribe(([source, query]: [string, string]) => {
|
||||
this.reload$.next({query: query, source: source});
|
||||
).subscribe(([entity, sourceId, query]: [string, string, string]) => {
|
||||
this.reload$.next({entity: entity || NONE_ENTITY_TYPE, query: query, sourceId: sourceId});
|
||||
this.selectLabel(entity);
|
||||
this.retrieveExternalSources();
|
||||
}));
|
||||
}
|
||||
@@ -138,11 +141,11 @@ export class SubmissionImportExternalComponent implements OnInit, OnDestroy {
|
||||
this.router.navigate(
|
||||
[],
|
||||
{
|
||||
queryParams: {source: event.sourceId, query: event.query},
|
||||
queryParams: event,
|
||||
replaceUrl: true
|
||||
}
|
||||
).then(() => {
|
||||
this.reload$.next({source: event.sourceId, query: event.query});
|
||||
this.reload$.next(event);
|
||||
this.retrieveExternalSources();
|
||||
});
|
||||
}
|
||||
@@ -157,6 +160,7 @@ export class SubmissionImportExternalComponent implements OnInit, OnDestroy {
|
||||
});
|
||||
const modalComp = this.modalRef.componentInstance;
|
||||
modalComp.externalSourceEntry = entry;
|
||||
modalComp.labelPrefix = this.label;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -173,28 +177,23 @@ export class SubmissionImportExternalComponent implements OnInit, OnDestroy {
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieve external source entries
|
||||
*
|
||||
* @param source The source tupe
|
||||
* @param query The query string to search
|
||||
* Retrieve external source entries.
|
||||
*/
|
||||
private retrieveExternalSources(): void {
|
||||
if (hasValue(this.retrieveExternalSourcesSub)) {
|
||||
this.retrieveExternalSourcesSub.unsubscribe();
|
||||
}
|
||||
this.retrieveExternalSourcesSub = this.reload$.pipe(
|
||||
filter((sourceQueryObject: { source: string, query: string }) => isNotEmpty(sourceQueryObject.source) && isNotEmpty(sourceQueryObject.query)),
|
||||
switchMap((sourceQueryObject: { source: string, query: string }) => {
|
||||
const source = sourceQueryObject.source;
|
||||
filter((sourceQueryObject: ExternalSourceData) => isNotEmpty(sourceQueryObject.sourceId) && isNotEmpty(sourceQueryObject.query)),
|
||||
switchMap((sourceQueryObject: ExternalSourceData) => {
|
||||
const query = sourceQueryObject.query;
|
||||
this.routeData.sourceId = source;
|
||||
this.routeData.query = query;
|
||||
this.routeData = sourceQueryObject;
|
||||
return this.searchConfigService.paginatedSearchOptions.pipe(
|
||||
tap((v) => this.isLoading$.next(true)),
|
||||
tap(() => this.isLoading$.next(true)),
|
||||
filter((searchOptions) => searchOptions.query === query),
|
||||
mergeMap((searchOptions) => this.externalService.getExternalSourceEntries(this.routeData.sourceId, searchOptions).pipe(
|
||||
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
|
||||
};
|
||||
}
|
||||
|
||||
}
|
||||
|
@@ -905,6 +905,8 @@
|
||||
|
||||
"collection.form.title": "Name",
|
||||
|
||||
"collection.form.entityType": "Entity Type",
|
||||
|
||||
|
||||
|
||||
"collection.listelement.badge": "Collection",
|
||||
@@ -1278,6 +1280,8 @@
|
||||
|
||||
"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.button": "Search all of DSpace",
|
||||
@@ -2612,6 +2616,7 @@
|
||||
|
||||
"nav.user.description" : "User profile bar",
|
||||
|
||||
"none.listelement.badge": "Item",
|
||||
|
||||
|
||||
"orgunit.listelement.badge": "Organizational Unit",
|
||||
@@ -3432,6 +3437,22 @@
|
||||
|
||||
"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.back-to-my-dspace": "Back to MyDSpace",
|
||||
|
Reference in New Issue
Block a user