mirror of
https://github.com/DSpace/dspace-angular.git
synced 2025-10-07 10:04:11 +00:00
Fixed conflicts after merge
This commit is contained in:
@@ -0,0 +1,15 @@
|
|||||||
|
<div class="parent mb-3">
|
||||||
|
<div class="upload">
|
||||||
|
<ds-uploader *ngIf="uploadFilesOptions.url !== ''"
|
||||||
|
[uploadFilesOptions]="uploadFilesOptions"
|
||||||
|
(onCompleteItem)="onCompleteItem($event)"
|
||||||
|
(onUploadError)="onUploadError()"></ds-uploader>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
<div class="add">
|
||||||
|
<button class="btn btn-lg btn-primary mt-1 ml-2" (click)="openDialog()" role="button">
|
||||||
|
<i class="fa fa-plus-circle" aria-hidden="true"></i> {{'mydspace.new-submission' | translate}}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div>
|
@@ -1,5 +1,5 @@
|
|||||||
import { ChangeDetectorRef, Component, NO_ERRORS_SCHEMA } from '@angular/core';
|
import { ChangeDetectorRef, Component, NO_ERRORS_SCHEMA } from '@angular/core';
|
||||||
import { async, ComponentFixture, inject, TestBed } from '@angular/core/testing';
|
import { async, ComponentFixture, inject, TestBed, tick, fakeAsync } from '@angular/core/testing';
|
||||||
import { RouterTestingModule } from '@angular/router/testing';
|
import { RouterTestingModule } from '@angular/router/testing';
|
||||||
|
|
||||||
import { Store } from '@ngrx/store';
|
import { Store } from '@ngrx/store';
|
||||||
@@ -21,6 +21,8 @@ import { NotificationsServiceStub } from '../../shared/testing/notifications-ser
|
|||||||
import { SharedModule } from '../../shared/shared.module';
|
import { SharedModule } from '../../shared/shared.module';
|
||||||
import { getMockScrollToService } from '../../shared/mocks/scroll-to-service.mock';
|
import { getMockScrollToService } from '../../shared/mocks/scroll-to-service.mock';
|
||||||
import { UploaderService } from '../../shared/uploader/uploader.service';
|
import { UploaderService } from '../../shared/uploader/uploader.service';
|
||||||
|
import { By } from '@angular/platform-browser';
|
||||||
|
import { NgbModal } from '@ng-bootstrap/ng-bootstrap';
|
||||||
|
|
||||||
describe('MyDSpaceNewSubmissionComponent test', () => {
|
describe('MyDSpaceNewSubmissionComponent test', () => {
|
||||||
|
|
||||||
@@ -54,6 +56,11 @@ describe('MyDSpaceNewSubmissionComponent test', () => {
|
|||||||
{ provide: ScrollToService, useValue: getMockScrollToService() },
|
{ provide: ScrollToService, useValue: getMockScrollToService() },
|
||||||
{ provide: Store, useValue: store },
|
{ provide: Store, useValue: store },
|
||||||
{ provide: TranslateService, useValue: translateService },
|
{ provide: TranslateService, useValue: translateService },
|
||||||
|
{
|
||||||
|
provide: NgbModal, useValue: {
|
||||||
|
open: () => {/*comment*/}
|
||||||
|
}
|
||||||
|
},
|
||||||
ChangeDetectorRef,
|
ChangeDetectorRef,
|
||||||
MyDSpaceNewSubmissionComponent,
|
MyDSpaceNewSubmissionComponent,
|
||||||
UploaderService
|
UploaderService
|
||||||
@@ -86,6 +93,25 @@ describe('MyDSpaceNewSubmissionComponent test', () => {
|
|||||||
}));
|
}));
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe('', () => {
|
||||||
|
let fixture: ComponentFixture<MyDSpaceNewSubmissionComponent>;
|
||||||
|
let comp: MyDSpaceNewSubmissionComponent;
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
fixture = TestBed.createComponent(MyDSpaceNewSubmissionComponent);
|
||||||
|
comp = fixture.componentInstance;
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should call app.openDialog', () => {
|
||||||
|
spyOn(comp, 'openDialog');
|
||||||
|
const submissionButton = fixture.debugElement.query(By.css('button.btn-primary'));
|
||||||
|
submissionButton.triggerEventHandler('click', {
|
||||||
|
preventDefault: () => {/**/
|
||||||
|
}
|
||||||
|
});
|
||||||
|
expect(comp.openDialog).toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
// declare a test component
|
// declare a test component
|
||||||
|
@@ -15,6 +15,9 @@ import { HALEndpointService } from '../../core/shared/hal-endpoint.service';
|
|||||||
import { NotificationType } from '../../shared/notifications/models/notification-type';
|
import { NotificationType } from '../../shared/notifications/models/notification-type';
|
||||||
import { hasValue } from '../../shared/empty.util';
|
import { hasValue } from '../../shared/empty.util';
|
||||||
import { SearchResult } from '../../shared/search/search-result.model';
|
import { SearchResult } from '../../shared/search/search-result.model';
|
||||||
|
import { Router } from '@angular/router';
|
||||||
|
import { NgbModal } from '@ng-bootstrap/ng-bootstrap';
|
||||||
|
import { CreateItemParentSelectorComponent } from 'src/app/shared/dso-selector/modal-wrappers/create-item-parent-selector/create-item-parent-selector.component';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* This component represents the whole mydspace page header
|
* This component represents the whole mydspace page header
|
||||||
@@ -55,7 +58,9 @@ export class MyDSpaceNewSubmissionComponent implements OnDestroy, OnInit {
|
|||||||
private halService: HALEndpointService,
|
private halService: HALEndpointService,
|
||||||
private notificationsService: NotificationsService,
|
private notificationsService: NotificationsService,
|
||||||
private store: Store<SubmissionState>,
|
private store: Store<SubmissionState>,
|
||||||
private translate: TranslateService) {
|
private translate: TranslateService,
|
||||||
|
private router: Router,
|
||||||
|
private modalService: NgbModal) {
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -105,6 +110,14 @@ export class MyDSpaceNewSubmissionComponent implements OnDestroy, OnInit {
|
|||||||
this.notificationsService.error(null, this.translate.get('mydspace.upload.upload-failed'));
|
this.notificationsService.error(null, this.translate.get('mydspace.upload.upload-failed'));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Method called on clicking the button "New Submition", It opens a dialog for
|
||||||
|
* select a collection.
|
||||||
|
*/
|
||||||
|
openDialog() {
|
||||||
|
this.modalService.open(CreateItemParentSelectorComponent);
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Unsubscribe from the subscription
|
* Unsubscribe from the subscription
|
||||||
*/
|
*/
|
||||||
|
@@ -13,13 +13,19 @@ import { RequestEntry } from './request.reducer';
|
|||||||
import { ErrorResponse, RestResponse } from '../cache/response.models';
|
import { ErrorResponse, RestResponse } from '../cache/response.models';
|
||||||
import { ObjectCacheService } from '../cache/object-cache.service';
|
import { ObjectCacheService } from '../cache/object-cache.service';
|
||||||
import { RemoteDataBuildService } from '../cache/builders/remote-data-build.service';
|
import { RemoteDataBuildService } from '../cache/builders/remote-data-build.service';
|
||||||
|
import { Collection } from '../shared/collection.model';
|
||||||
|
import { PageInfo } from '../shared/page-info.model';
|
||||||
|
import { PaginatedList } from './paginated-list';
|
||||||
|
import { createSuccessfulRemoteDataObject } from 'src/app/shared/remote-data.utils';
|
||||||
|
import { hot, getTestScheduler, cold } from 'jasmine-marbles';
|
||||||
|
import { TestScheduler } from 'rxjs/testing';
|
||||||
|
|
||||||
const url = 'fake-url';
|
const url = 'fake-url';
|
||||||
const collectionId = 'fake-collection-id';
|
const collectionId = 'fake-collection-id';
|
||||||
|
|
||||||
describe('CollectionDataService', () => {
|
describe('CollectionDataService', () => {
|
||||||
let service: CollectionDataService;
|
let service: CollectionDataService;
|
||||||
|
let scheduler: TestScheduler;
|
||||||
let requestService: RequestService;
|
let requestService: RequestService;
|
||||||
let translate: TranslateService;
|
let translate: TranslateService;
|
||||||
let notificationsService: any;
|
let notificationsService: any;
|
||||||
@@ -27,6 +33,44 @@ describe('CollectionDataService', () => {
|
|||||||
let objectCache: ObjectCacheService;
|
let objectCache: ObjectCacheService;
|
||||||
let halService: any;
|
let halService: any;
|
||||||
|
|
||||||
|
const mockCollection1: Collection = Object.assign(new Collection(), {
|
||||||
|
id: 'test-collection-1-1',
|
||||||
|
name: 'test-collection-1',
|
||||||
|
_links: {
|
||||||
|
self: {
|
||||||
|
href: 'https://rest.api/collections/test-collection-1-1'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const mockCollection2: Collection = Object.assign(new Collection(), {
|
||||||
|
id: 'test-collection-2-2',
|
||||||
|
name: 'test-collection-2',
|
||||||
|
_links: {
|
||||||
|
self: {
|
||||||
|
href: 'https://rest.api/collections/test-collection-2-2'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const mockCollection3: Collection = Object.assign(new Collection(), {
|
||||||
|
id: 'test-collection-3-3',
|
||||||
|
name: 'test-collection-3',
|
||||||
|
_links: {
|
||||||
|
self: {
|
||||||
|
href: 'https://rest.api/collections/test-collection-3-3'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const queryString = 'test-string';
|
||||||
|
const communityId = '8b3c613a-5a4b-438b-9686-be1d5b4a1c5a';
|
||||||
|
|
||||||
|
const pageInfo = new PageInfo();
|
||||||
|
const array = [mockCollection1, mockCollection2, mockCollection3];
|
||||||
|
const paginatedList = new PaginatedList(pageInfo, array);
|
||||||
|
const paginatedListRD = createSuccessfulRemoteDataObject(paginatedList);
|
||||||
|
|
||||||
describe('when the requests are successful', () => {
|
describe('when the requests are successful', () => {
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
createService();
|
createService();
|
||||||
@@ -74,6 +118,43 @@ describe('CollectionDataService', () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe('when calling getAuthorizedCollection', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
scheduler = getTestScheduler();
|
||||||
|
spyOn(service, 'getAuthorizedCollection').and.callThrough();
|
||||||
|
spyOn(service, 'getAuthorizedCollectionByCommunity').and.callThrough();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should proxy the call to getAuthorizedCollection', () => {
|
||||||
|
scheduler.schedule(() => service.getAuthorizedCollection(queryString));
|
||||||
|
scheduler.flush();
|
||||||
|
|
||||||
|
expect(service.getAuthorizedCollection).toHaveBeenCalledWith(queryString);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return a RemoteData<PaginatedList<Colletion>> for the getAuthorizedCollection', () => {
|
||||||
|
const result = service.getAuthorizedCollection(queryString)
|
||||||
|
const expected = cold('a|', {
|
||||||
|
a: paginatedListRD
|
||||||
|
});
|
||||||
|
expect(result).toBeObservable(expected);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should proxy the call to getAuthorizedCollectionByCommunity', () => {
|
||||||
|
scheduler.schedule(() => service.getAuthorizedCollectionByCommunity(communityId, queryString));
|
||||||
|
scheduler.flush();
|
||||||
|
|
||||||
|
expect(service.getAuthorizedCollectionByCommunity).toHaveBeenCalledWith(communityId, queryString);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return a RemoteData<PaginatedList<Colletion>> for the getAuthorizedCollectionByCommunity', () => {
|
||||||
|
const result = service.getAuthorizedCollectionByCommunity(communityId, queryString)
|
||||||
|
const expected = cold('a|', {
|
||||||
|
a: paginatedListRD
|
||||||
|
});
|
||||||
|
expect(result).toBeObservable(expected);
|
||||||
|
});
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('when the requests are unsuccessful', () => {
|
describe('when the requests are unsuccessful', () => {
|
||||||
@@ -117,7 +198,9 @@ describe('CollectionDataService', () => {
|
|||||||
function createService(requestEntry$?) {
|
function createService(requestEntry$?) {
|
||||||
requestService = getMockRequestService(requestEntry$);
|
requestService = getMockRequestService(requestEntry$);
|
||||||
rdbService = jasmine.createSpyObj('rdbService', {
|
rdbService = jasmine.createSpyObj('rdbService', {
|
||||||
buildList: jasmine.createSpy('buildList')
|
buildList: hot('a|', {
|
||||||
|
a: paginatedListRD
|
||||||
|
})
|
||||||
});
|
});
|
||||||
objectCache = jasmine.createSpyObj('objectCache', {
|
objectCache = jasmine.createSpyObj('objectCache', {
|
||||||
remove: jasmine.createSpy('remove')
|
remove: jasmine.createSpy('remove')
|
||||||
|
@@ -72,14 +72,18 @@ export class CollectionDataService extends ComColDataService<Collection> {
|
|||||||
/**
|
/**
|
||||||
* Get all collections the user is authorized to submit to
|
* 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 options The [[FindListOptions]] object
|
* @param options The [[FindListOptions]] object
|
||||||
* @return Observable<RemoteData<PaginatedList<Collection>>>
|
* @return Observable<RemoteData<PaginatedList<Collection>>>
|
||||||
* collection list
|
* collection list
|
||||||
*/
|
*/
|
||||||
getAuthorizedCollection(options: FindListOptions = {}): Observable<RemoteData<PaginatedList<Collection>>> {
|
getAuthorizedCollection(query: string, options: FindListOptions = {}, ...linksToFollow: Array<FollowLinkConfig<Collection>>): Observable<RemoteData<PaginatedList<Collection>>> {
|
||||||
const searchHref = 'findAuthorized';
|
const searchHref = 'findSubmitAuthorized';
|
||||||
|
options = Object.assign({}, options, {
|
||||||
|
searchParams: [new RequestParam('query', query)]
|
||||||
|
});
|
||||||
|
|
||||||
return this.searchBy(searchHref, options).pipe(
|
return this.searchBy(searchHref, options, ...linksToFollow).pipe(
|
||||||
filter((collections: RemoteData<PaginatedList<Collection>>) => !collections.isResponsePending));
|
filter((collections: RemoteData<PaginatedList<Collection>>) => !collections.isResponsePending));
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -87,14 +91,18 @@ export class CollectionDataService extends ComColDataService<Collection> {
|
|||||||
* Get all collections the user is authorized to submit to, by community
|
* Get all collections the user is authorized to submit to, by community
|
||||||
*
|
*
|
||||||
* @param communityId The community id
|
* @param communityId The community id
|
||||||
|
* @param query limit the returned collection to those with metadata values matching the query terms.
|
||||||
* @param options The [[FindListOptions]] object
|
* @param options The [[FindListOptions]] object
|
||||||
* @return Observable<RemoteData<PaginatedList<Collection>>>
|
* @return Observable<RemoteData<PaginatedList<Collection>>>
|
||||||
* collection list
|
* collection list
|
||||||
*/
|
*/
|
||||||
getAuthorizedCollectionByCommunity(communityId: string, options: FindListOptions = {}): Observable<RemoteData<PaginatedList<Collection>>> {
|
getAuthorizedCollectionByCommunity(communityId: string, query: string, options: FindListOptions = {}): Observable<RemoteData<PaginatedList<Collection>>> {
|
||||||
const searchHref = 'findAuthorizedByCommunity';
|
const searchHref = 'findSubmitAuthorizedByCommunity';
|
||||||
options = Object.assign({}, options, {
|
options = Object.assign({}, options, {
|
||||||
searchParams: [new RequestParam('uuid', communityId)]
|
searchParams: [
|
||||||
|
new RequestParam('uuid', communityId),
|
||||||
|
new RequestParam('query', query)
|
||||||
|
]
|
||||||
});
|
});
|
||||||
|
|
||||||
return this.searchBy(searchHref, options).pipe(
|
return this.searchBy(searchHref, options).pipe(
|
||||||
@@ -108,7 +116,7 @@ export class CollectionDataService extends ComColDataService<Collection> {
|
|||||||
* true if the user has at least one collection to submit to
|
* true if the user has at least one collection to submit to
|
||||||
*/
|
*/
|
||||||
hasAuthorizedCollection(): Observable<boolean> {
|
hasAuthorizedCollection(): Observable<boolean> {
|
||||||
const searchHref = 'findAuthorized';
|
const searchHref = 'findSubmitAuthorized';
|
||||||
const options = new FindListOptions();
|
const options = new FindListOptions();
|
||||||
options.elementsPerPage = 1;
|
options.elementsPerPage = 1;
|
||||||
|
|
||||||
|
@@ -0,0 +1,43 @@
|
|||||||
|
<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>
|
||||||
|
<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)">
|
||||||
|
<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>
|
||||||
|
</li>
|
||||||
|
<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>
|
@@ -0,0 +1,15 @@
|
|||||||
|
.scrollable-menu {
|
||||||
|
height: auto;
|
||||||
|
max-height: $dropdown-menu-max-height;
|
||||||
|
overflow-x: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.collection-item {
|
||||||
|
border-bottom: $dropdown-border-width solid $dropdown-border-color;
|
||||||
|
}
|
||||||
|
|
||||||
|
#collectionControlsDropdownMenu {
|
||||||
|
outline: 0;
|
||||||
|
left: 0 !important;
|
||||||
|
box-shadow: $btn-focus-box-shadow;
|
||||||
|
}
|
@@ -0,0 +1,241 @@
|
|||||||
|
import { async, ComponentFixture, TestBed, fakeAsync, tick } from '@angular/core/testing';
|
||||||
|
|
||||||
|
import { CollectionDropdownComponent } from './collection-dropdown.component';
|
||||||
|
import { FollowLinkConfig } from '../utils/follow-link-config.model';
|
||||||
|
import { Observable, of } from 'rxjs';
|
||||||
|
import { RemoteData } from 'src/app/core/data/remote-data';
|
||||||
|
import { PaginatedList } from 'src/app/core/data/paginated-list';
|
||||||
|
import { cold, getTestScheduler, hot } from 'jasmine-marbles';
|
||||||
|
import { createSuccessfulRemoteDataObject } from '../remote-data.utils';
|
||||||
|
import { PageInfo } from 'src/app/core/shared/page-info.model';
|
||||||
|
import { Collection } from '../../core/shared/collection.model';
|
||||||
|
import { NO_ERRORS_SCHEMA, ChangeDetectorRef, ElementRef } from '@angular/core';
|
||||||
|
import { CollectionDataService } from 'src/app/core/data/collection-data.service';
|
||||||
|
import { FindListOptions } from 'src/app/core/data/request.models';
|
||||||
|
import { TranslateModule, TranslateLoader } from '@ngx-translate/core';
|
||||||
|
import { TranslateLoaderMock } from '../mocks/translate-loader.mock';
|
||||||
|
import { TestScheduler } from 'rxjs/testing';
|
||||||
|
import { By } from '@angular/platform-browser';
|
||||||
|
import { Community } from 'src/app/core/shared/community.model';
|
||||||
|
|
||||||
|
const community: Community = Object.assign(new Community(), {
|
||||||
|
id: 'ce64f48e-2c9b-411a-ac36-ee429c0e6a88',
|
||||||
|
uuid: 'ce64f48e-2c9b-411a-ac36-ee429c0e6a88',
|
||||||
|
name: 'Community 1'
|
||||||
|
});
|
||||||
|
|
||||||
|
const collections: Collection[] = [
|
||||||
|
Object.assign(new Collection(), {
|
||||||
|
id: 'ce64f48e-2c9b-411a-ac36-ee429c0e6a88',
|
||||||
|
name: 'Collection 1',
|
||||||
|
metadata: [
|
||||||
|
{
|
||||||
|
key: 'dc.title',
|
||||||
|
language: 'en_US',
|
||||||
|
value: 'Community 1-Collection 1'
|
||||||
|
}],
|
||||||
|
parentCommunity: of(
|
||||||
|
new RemoteData(false, false, true, undefined, community, 200)
|
||||||
|
)
|
||||||
|
}),
|
||||||
|
Object.assign(new Collection(), {
|
||||||
|
id: '59ee713b-ee53-4220-8c3f-9860dc84fe33',
|
||||||
|
name: 'Collection 2',
|
||||||
|
metadata: [
|
||||||
|
{
|
||||||
|
key: 'dc.title',
|
||||||
|
language: 'en_US',
|
||||||
|
value: 'Community 1-Collection 2'
|
||||||
|
}],
|
||||||
|
parentCommunity: of(
|
||||||
|
new RemoteData(false, false, true, undefined, community, 200)
|
||||||
|
)
|
||||||
|
}),
|
||||||
|
Object.assign(new Collection(), {
|
||||||
|
id: 'e9dbf393-7127-415f-8919-55be34a6e9ed',
|
||||||
|
name: 'Collection 3',
|
||||||
|
metadata: [
|
||||||
|
{
|
||||||
|
key: 'dc.title',
|
||||||
|
language: 'en_US',
|
||||||
|
value: 'Community 1-Collection 3'
|
||||||
|
}],
|
||||||
|
parentCommunity: of(
|
||||||
|
new RemoteData(false, false, true, undefined, community, 200)
|
||||||
|
)
|
||||||
|
}),
|
||||||
|
Object.assign(new Collection(), {
|
||||||
|
id: '59da2ff0-9bf4-45bf-88be-e35abd33f304',
|
||||||
|
name: 'Collection 4',
|
||||||
|
metadata: [
|
||||||
|
{
|
||||||
|
key: 'dc.title',
|
||||||
|
language: 'en_US',
|
||||||
|
value: 'Community 1-Collection 4'
|
||||||
|
}],
|
||||||
|
parentCommunity: of(
|
||||||
|
new RemoteData(false, false, true, undefined, community, 200)
|
||||||
|
)
|
||||||
|
}),
|
||||||
|
Object.assign(new Collection(), {
|
||||||
|
id: 'a5159760-f362-4659-9e81-e3253ad91ede',
|
||||||
|
name: 'Collection 5',
|
||||||
|
metadata: [
|
||||||
|
{
|
||||||
|
key: 'dc.title',
|
||||||
|
language: 'en_US',
|
||||||
|
value: 'Community 1-Collection 5'
|
||||||
|
}],
|
||||||
|
parentCommunity: of(
|
||||||
|
new RemoteData(false, false, true, undefined, community, 200)
|
||||||
|
)
|
||||||
|
})
|
||||||
|
];
|
||||||
|
|
||||||
|
const listElementMock = {
|
||||||
|
communities: [
|
||||||
|
{
|
||||||
|
id: 'ce64f48e-2c9b-411a-ac36-ee429c0e6a88',
|
||||||
|
name: 'Community 1'
|
||||||
|
}
|
||||||
|
],
|
||||||
|
collection: {
|
||||||
|
id: 'e9dbf393-7127-415f-8919-55be34a6e9ed',
|
||||||
|
uuid: 'e9dbf393-7127-415f-8919-55be34a6e9ed',
|
||||||
|
name: 'Collection 3'
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// tslint:disable-next-line: max-classes-per-file
|
||||||
|
class CollectionDataServiceMock {
|
||||||
|
getAuthorizedCollection(query: string, options: FindListOptions = {}, ...linksToFollow: Array<FollowLinkConfig<Collection>>): Observable<RemoteData<PaginatedList<Collection>>> {
|
||||||
|
return of(
|
||||||
|
createSuccessfulRemoteDataObject(
|
||||||
|
new PaginatedList(new PageInfo(), collections)
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
describe('CollectionDropdownComponent', () => {
|
||||||
|
let component: CollectionDropdownComponent;
|
||||||
|
let fixture: ComponentFixture<CollectionDropdownComponent>;
|
||||||
|
let scheduler: TestScheduler;
|
||||||
|
const searchedCollection = 'TEXT';
|
||||||
|
|
||||||
|
beforeEach(async(() => {
|
||||||
|
TestBed.configureTestingModule({
|
||||||
|
imports: [
|
||||||
|
TranslateModule.forRoot({
|
||||||
|
loader: {
|
||||||
|
provide: TranslateLoader,
|
||||||
|
useClass: TranslateLoaderMock
|
||||||
|
}
|
||||||
|
})
|
||||||
|
],
|
||||||
|
declarations: [ CollectionDropdownComponent ],
|
||||||
|
providers: [
|
||||||
|
{provide: CollectionDataService, useClass: CollectionDataServiceMock},
|
||||||
|
{provide: ChangeDetectorRef, useValue: {}},
|
||||||
|
{provide: ElementRef, userValue: {}}
|
||||||
|
],
|
||||||
|
schemas: [NO_ERRORS_SCHEMA]
|
||||||
|
})
|
||||||
|
.compileComponents();
|
||||||
|
}));
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
scheduler = getTestScheduler();
|
||||||
|
fixture = TestBed.createComponent(CollectionDropdownComponent);
|
||||||
|
component = fixture.componentInstance;
|
||||||
|
fixture.detectChanges();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should populate collections list with five items', () => {
|
||||||
|
const elements = fixture.debugElement.queryAll(By.css('.collection-item'));
|
||||||
|
expect(elements.length).toEqual(5);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should trigger onSelect method when select a new collection from list', fakeAsync(() => {
|
||||||
|
spyOn(component, 'onSelect');
|
||||||
|
const collectionItem = fixture.debugElement.query(By.css('.collection-item:nth-child(2)'));
|
||||||
|
collectionItem.triggerEventHandler('click', null);
|
||||||
|
fixture.detectChanges();
|
||||||
|
tick();
|
||||||
|
fixture.whenStable().then(() => {
|
||||||
|
expect(component.onSelect).toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
}));
|
||||||
|
|
||||||
|
it('should init component with collection list', fakeAsync(() => {
|
||||||
|
spyOn(component.subs, 'push').and.callThrough();
|
||||||
|
spyOn(component, 'resetPagination').and.callThrough();
|
||||||
|
spyOn(component, 'populateCollectionList').and.callThrough();
|
||||||
|
component.ngOnInit();
|
||||||
|
tick();
|
||||||
|
fixture.detectChanges();
|
||||||
|
|
||||||
|
fixture.whenStable().then(() => {
|
||||||
|
expect(component.subs.push).toHaveBeenCalled();
|
||||||
|
expect(component.resetPagination).toHaveBeenCalled();
|
||||||
|
expect(component.populateCollectionList).toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
}));
|
||||||
|
|
||||||
|
it('should emit collectionChange event when selecting a new collection', () => {
|
||||||
|
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 reset collections list after reset of searchField', fakeAsync(() => {
|
||||||
|
spyOn(component.subs, 'push').and.callThrough();
|
||||||
|
spyOn(component, 'reset').and.callThrough();
|
||||||
|
spyOn(component.searchField, 'setValue').and.callThrough();
|
||||||
|
spyOn(component, 'resetPagination').and.callThrough();
|
||||||
|
spyOn(component, 'populateCollectionList').and.callThrough();
|
||||||
|
component.reset();
|
||||||
|
const input = fixture.debugElement.query(By.css('input.form-control'));
|
||||||
|
const el = input.nativeElement;
|
||||||
|
el.value = searchedCollection;
|
||||||
|
el.dispatchEvent(new Event('input'));
|
||||||
|
fixture.detectChanges();
|
||||||
|
tick(500);
|
||||||
|
|
||||||
|
fixture.whenStable().then(() => {
|
||||||
|
expect(component.reset).toHaveBeenCalled();
|
||||||
|
expect(component.searchField.setValue).toHaveBeenCalledWith('');
|
||||||
|
expect(component.resetPagination).toHaveBeenCalled();
|
||||||
|
expect(component.currentQuery).toEqual('');
|
||||||
|
expect(component.populateCollectionList).toHaveBeenCalledWith(component.currentQuery, component.currentPage);
|
||||||
|
expect(component.searchListCollection).toEqual(collections as any);
|
||||||
|
expect(component.subs.push).toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
}));
|
||||||
|
|
||||||
|
it('should reset searchField when dropdown menu has been closed', () => {
|
||||||
|
spyOn(component.searchField, 'setValue').and.callThrough();
|
||||||
|
component.reset();
|
||||||
|
|
||||||
|
expect(component.searchField.setValue).toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
|
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.currentQuery).toEqual('');
|
||||||
|
expect(component.hasNextPage).toEqual(true);
|
||||||
|
expect(component.searchListCollection).toEqual([]);
|
||||||
|
});
|
||||||
|
});
|
@@ -0,0 +1,236 @@
|
|||||||
|
import { Component, OnInit, HostListener, ChangeDetectorRef, OnDestroy, Output, EventEmitter, ElementRef } from '@angular/core';
|
||||||
|
import { FormControl } from '@angular/forms';
|
||||||
|
import { Observable, Subscription, BehaviorSubject } from 'rxjs';
|
||||||
|
import { hasValue } from '../empty.util';
|
||||||
|
import { map, mergeMap, startWith, debounceTime, distinctUntilChanged, switchMap, merge, scan, reduce } from 'rxjs/operators';
|
||||||
|
import { RemoteData } from 'src/app/core/data/remote-data';
|
||||||
|
import { FindListOptions } from 'src/app/core/data/request.models';
|
||||||
|
import { PaginatedList } from 'src/app/core/data/paginated-list';
|
||||||
|
import { Community } from 'src/app/core/shared/community.model';
|
||||||
|
import { CollectionDataService } from 'src/app/core/data/collection-data.service';
|
||||||
|
import { Collection } from '../../core/shared/collection.model';
|
||||||
|
import { followLink } from '../utils/follow-link-config.model';
|
||||||
|
import { getFirstSucceededRemoteDataPayload, getSucceededRemoteWithNotEmptyData } from '../../core/shared/operators';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* An interface to represent a collection entry
|
||||||
|
*/
|
||||||
|
interface CollectionListEntryItem {
|
||||||
|
id: string;
|
||||||
|
uuid: string;
|
||||||
|
name: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* An interface to represent an entry in the collection list
|
||||||
|
*/
|
||||||
|
interface CollectionListEntry {
|
||||||
|
communities: CollectionListEntryItem[],
|
||||||
|
collection: CollectionListEntryItem
|
||||||
|
}
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
selector: 'ds-collection-dropdown',
|
||||||
|
templateUrl: './collection-dropdown.component.html',
|
||||||
|
styleUrls: ['./collection-dropdown.component.scss']
|
||||||
|
})
|
||||||
|
export class CollectionDropdownComponent implements OnInit, OnDestroy {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The search form control
|
||||||
|
* @type {FormControl}
|
||||||
|
*/
|
||||||
|
public searchField: FormControl = new FormControl();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The collection list obtained from a search
|
||||||
|
* @type {Observable<CollectionListEntry[]>}
|
||||||
|
*/
|
||||||
|
public searchListCollection$: Observable<CollectionListEntry[]>;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Array to track all subscriptions and unsubscribe them onDestroy
|
||||||
|
* @type {Array}
|
||||||
|
*/
|
||||||
|
public subs: Subscription[] = [];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The list of collection to render
|
||||||
|
*/
|
||||||
|
searchListCollection: CollectionListEntry[] = [];
|
||||||
|
|
||||||
|
@Output() selectionChange = new EventEmitter<CollectionListEntry>();
|
||||||
|
/**
|
||||||
|
* A boolean representing if the loader is visible or not
|
||||||
|
*/
|
||||||
|
isLoadingList: BehaviorSubject<boolean> = new BehaviorSubject(false);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A numeric representig current page
|
||||||
|
*/
|
||||||
|
currentPage: number;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A boolean representing if exist another page to render
|
||||||
|
*/
|
||||||
|
hasNextPage: boolean;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Current seach query used to filter collection list
|
||||||
|
*/
|
||||||
|
currentQuery: string;
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
private changeDetectorRef: ChangeDetectorRef,
|
||||||
|
private collectionDataService: CollectionDataService,
|
||||||
|
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 collection list
|
||||||
|
*/
|
||||||
|
ngOnInit() {
|
||||||
|
this.subs.push(this.searchField.valueChanges.pipe(
|
||||||
|
debounceTime(500),
|
||||||
|
distinctUntilChanged(),
|
||||||
|
startWith('')
|
||||||
|
).subscribe(
|
||||||
|
(next) => {
|
||||||
|
if (hasValue(next) && next !== this.currentQuery) {
|
||||||
|
this.resetPagination();
|
||||||
|
this.currentQuery = next;
|
||||||
|
this.populateCollectionList(this.currentQuery, this.currentPage);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
));
|
||||||
|
// Workaround for prevent the scroll of main page when this component is placed in a dialog
|
||||||
|
setTimeout(() => this.el.nativeElement.querySelector('input').focus(), 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if dropdown scrollbar is at the top or bottom of the dropdown list
|
||||||
|
*
|
||||||
|
* @param event
|
||||||
|
*/
|
||||||
|
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
|
||||||
|
*/
|
||||||
|
onScrollDown() {
|
||||||
|
if ( this.hasNextPage ) {
|
||||||
|
this.populateCollectionList(this.currentQuery, ++this.currentPage);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Emit a [selectionChange] event when a new collection is selected from list
|
||||||
|
*
|
||||||
|
* @param event
|
||||||
|
* the selected [CollectionListEntry]
|
||||||
|
*/
|
||||||
|
onSelect(event: CollectionListEntry) {
|
||||||
|
this.selectionChange.emit(event);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Method called for populate the collection list
|
||||||
|
* @param query text for filter the collection list
|
||||||
|
* @param page page number
|
||||||
|
*/
|
||||||
|
populateCollectionList(query: string, page: number) {
|
||||||
|
this.isLoadingList.next(true);
|
||||||
|
// Set the pagination info
|
||||||
|
const findOptions: FindListOptions = {
|
||||||
|
elementsPerPage: 10,
|
||||||
|
currentPage: page
|
||||||
|
};
|
||||||
|
this.searchListCollection$ = this.collectionDataService
|
||||||
|
.getAuthorizedCollection(query, findOptions, followLink('parentCommunity'))
|
||||||
|
.pipe(
|
||||||
|
getSucceededRemoteWithNotEmptyData(),
|
||||||
|
switchMap((collections: RemoteData<PaginatedList<Collection>>) => {
|
||||||
|
if ( (this.searchListCollection.length + findOptions.elementsPerPage) >= collections.payload.totalElements ) {
|
||||||
|
this.hasNextPage = false;
|
||||||
|
}
|
||||||
|
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(); }
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Unsubscribe from all subscriptions
|
||||||
|
*/
|
||||||
|
ngOnDestroy(): void {
|
||||||
|
this.subs.filter((sub) => hasValue(sub)).forEach((sub) => sub.unsubscribe());
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Reset search form control
|
||||||
|
*/
|
||||||
|
reset() {
|
||||||
|
this.searchField.setValue('');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Reset pagination values
|
||||||
|
*/
|
||||||
|
resetPagination() {
|
||||||
|
this.currentPage = 1;
|
||||||
|
this.currentQuery = '';
|
||||||
|
this.hasNextPage = true;
|
||||||
|
this.searchListCollection = [];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Hide/Show the collection list loader
|
||||||
|
* @param hideShow true for show, false otherwise
|
||||||
|
*/
|
||||||
|
hideShowLoader(hideShow: boolean) {
|
||||||
|
this.isLoadingList.next(hideShow);
|
||||||
|
}
|
||||||
|
}
|
@@ -0,0 +1,11 @@
|
|||||||
|
<div>
|
||||||
|
<div class="modal-header">{{'dso-selector.'+ action + '.' + objectType.toString().toLowerCase() + '.head' | translate}}
|
||||||
|
<button type="button" class="close" (click)="close()" aria-label="Close">
|
||||||
|
<span aria-hidden="true">×</span>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<div class="modal-body">
|
||||||
|
<ds-collection-dropdown (selectionChange)="selectObject($event.collection)">
|
||||||
|
</ds-collection-dropdown>
|
||||||
|
</div>
|
||||||
|
</div>
|
@@ -13,7 +13,8 @@ import { DSOSelectorModalWrapperComponent, SelectorActionType } from '../dso-sel
|
|||||||
@Component({
|
@Component({
|
||||||
selector: 'ds-create-item-parent-selector',
|
selector: 'ds-create-item-parent-selector',
|
||||||
// styleUrls: ['./create-item-parent-selector.component.scss'],
|
// styleUrls: ['./create-item-parent-selector.component.scss'],
|
||||||
templateUrl: '../dso-selector-modal-wrapper.component.html',
|
// templateUrl: '../dso-selector-modal-wrapper.component.html',
|
||||||
|
templateUrl: './create-item-parent-selector.component.html'
|
||||||
})
|
})
|
||||||
export class CreateItemParentSelectorComponent extends DSOSelectorModalWrapperComponent implements OnInit {
|
export class CreateItemParentSelectorComponent extends DSOSelectorModalWrapperComponent implements OnInit {
|
||||||
objectType = DSpaceObjectType.ITEM;
|
objectType = DSpaceObjectType.ITEM;
|
||||||
|
@@ -202,6 +202,7 @@ import { ResourcePolicyTargetResolver } from './resource-policies/resolvers/reso
|
|||||||
import { ResourcePolicyResolver } from './resource-policies/resolvers/resource-policy.resolver';
|
import { ResourcePolicyResolver } from './resource-policies/resolvers/resource-policy.resolver';
|
||||||
import { EpersonSearchBoxComponent } from './resource-policies/form/eperson-group-list/eperson-search-box/eperson-search-box.component';
|
import { EpersonSearchBoxComponent } from './resource-policies/form/eperson-group-list/eperson-search-box/eperson-search-box.component';
|
||||||
import { GroupSearchBoxComponent } from './resource-policies/form/eperson-group-list/group-search-box/group-search-box.component';
|
import { GroupSearchBoxComponent } from './resource-policies/form/eperson-group-list/group-search-box/group-search-box.component';
|
||||||
|
import { CollectionDropdownComponent } from './collection-dropdown/collection-dropdown.component';
|
||||||
|
|
||||||
const MODULES = [
|
const MODULES = [
|
||||||
// Do NOT include UniversalModule, HttpModule, or JsonpModule here
|
// Do NOT include UniversalModule, HttpModule, or JsonpModule here
|
||||||
@@ -386,7 +387,8 @@ const COMPONENTS = [
|
|||||||
ResourcePolicyFormComponent,
|
ResourcePolicyFormComponent,
|
||||||
EpersonGroupListComponent,
|
EpersonGroupListComponent,
|
||||||
EpersonSearchBoxComponent,
|
EpersonSearchBoxComponent,
|
||||||
GroupSearchBoxComponent
|
GroupSearchBoxComponent,
|
||||||
|
CollectionDropdownComponent
|
||||||
];
|
];
|
||||||
|
|
||||||
const ENTRY_COMPONENTS = [
|
const ENTRY_COMPONENTS = [
|
||||||
@@ -504,8 +506,7 @@ const DIRECTIVES = [
|
|||||||
...COMPONENTS,
|
...COMPONENTS,
|
||||||
...DIRECTIVES,
|
...DIRECTIVES,
|
||||||
...ENTRY_COMPONENTS,
|
...ENTRY_COMPONENTS,
|
||||||
...SHARED_ITEM_PAGE_COMPONENTS,
|
...SHARED_ITEM_PAGE_COMPONENTS
|
||||||
|
|
||||||
],
|
],
|
||||||
providers: [
|
providers: [
|
||||||
...PROVIDERS
|
...PROVIDERS
|
||||||
|
@@ -1,5 +1,20 @@
|
|||||||
<div>
|
<div>
|
||||||
<div ngbDropdown #collectionControls="ngbDropdown" class="btn-group input-group" (openChange)="toggled($event)">
|
<div
|
||||||
|
*ngIf="!(available$ | async)"
|
||||||
|
class="input-group mb-3">
|
||||||
|
<div class="input-group-prepend">
|
||||||
|
<span class="input-group-text">{{ 'submission.sections.general.collection' | translate }}</span>
|
||||||
|
</div>
|
||||||
|
<div class="input-group-append">
|
||||||
|
<span class="input-group-text">{{ selectedCollectionName$ | async }}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
ngbDropdown
|
||||||
|
#collectionControls="ngbDropdown"
|
||||||
|
*ngIf="(available$ | async)"
|
||||||
|
class="btn-group input-group"
|
||||||
|
(openChange)="toggled($event)">
|
||||||
<div class="input-group-prepend">
|
<div class="input-group-prepend">
|
||||||
<span id="collectionControlsMenuLabel" class="input-group-text">
|
<span id="collectionControlsMenuLabel" class="input-group-text">
|
||||||
{{ 'submission.sections.general.collection' | translate }}
|
{{ 'submission.sections.general.collection' | translate }}
|
||||||
@@ -10,7 +25,7 @@
|
|||||||
class="btn btn-outline-primary"
|
class="btn btn-outline-primary"
|
||||||
(blur)="onClose()"
|
(blur)="onClose()"
|
||||||
(click)="onClose()"
|
(click)="onClose()"
|
||||||
[disabled]="(disabled$ | async) || (processingChange$ | async)"
|
[disabled]="(processingChange$ | async)"
|
||||||
ngbDropdownToggle>
|
ngbDropdownToggle>
|
||||||
<span *ngIf="(processingChange$ | async)"><i class='fas fa-circle-notch fa-spin'></i></span>
|
<span *ngIf="(processingChange$ | async)"><i class='fas fa-circle-notch fa-spin'></i></span>
|
||||||
<span *ngIf="!(processingChange$ | async)">{{ selectedCollectionName$ | async }}</span>
|
<span *ngIf="!(processingChange$ | async)">{{ selectedCollectionName$ | async }}</span>
|
||||||
@@ -20,31 +35,9 @@
|
|||||||
class="dropdown-menu"
|
class="dropdown-menu"
|
||||||
id="collectionControlsDropdownMenu"
|
id="collectionControlsDropdownMenu"
|
||||||
aria-labelledby="collectionControlsMenuButton">
|
aria-labelledby="collectionControlsMenuButton">
|
||||||
<div class="form-group w-100 pr-2 pl-2">
|
<ds-collection-dropdown
|
||||||
<input *ngIf="searchField"
|
(selectionChange)="onSelect($event)">
|
||||||
type="search"
|
</ds-collection-dropdown>
|
||||||
class="form-control w-100"
|
|
||||||
(click)="$event.stopPropagation();"
|
|
||||||
placeholder="{{ 'submission.sections.general.search-collection' | translate }}"
|
|
||||||
[formControl]="searchField">
|
|
||||||
</div>
|
|
||||||
<div class="dropdown-divider"></div>
|
|
||||||
<div class="scrollable-menu" aria-labelledby="dropdownMenuButton" (scroll)="onScroll($event)">
|
|
||||||
<button class="dropdown-item disabled" *ngIf="(searchListCollection$ | async)?.length == 0">
|
|
||||||
{{'submission.sections.general.no-collection' | translate}}
|
|
||||||
</button>
|
|
||||||
<button *ngFor="let listItem of (searchListCollection$ | async)"
|
|
||||||
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>
|
|
||||||
</li>
|
|
||||||
<li class="list-item text-truncate text-primary font-weight-bold">{{ listItem.collection.name}}</li>
|
|
||||||
</ul>
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@@ -1,14 +1,11 @@
|
|||||||
import { ChangeDetectorRef, Component, CUSTOM_ELEMENTS_SCHEMA, DebugElement, SimpleChange } from '@angular/core';
|
import { ChangeDetectorRef, Component, CUSTOM_ELEMENTS_SCHEMA, DebugElement } from '@angular/core';
|
||||||
import { async, ComponentFixture, fakeAsync, inject, TestBed, tick } from '@angular/core/testing';
|
import { async, ComponentFixture, fakeAsync, inject, TestBed, tick } from '@angular/core/testing';
|
||||||
import { By } from '@angular/platform-browser';
|
import { By } from '@angular/platform-browser';
|
||||||
import { FormsModule, ReactiveFormsModule } from '@angular/forms';
|
import { FormsModule, ReactiveFormsModule } from '@angular/forms';
|
||||||
|
|
||||||
import { of as observableOf } from 'rxjs';
|
|
||||||
import { filter } from 'rxjs/operators';
|
|
||||||
import { TranslateModule } from '@ngx-translate/core';
|
import { TranslateModule } from '@ngx-translate/core';
|
||||||
import { NgbModule } from '@ng-bootstrap/ng-bootstrap';
|
import { NgbModule } from '@ng-bootstrap/ng-bootstrap';
|
||||||
import { Store } from '@ngrx/store';
|
import { Store } from '@ngrx/store';
|
||||||
import { cold } from 'jasmine-marbles';
|
|
||||||
|
|
||||||
import { SubmissionServiceStub } from '../../../shared/testing/submission-service.stub';
|
import { SubmissionServiceStub } from '../../../shared/testing/submission-service.stub';
|
||||||
import { mockSubmissionId, mockSubmissionRestResponse } from '../../../shared/mocks/submission.mock';
|
import { mockSubmissionId, mockSubmissionRestResponse } from '../../../shared/mocks/submission.mock';
|
||||||
@@ -19,121 +16,26 @@ import { SubmissionJsonPatchOperationsService } from '../../../core/submission/s
|
|||||||
import { SubmissionJsonPatchOperationsServiceStub } from '../../../shared/testing/submission-json-patch-operations-service.stub';
|
import { SubmissionJsonPatchOperationsServiceStub } from '../../../shared/testing/submission-json-patch-operations-service.stub';
|
||||||
import { JsonPatchOperationsBuilder } from '../../../core/json-patch/builder/json-patch-operations-builder';
|
import { JsonPatchOperationsBuilder } from '../../../core/json-patch/builder/json-patch-operations-builder';
|
||||||
import { JsonPatchOperationPathCombiner } from '../../../core/json-patch/builder/json-patch-operation-path-combiner';
|
import { JsonPatchOperationPathCombiner } from '../../../core/json-patch/builder/json-patch-operation-path-combiner';
|
||||||
import { RemoteData } from '../../../core/data/remote-data';
|
|
||||||
import { Community } from '../../../core/shared/community.model';
|
|
||||||
import { PaginatedList } from '../../../core/data/paginated-list';
|
|
||||||
import { PageInfo } from '../../../core/shared/page-info.model';
|
|
||||||
import { Collection } from '../../../core/shared/collection.model';
|
|
||||||
import { createTestComponent } from '../../../shared/testing/utils.test';
|
import { createTestComponent } from '../../../shared/testing/utils.test';
|
||||||
import { CollectionDataService } from '../../../core/data/collection-data.service';
|
import { CollectionDataService } from '../../../core/data/collection-data.service';
|
||||||
|
import { hot, cold } from 'jasmine-marbles';
|
||||||
|
import { of } from 'rxjs';
|
||||||
|
import { SectionsService } from '../../sections/sections.service';
|
||||||
|
import { componentFactoryName } from '@angular/compiler';
|
||||||
|
import { Collection } from 'src/app/core/shared/collection.model';
|
||||||
|
|
||||||
const subcommunities = [Object.assign(new Community(), {
|
describe('SubmissionFormCollectionComponent Component', () => {
|
||||||
name: 'SubCommunity 1',
|
|
||||||
id: '123456789-1',
|
|
||||||
metadata: [
|
|
||||||
{
|
|
||||||
key: 'dc.title',
|
|
||||||
language: 'en_US',
|
|
||||||
value: 'SubCommunity 1'
|
|
||||||
}]
|
|
||||||
}),
|
|
||||||
Object.assign(new Community(), {
|
|
||||||
name: 'SubCommunity 1',
|
|
||||||
id: '123456789s-1',
|
|
||||||
metadata: [
|
|
||||||
{
|
|
||||||
key: 'dc.title',
|
|
||||||
language: 'en_US',
|
|
||||||
value: 'SubCommunity 1'
|
|
||||||
}]
|
|
||||||
})
|
|
||||||
];
|
|
||||||
|
|
||||||
const mockCommunity1Collection1 = Object.assign(new Collection(), {
|
let comp: SubmissionFormCollectionComponent;
|
||||||
name: 'Community 1-Collection 1',
|
let compAsAny: any;
|
||||||
id: '1234567890-1',
|
let fixture: ComponentFixture<SubmissionFormCollectionComponent>;
|
||||||
metadata: [
|
let submissionServiceStub: SubmissionServiceStub;
|
||||||
{
|
let jsonPatchOpServiceStub: SubmissionJsonPatchOperationsServiceStub;
|
||||||
key: 'dc.title',
|
|
||||||
language: 'en_US',
|
|
||||||
value: 'Community 1-Collection 1'
|
|
||||||
}]
|
|
||||||
});
|
|
||||||
|
|
||||||
const mockCommunity1Collection2 = Object.assign(new Collection(), {
|
const submissionId = mockSubmissionId;
|
||||||
name: 'Community 1-Collection 2',
|
const collectionId = '1234567890-1';
|
||||||
id: '1234567890-2',
|
const definition = 'traditional';
|
||||||
metadata: [
|
const submissionRestResponse = mockSubmissionRestResponse;
|
||||||
{
|
|
||||||
key: 'dc.title',
|
|
||||||
language: 'en_US',
|
|
||||||
value: 'Community 1-Collection 2'
|
|
||||||
}]
|
|
||||||
});
|
|
||||||
|
|
||||||
const mockCommunity2Collection1 = Object.assign(new Collection(), {
|
|
||||||
name: 'Community 2-Collection 1',
|
|
||||||
id: '1234567890-3',
|
|
||||||
metadata: [
|
|
||||||
{
|
|
||||||
key: 'dc.title',
|
|
||||||
language: 'en_US',
|
|
||||||
value: 'Community 2-Collection 1'
|
|
||||||
}]
|
|
||||||
});
|
|
||||||
|
|
||||||
const mockCommunity2Collection2 = Object.assign(new Collection(), {
|
|
||||||
name: 'Community 2-Collection 2',
|
|
||||||
id: '1234567890-4',
|
|
||||||
metadata: [
|
|
||||||
{
|
|
||||||
key: 'dc.title',
|
|
||||||
language: 'en_US',
|
|
||||||
value: 'Community 2-Collection 2'
|
|
||||||
}]
|
|
||||||
});
|
|
||||||
|
|
||||||
const mockCommunity = Object.assign(new Community(), {
|
|
||||||
name: 'Community 1',
|
|
||||||
id: '123456789-1',
|
|
||||||
metadata: [
|
|
||||||
{
|
|
||||||
key: 'dc.title',
|
|
||||||
language: 'en_US',
|
|
||||||
value: 'Community 1'
|
|
||||||
}],
|
|
||||||
collections: observableOf(new RemoteData(true, true, true,
|
|
||||||
undefined, new PaginatedList(new PageInfo(), [mockCommunity1Collection1, mockCommunity1Collection2]))),
|
|
||||||
subcommunities: observableOf(new RemoteData(true, true, true,
|
|
||||||
undefined, new PaginatedList(new PageInfo(), subcommunities))),
|
|
||||||
});
|
|
||||||
|
|
||||||
const mockCommunity2 = Object.assign(new Community(), {
|
|
||||||
name: 'Community 2',
|
|
||||||
id: '123456789-2',
|
|
||||||
metadata: [
|
|
||||||
{
|
|
||||||
key: 'dc.title',
|
|
||||||
language: 'en_US',
|
|
||||||
value: 'Community 2'
|
|
||||||
}],
|
|
||||||
collections: observableOf(new RemoteData(true, true, true,
|
|
||||||
undefined, new PaginatedList(new PageInfo(), [mockCommunity2Collection1, mockCommunity2Collection2]))),
|
|
||||||
subcommunities: observableOf(new RemoteData(true, true, true,
|
|
||||||
undefined, new PaginatedList(new PageInfo(), []))),
|
|
||||||
});
|
|
||||||
|
|
||||||
const mockCommunity1Collection1Rd = observableOf(new RemoteData(true, true, true,
|
|
||||||
undefined, mockCommunity1Collection1));
|
|
||||||
|
|
||||||
const mockCommunityList = observableOf(new RemoteData(true, true, true,
|
|
||||||
undefined, new PaginatedList(new PageInfo(), [mockCommunity, mockCommunity2])));
|
|
||||||
|
|
||||||
const mockCommunityCollectionList = observableOf(new RemoteData(true, true, true,
|
|
||||||
undefined, new PaginatedList(new PageInfo(), [mockCommunity1Collection1, mockCommunity1Collection2])));
|
|
||||||
|
|
||||||
const mockCommunity2CollectionList = observableOf(new RemoteData(true, true, true,
|
|
||||||
undefined, new PaginatedList(new PageInfo(), [mockCommunity2Collection1, mockCommunity2Collection2])));
|
|
||||||
|
|
||||||
const mockCollectionList = [
|
const mockCollectionList = [
|
||||||
{
|
{
|
||||||
@@ -186,20 +88,6 @@ const mockCollectionList = [
|
|||||||
}
|
}
|
||||||
];
|
];
|
||||||
|
|
||||||
describe('SubmissionFormCollectionComponent Component', () => {
|
|
||||||
|
|
||||||
let comp: SubmissionFormCollectionComponent;
|
|
||||||
let compAsAny: any;
|
|
||||||
let fixture: ComponentFixture<SubmissionFormCollectionComponent>;
|
|
||||||
let submissionServiceStub: SubmissionServiceStub;
|
|
||||||
let jsonPatchOpServiceStub: SubmissionJsonPatchOperationsServiceStub;
|
|
||||||
|
|
||||||
const submissionId = mockSubmissionId;
|
|
||||||
const collectionId = '1234567890-1';
|
|
||||||
const definition = 'traditional';
|
|
||||||
const submissionRestResponse = mockSubmissionRestResponse;
|
|
||||||
const searchedCollection = 'Community 2-Collection 2';
|
|
||||||
|
|
||||||
const communityDataService: any = jasmine.createSpyObj('communityDataService', {
|
const communityDataService: any = jasmine.createSpyObj('communityDataService', {
|
||||||
findAll: jasmine.createSpy('findAll')
|
findAll: jasmine.createSpy('findAll')
|
||||||
});
|
});
|
||||||
@@ -217,6 +105,10 @@ describe('SubmissionFormCollectionComponent Component', () => {
|
|||||||
replace: jasmine.createSpy('replace')
|
replace: jasmine.createSpy('replace')
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const sectionsService: any = jasmine.createSpyObj('sectionsService', {
|
||||||
|
isSectionAvailable: of(true)
|
||||||
|
});
|
||||||
|
|
||||||
beforeEach(async(() => {
|
beforeEach(async(() => {
|
||||||
TestBed.configureTestingModule({
|
TestBed.configureTestingModule({
|
||||||
imports: [
|
imports: [
|
||||||
@@ -236,6 +128,7 @@ describe('SubmissionFormCollectionComponent Component', () => {
|
|||||||
{ provide: CommunityDataService, useValue: communityDataService },
|
{ provide: CommunityDataService, useValue: communityDataService },
|
||||||
{ provide: JsonPatchOperationsBuilder, useValue: jsonPatchOpBuilder },
|
{ provide: JsonPatchOperationsBuilder, useValue: jsonPatchOpBuilder },
|
||||||
{ provide: Store, useValue: store },
|
{ provide: Store, useValue: store },
|
||||||
|
{ provide: SectionsService, useValue: sectionsService },
|
||||||
ChangeDetectorRef,
|
ChangeDetectorRef,
|
||||||
SubmissionFormCollectionComponent
|
SubmissionFormCollectionComponent
|
||||||
],
|
],
|
||||||
@@ -299,72 +192,11 @@ describe('SubmissionFormCollectionComponent Component', () => {
|
|||||||
expect(compAsAny.pathCombiner).toEqual(expected);
|
expect(compAsAny.pathCombiner).toEqual(expected);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should init collection list properly', () => {
|
|
||||||
communityDataService.findAll.and.returnValue(mockCommunityList);
|
|
||||||
collectionDataService.findById.and.returnValue(mockCommunity1Collection1Rd);
|
|
||||||
collectionDataService.getAuthorizedCollectionByCommunity.and.returnValues(mockCommunityCollectionList, mockCommunity2CollectionList);
|
|
||||||
|
|
||||||
comp.ngOnChanges({
|
|
||||||
currentCollectionId: new SimpleChange(null, collectionId, true)
|
|
||||||
});
|
|
||||||
|
|
||||||
expect(comp.searchListCollection$).toBeObservable(cold('(ab)', {
|
|
||||||
a: [],
|
|
||||||
b: mockCollectionList
|
|
||||||
}));
|
|
||||||
|
|
||||||
expect(comp.selectedCollectionName$).toBeObservable(cold('(a|)', {
|
|
||||||
a: 'Community 1-Collection 1'
|
|
||||||
}));
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should show only the searched collection', () => {
|
|
||||||
comp.searchListCollection$ = observableOf(mockCollectionList);
|
|
||||||
fixture.detectChanges();
|
|
||||||
|
|
||||||
comp.searchField.setValue(searchedCollection);
|
|
||||||
fixture.detectChanges();
|
|
||||||
|
|
||||||
comp.searchListCollection$.pipe(
|
|
||||||
filter(() => !comp.disabled$.getValue())
|
|
||||||
).subscribe((list) => {
|
|
||||||
expect(list).toEqual([mockCollectionList[3]]);
|
|
||||||
});
|
|
||||||
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should emit collectionChange event when selecting a new collection', () => {
|
|
||||||
spyOn(comp.searchField, 'reset').and.callThrough();
|
|
||||||
spyOn(comp.collectionChange, 'emit').and.callThrough();
|
|
||||||
jsonPatchOpServiceStub.jsonPatchByResourceID.and.returnValue(observableOf(submissionRestResponse));
|
|
||||||
comp.ngOnInit();
|
|
||||||
comp.onSelect(mockCollectionList[1]);
|
|
||||||
fixture.detectChanges();
|
|
||||||
|
|
||||||
expect(comp.searchField.reset).toHaveBeenCalled();
|
|
||||||
expect(comp.collectionChange.emit).toHaveBeenCalledWith(submissionRestResponse[0] as any);
|
|
||||||
expect(submissionServiceStub.changeSubmissionCollection).toHaveBeenCalled();
|
|
||||||
expect(comp.selectedCollectionId).toBe(mockCollectionList[1].collection.id);
|
|
||||||
expect(comp.selectedCollectionName$).toBeObservable(cold('(a|)', {
|
|
||||||
a: mockCollectionList[1].collection.name
|
|
||||||
}));
|
|
||||||
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should reset searchField when dropdown menu has been closed', () => {
|
|
||||||
spyOn(comp.searchField, 'reset').and.callThrough();
|
|
||||||
comp.toggled(false);
|
|
||||||
|
|
||||||
expect(comp.searchField.reset).toHaveBeenCalled();
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('', () => {
|
describe('', () => {
|
||||||
let dropdowBtn: DebugElement;
|
let dropdowBtn: DebugElement;
|
||||||
let dropdownMenu: DebugElement;
|
let dropdownMenu: DebugElement;
|
||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
|
|
||||||
comp.searchListCollection$ = observableOf(mockCollectionList);
|
|
||||||
fixture.detectChanges();
|
fixture.detectChanges();
|
||||||
dropdowBtn = fixture.debugElement.query(By.css('#collectionControlsMenuButton'));
|
dropdowBtn = fixture.debugElement.query(By.css('#collectionControlsMenuButton'));
|
||||||
dropdownMenu = fixture.debugElement.query(By.css('#collectionControlsDropdownMenu'));
|
dropdownMenu = fixture.debugElement.query(By.css('#collectionControlsDropdownMenu'));
|
||||||
@@ -387,49 +219,46 @@ describe('SubmissionFormCollectionComponent Component', () => {
|
|||||||
fixture.whenStable().then(() => {
|
fixture.whenStable().then(() => {
|
||||||
expect(comp.onClose).toHaveBeenCalled();
|
expect(comp.onClose).toHaveBeenCalled();
|
||||||
expect(dropdownMenu.nativeElement.classList).toContain('show');
|
expect(dropdownMenu.nativeElement.classList).toContain('show');
|
||||||
expect(dropdownMenu.queryAll(By.css('.collection-item')).length).toBe(4);
|
|
||||||
});
|
});
|
||||||
}));
|
}));
|
||||||
|
|
||||||
it('should trigger onSelect method when select a new collection from dropdown menu', fakeAsync(() => {
|
it('the dropdown menu should be enable', () => {
|
||||||
|
const dropDown = fixture.debugElement.query(By.css('#collectionControlsDropdownMenu'));
|
||||||
spyOn(comp, 'onSelect');
|
expect(dropDown).toBeTruthy();
|
||||||
dropdowBtn.triggerEventHandler('click', null);
|
|
||||||
tick();
|
|
||||||
fixture.detectChanges();
|
|
||||||
|
|
||||||
const secondLink: DebugElement = dropdownMenu.query(By.css('.collection-item:nth-child(2)'));
|
|
||||||
secondLink.triggerEventHandler('click', null);
|
|
||||||
tick();
|
|
||||||
fixture.detectChanges();
|
|
||||||
|
|
||||||
fixture.whenStable().then(() => {
|
|
||||||
|
|
||||||
expect(comp.onSelect).toHaveBeenCalled();
|
|
||||||
});
|
});
|
||||||
}));
|
|
||||||
|
|
||||||
it('should update searchField on input type', fakeAsync(() => {
|
it('the dropdown menu should be disabled', () => {
|
||||||
|
comp.available$ = of(false);
|
||||||
dropdowBtn.triggerEventHandler('click', null);
|
|
||||||
tick();
|
|
||||||
fixture.detectChanges();
|
fixture.detectChanges();
|
||||||
|
const dropDown = fixture.debugElement.query(By.css('#collectionControlsDropdownMenu'));
|
||||||
fixture.whenStable().then(() => {
|
expect(dropDown).toBeFalsy();
|
||||||
const input = fixture.debugElement.query(By.css('input.form-control'));
|
|
||||||
const el = input.nativeElement;
|
|
||||||
|
|
||||||
expect(el.value).toBe('');
|
|
||||||
|
|
||||||
el.value = searchedCollection;
|
|
||||||
el.dispatchEvent(new Event('input'));
|
|
||||||
|
|
||||||
fixture.detectChanges();
|
|
||||||
|
|
||||||
expect(fixture.componentInstance.searchField.value).toEqual(searchedCollection);
|
|
||||||
});
|
});
|
||||||
}));
|
|
||||||
|
|
||||||
|
it('should be simulated when the drop-down menu is closed', () => {
|
||||||
|
spyOn(comp, 'onClose');
|
||||||
|
comp.onClose();
|
||||||
|
expect(comp.onClose).toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should be simulated when the drop-down menu is toggled', () => {
|
||||||
|
spyOn(comp, 'toggled');
|
||||||
|
comp.toggled(false);
|
||||||
|
expect(comp.toggled).toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should ', () => {
|
||||||
|
spyOn(comp.collectionChange, 'emit').and.callThrough();
|
||||||
|
jsonPatchOpServiceStub.jsonPatchByResourceID.and.returnValue(of(submissionRestResponse));
|
||||||
|
comp.ngOnInit();
|
||||||
|
comp.onSelect(mockCollectionList[1]);
|
||||||
|
fixture.detectChanges();
|
||||||
|
|
||||||
|
expect(submissionServiceStub.changeSubmissionCollection).toHaveBeenCalled();
|
||||||
|
expect(comp.selectedCollectionId).toBe(mockCollectionList[1].collection.id);
|
||||||
|
expect(comp.selectedCollectionName$).toBeObservable(cold('(a|)', {
|
||||||
|
a: mockCollectionList[1].collection.name
|
||||||
|
}));
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
});
|
});
|
||||||
|
@@ -7,52 +7,28 @@ import {
|
|||||||
OnChanges,
|
OnChanges,
|
||||||
OnInit,
|
OnInit,
|
||||||
Output,
|
Output,
|
||||||
SimpleChanges
|
SimpleChanges,
|
||||||
|
ViewChild
|
||||||
} from '@angular/core';
|
} from '@angular/core';
|
||||||
import { FormControl } from '@angular/forms';
|
|
||||||
|
|
||||||
import { BehaviorSubject, combineLatest, Observable, of as observableOf, Subscription } from 'rxjs';
|
import { BehaviorSubject, Observable, of as observableOf, Subscription } from 'rxjs';
|
||||||
import {
|
import {
|
||||||
debounceTime,
|
|
||||||
distinctUntilChanged,
|
|
||||||
filter,
|
|
||||||
find,
|
find,
|
||||||
flatMap,
|
map
|
||||||
map,
|
|
||||||
mergeMap,
|
|
||||||
reduce,
|
|
||||||
startWith
|
|
||||||
} from 'rxjs/operators';
|
} from 'rxjs/operators';
|
||||||
|
|
||||||
import { Collection } from '../../../core/shared/collection.model';
|
import { Collection } from '../../../core/shared/collection.model';
|
||||||
import { CommunityDataService } from '../../../core/data/community-data.service';
|
import { CommunityDataService } from '../../../core/data/community-data.service';
|
||||||
import { Community } from '../../../core/shared/community.model';
|
import { hasValue, isNotEmpty } from '../../../shared/empty.util';
|
||||||
import { hasValue, isEmpty, isNotEmpty } from '../../../shared/empty.util';
|
|
||||||
import { RemoteData } from '../../../core/data/remote-data';
|
import { RemoteData } from '../../../core/data/remote-data';
|
||||||
import { JsonPatchOperationPathCombiner } from '../../../core/json-patch/builder/json-patch-operation-path-combiner';
|
import { JsonPatchOperationPathCombiner } from '../../../core/json-patch/builder/json-patch-operation-path-combiner';
|
||||||
import { JsonPatchOperationsBuilder } from '../../../core/json-patch/builder/json-patch-operations-builder';
|
import { JsonPatchOperationsBuilder } from '../../../core/json-patch/builder/json-patch-operations-builder';
|
||||||
import { PaginatedList } from '../../../core/data/paginated-list';
|
|
||||||
import { SubmissionService } from '../../submission.service';
|
import { SubmissionService } from '../../submission.service';
|
||||||
import { SubmissionObject } from '../../../core/submission/models/submission-object.model';
|
import { SubmissionObject } from '../../../core/submission/models/submission-object.model';
|
||||||
import { SubmissionJsonPatchOperationsService } from '../../../core/submission/submission-json-patch-operations.service';
|
import { SubmissionJsonPatchOperationsService } from '../../../core/submission/submission-json-patch-operations.service';
|
||||||
import { CollectionDataService } from '../../../core/data/collection-data.service';
|
import { CollectionDataService } from '../../../core/data/collection-data.service';
|
||||||
import { FindListOptions } from '../../../core/data/request.models';
|
import { CollectionDropdownComponent } from 'src/app/shared/collection-dropdown/collection-dropdown.component';
|
||||||
|
import { SectionsService } from '../../sections/sections.service';
|
||||||
/**
|
|
||||||
* An interface to represent a collection entry
|
|
||||||
*/
|
|
||||||
interface CollectionListEntryItem {
|
|
||||||
id: string;
|
|
||||||
name: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* An interface to represent an entry in the collection list
|
|
||||||
*/
|
|
||||||
interface CollectionListEntry {
|
|
||||||
communities: CollectionListEntryItem[],
|
|
||||||
collection: CollectionListEntryItem
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* This component allows to show the current collection the submission belonging to and to change it.
|
* This component allows to show the current collection the submission belonging to and to change it.
|
||||||
@@ -88,30 +64,12 @@ export class SubmissionFormCollectionComponent implements OnChanges, OnInit {
|
|||||||
*/
|
*/
|
||||||
@Output() collectionChange: EventEmitter<SubmissionObject> = new EventEmitter<SubmissionObject>();
|
@Output() collectionChange: EventEmitter<SubmissionObject> = new EventEmitter<SubmissionObject>();
|
||||||
|
|
||||||
/**
|
|
||||||
* A boolean representing if this dropdown button is disabled
|
|
||||||
* @type {BehaviorSubject<boolean>}
|
|
||||||
*/
|
|
||||||
public disabled$ = new BehaviorSubject<boolean>(true);
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* A boolean representing if a collection change operation is processing
|
* A boolean representing if a collection change operation is processing
|
||||||
* @type {BehaviorSubject<boolean>}
|
* @type {BehaviorSubject<boolean>}
|
||||||
*/
|
*/
|
||||||
public processingChange$ = new BehaviorSubject<boolean>(false);
|
public processingChange$ = new BehaviorSubject<boolean>(false);
|
||||||
|
|
||||||
/**
|
|
||||||
* The search form control
|
|
||||||
* @type {FormControl}
|
|
||||||
*/
|
|
||||||
public searchField: FormControl = new FormControl();
|
|
||||||
|
|
||||||
/**
|
|
||||||
* The collection list obtained from a search
|
|
||||||
* @type {Observable<CollectionListEntry[]>}
|
|
||||||
*/
|
|
||||||
public searchListCollection$: Observable<CollectionListEntry[]>;
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The selected collection id
|
* The selected collection id
|
||||||
* @type {string}
|
* @type {string}
|
||||||
@@ -130,24 +88,23 @@ export class SubmissionFormCollectionComponent implements OnChanges, OnInit {
|
|||||||
*/
|
*/
|
||||||
protected pathCombiner: JsonPatchOperationPathCombiner;
|
protected pathCombiner: JsonPatchOperationPathCombiner;
|
||||||
|
|
||||||
/**
|
|
||||||
* 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;
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Array to track all subscriptions and unsubscribe them onDestroy
|
* Array to track all subscriptions and unsubscribe them onDestroy
|
||||||
* @type {Array}
|
* @type {Array}
|
||||||
*/
|
*/
|
||||||
private subs: Subscription[] = [];
|
private subs: Subscription[] = [];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The html child that contains the collections list
|
||||||
|
*/
|
||||||
|
@ViewChild(CollectionDropdownComponent, {static: false}) collectionDropdown: CollectionDropdownComponent;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A boolean representing if the collection section is available
|
||||||
|
* @type {BehaviorSubject<boolean>}
|
||||||
|
*/
|
||||||
|
available$: Observable<boolean>;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Initialize instance variables
|
* Initialize instance variables
|
||||||
*
|
*
|
||||||
@@ -159,37 +116,11 @@ export class SubmissionFormCollectionComponent implements OnChanges, OnInit {
|
|||||||
* @param {SubmissionService} submissionService
|
* @param {SubmissionService} submissionService
|
||||||
*/
|
*/
|
||||||
constructor(protected cdr: ChangeDetectorRef,
|
constructor(protected cdr: ChangeDetectorRef,
|
||||||
private communityDataService: CommunityDataService,
|
|
||||||
private collectionDataService: CollectionDataService,
|
private collectionDataService: CollectionDataService,
|
||||||
private operationsBuilder: JsonPatchOperationsBuilder,
|
private operationsBuilder: JsonPatchOperationsBuilder,
|
||||||
private operationsService: SubmissionJsonPatchOperationsService,
|
private operationsService: SubmissionJsonPatchOperationsService,
|
||||||
private submissionService: SubmissionService) {
|
private submissionService: SubmissionService,
|
||||||
}
|
private sectionsService: SectionsService) {
|
||||||
|
|
||||||
/**
|
|
||||||
* 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();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Check if dropdown scrollbar is at the top or bottom of the dropdown list
|
|
||||||
*
|
|
||||||
* @param event
|
|
||||||
*/
|
|
||||||
onScroll(event) {
|
|
||||||
this.scrollableBottom = (event.target.scrollTop + event.target.clientHeight === event.target.scrollHeight);
|
|
||||||
this.scrollableTop = (event.target.scrollTop === 0);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -204,51 +135,6 @@ export class SubmissionFormCollectionComponent implements OnChanges, OnInit {
|
|||||||
find((collectionRD: RemoteData<Collection>) => isNotEmpty(collectionRD.payload)),
|
find((collectionRD: RemoteData<Collection>) => isNotEmpty(collectionRD.payload)),
|
||||||
map((collectionRD: RemoteData<Collection>) => collectionRD.payload.name)
|
map((collectionRD: RemoteData<Collection>) => collectionRD.payload.name)
|
||||||
);
|
);
|
||||||
|
|
||||||
const findOptions: FindListOptions = {
|
|
||||||
elementsPerPage: 1000
|
|
||||||
};
|
|
||||||
|
|
||||||
// Retrieve collection list only when is the first change
|
|
||||||
if (changes.currentCollectionId.isFirstChange()) {
|
|
||||||
// @TODO replace with search/top browse endpoint
|
|
||||||
// @TODO implement community/subcommunity hierarchy
|
|
||||||
const communities$ = this.communityDataService.findAll(findOptions).pipe(
|
|
||||||
find((communities: RemoteData<PaginatedList<Community>>) => isNotEmpty(communities.payload)),
|
|
||||||
mergeMap((communities: RemoteData<PaginatedList<Community>>) => communities.payload.page));
|
|
||||||
|
|
||||||
const listCollection$ = communities$.pipe(
|
|
||||||
flatMap((communityData: Community) => {
|
|
||||||
return this.collectionDataService.getAuthorizedCollectionByCommunity(communityData.uuid, findOptions).pipe(
|
|
||||||
find((collections: RemoteData<PaginatedList<Collection>>) => !collections.isResponsePending && collections.hasSucceeded),
|
|
||||||
mergeMap((collections: RemoteData<PaginatedList<Collection>>) => collections.payload.page),
|
|
||||||
filter((collectionData: Collection) => isNotEmpty(collectionData)),
|
|
||||||
map((collectionData: Collection) => ({
|
|
||||||
communities: [{ id: communityData.id, name: communityData.name }],
|
|
||||||
collection: { id: collectionData.id, name: collectionData.name }
|
|
||||||
}))
|
|
||||||
);
|
|
||||||
}),
|
|
||||||
reduce((acc: any, value: any) => [...acc, ...value], []),
|
|
||||||
startWith([])
|
|
||||||
);
|
|
||||||
|
|
||||||
const searchTerm$ = this.searchField.valueChanges.pipe(
|
|
||||||
debounceTime(200),
|
|
||||||
distinctUntilChanged(),
|
|
||||||
startWith('')
|
|
||||||
);
|
|
||||||
|
|
||||||
this.searchListCollection$ = combineLatest(searchTerm$, listCollection$).pipe(
|
|
||||||
map(([searchTerm, listCollection]) => {
|
|
||||||
this.disabled$.next(isEmpty(listCollection));
|
|
||||||
if (isEmpty(searchTerm)) {
|
|
||||||
return listCollection;
|
|
||||||
} else {
|
|
||||||
return listCollection.filter((v) => v.collection.name.toLowerCase().indexOf(searchTerm.toLowerCase()) > -1).slice(0, 5);
|
|
||||||
}
|
|
||||||
}));
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -257,6 +143,7 @@ export class SubmissionFormCollectionComponent implements OnChanges, OnInit {
|
|||||||
*/
|
*/
|
||||||
ngOnInit() {
|
ngOnInit() {
|
||||||
this.pathCombiner = new JsonPatchOperationPathCombiner('sections', 'collection');
|
this.pathCombiner = new JsonPatchOperationPathCombiner('sections', 'collection');
|
||||||
|
this.available$ = this.sectionsService.isSectionAvailable(this.submissionId, 'collection');
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -273,7 +160,6 @@ export class SubmissionFormCollectionComponent implements OnChanges, OnInit {
|
|||||||
* the selected [CollectionListEntryItem]
|
* the selected [CollectionListEntryItem]
|
||||||
*/
|
*/
|
||||||
onSelect(event) {
|
onSelect(event) {
|
||||||
this.searchField.reset();
|
|
||||||
this.processingChange$.next(true);
|
this.processingChange$.next(true);
|
||||||
this.operationsBuilder.replace(this.pathCombiner.getPath(), event.collection.id, true);
|
this.operationsBuilder.replace(this.pathCombiner.getPath(), event.collection.id, true);
|
||||||
this.subs.push(this.operationsService.jsonPatchByResourceID(
|
this.subs.push(this.operationsService.jsonPatchByResourceID(
|
||||||
@@ -296,7 +182,7 @@ export class SubmissionFormCollectionComponent implements OnChanges, OnInit {
|
|||||||
* Reset search form control on dropdown menu close
|
* Reset search form control on dropdown menu close
|
||||||
*/
|
*/
|
||||||
onClose() {
|
onClose() {
|
||||||
this.searchField.reset();
|
this.collectionDropdown.reset();
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -307,7 +193,7 @@ export class SubmissionFormCollectionComponent implements OnChanges, OnInit {
|
|||||||
*/
|
*/
|
||||||
toggled(isOpen: boolean) {
|
toggled(isOpen: boolean) {
|
||||||
if (!isOpen) {
|
if (!isOpen) {
|
||||||
this.searchField.reset();
|
this.collectionDropdown.reset();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
Reference in New Issue
Block a user