mirror of
https://github.com/DSpace/dspace-angular.git
synced 2025-10-07 01:54:15 +00:00
Merge branch 'master' into w2p-71504_Authorization-for-downloads-of-restricted-bitstreams
Conflicts: src/app/shared/shared.module.ts
This commit is contained in:
@@ -1,4 +1,4 @@
|
||||
::ng-deep {
|
||||
:host ::ng-deep {
|
||||
.fa-chevron-right {
|
||||
padding-left: $spacer/2;
|
||||
font-size: 0.5rem;
|
||||
|
@@ -32,6 +32,7 @@ import { ItemVersionHistoryComponent } from './item-version-history/item-version
|
||||
import { ItemAuthorizationsComponent } from './item-authorizations/item-authorizations.component';
|
||||
import { ResourcePolicyEditComponent } from '../../shared/resource-policies/edit/resource-policy-edit.component';
|
||||
import { ResourcePolicyCreateComponent } from '../../shared/resource-policies/create/resource-policy-create.component';
|
||||
import { ObjectValuesPipe } from '../../shared/utils/object-values-pipe';
|
||||
|
||||
/**
|
||||
* Module that contains all components related to the Edit Item page administrator functionality
|
||||
@@ -75,7 +76,8 @@ import { ResourcePolicyCreateComponent } from '../../shared/resource-policies/cr
|
||||
ResourcePolicyCreateComponent,
|
||||
],
|
||||
providers: [
|
||||
BundleDataService
|
||||
BundleDataService,
|
||||
ObjectValuesPipe
|
||||
]
|
||||
})
|
||||
export class EditItemPageModule {
|
||||
|
@@ -36,7 +36,8 @@
|
||||
<ds-item-edit-bitstream-bundle *ngFor="let bundle of bundles"
|
||||
[bundle]="bundle"
|
||||
[item]="item"
|
||||
[columnSizes]="columnSizes">
|
||||
[columnSizes]="columnSizes"
|
||||
(dropObject)="dropBitstream(bundle, $event)">
|
||||
</ds-item-edit-bitstream-bundle>
|
||||
</div>
|
||||
<div *ngIf="bundles?.length === 0"
|
||||
|
@@ -188,8 +188,21 @@ describe('ItemBitstreamsComponent', () => {
|
||||
it('should not call deleteAndReturnResponse on the bitstreamService for the unmarked field', () => {
|
||||
expect(bitstreamService.deleteAndReturnResponse).not.toHaveBeenCalledWith(bitstream1.id);
|
||||
});
|
||||
});
|
||||
|
||||
it('should send out a patch for the move operations', () => {
|
||||
describe('when dropBitstream is called', () => {
|
||||
beforeEach((done) => {
|
||||
comp.dropBitstream(bundle, {
|
||||
fromIndex: 0,
|
||||
toIndex: 50,
|
||||
// tslint:disable-next-line:no-empty
|
||||
finish: () => {
|
||||
done();
|
||||
}
|
||||
})
|
||||
});
|
||||
|
||||
it('should send out a patch for the move operation', () => {
|
||||
expect(bundleService.patch).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
@@ -1,6 +1,6 @@
|
||||
import { ChangeDetectorRef, Component, Inject, OnDestroy } from '@angular/core';
|
||||
import { ChangeDetectorRef, Component, NgZone, OnDestroy } from '@angular/core';
|
||||
import { AbstractItemUpdateComponent } from '../abstract-item-update/abstract-item-update.component';
|
||||
import { filter, map, switchMap, take, tap } from 'rxjs/operators';
|
||||
import { filter, map, switchMap, take } from 'rxjs/operators';
|
||||
import { Observable } from 'rxjs/internal/Observable';
|
||||
import { Subscription } from 'rxjs/internal/Subscription';
|
||||
import { ItemDataService } from '../../../core/data/item-data.service';
|
||||
@@ -9,8 +9,8 @@ import { ActivatedRoute, Router } from '@angular/router';
|
||||
import { NotificationsService } from '../../../shared/notifications/notifications.service';
|
||||
import { TranslateService } from '@ngx-translate/core';
|
||||
import { BitstreamDataService } from '../../../core/data/bitstream-data.service';
|
||||
import { hasValue, isNotEmpty, isNotEmptyOperator } from '../../../shared/empty.util';
|
||||
import { zip as observableZip, combineLatest as observableCombineLatest, of as observableOf } from 'rxjs';
|
||||
import { hasValue, isNotEmpty } from '../../../shared/empty.util';
|
||||
import { zip as observableZip, of as observableOf } from 'rxjs';
|
||||
import { ErrorResponse, RestResponse } from '../../../core/cache/response.models';
|
||||
import { ObjectCacheService } from '../../../core/cache/object-cache.service';
|
||||
import { RequestService } from '../../../core/data/request.service';
|
||||
@@ -22,8 +22,6 @@ import { Bundle } from '../../../core/shared/bundle.model';
|
||||
import { FieldUpdate, FieldUpdates } from '../../../core/data/object-updates/object-updates.reducer';
|
||||
import { Bitstream } from '../../../core/shared/bitstream.model';
|
||||
import { FieldChangeType } from '../../../core/data/object-updates/object-updates.actions';
|
||||
import { Operation } from 'fast-json-patch';
|
||||
import { MoveOperation } from 'fast-json-patch/lib/core';
|
||||
import { BundleDataService } from '../../../core/data/bundle-data.service';
|
||||
import { PaginatedSearchOptions } from '../../../shared/search/paginated-search-options.model';
|
||||
import { ResponsiveColumnSizes } from '../../../shared/responsive-table-sizes/responsive-column-sizes';
|
||||
@@ -90,7 +88,8 @@ export class ItemBitstreamsComponent extends AbstractItemUpdateComponent impleme
|
||||
public objectCache: ObjectCacheService,
|
||||
public requestService: RequestService,
|
||||
public cdRef: ChangeDetectorRef,
|
||||
public bundleService: BundleDataService
|
||||
public bundleService: BundleDataService,
|
||||
public zone: NgZone
|
||||
) {
|
||||
super(itemService, objectUpdatesService, router, notificationsService, translateService, route);
|
||||
}
|
||||
@@ -143,7 +142,6 @@ export class ItemBitstreamsComponent extends AbstractItemUpdateComponent impleme
|
||||
|
||||
/**
|
||||
* Submit the current changes
|
||||
* Bitstreams that were dragged around send out a patch request with move operations to the rest API
|
||||
* Bitstreams marked as deleted send out a delete request to the rest API
|
||||
* Display notifications and reset the current item/updates
|
||||
*/
|
||||
@@ -151,32 +149,6 @@ export class ItemBitstreamsComponent extends AbstractItemUpdateComponent impleme
|
||||
this.submitting = true;
|
||||
const bundlesOnce$ = this.bundles$.pipe(take(1));
|
||||
|
||||
// Fetch all move operations for each bundle
|
||||
const moveOperations$ = bundlesOnce$.pipe(
|
||||
switchMap((bundles: Bundle[]) => observableZip(...bundles.map((bundle: Bundle) =>
|
||||
this.objectUpdatesService.getMoveOperations(bundle.self).pipe(
|
||||
take(1),
|
||||
map((operations: MoveOperation[]) => [...operations.map((operation: MoveOperation) => Object.assign(operation, {
|
||||
from: `/_links/bitstreams${operation.from}/href`,
|
||||
path: `/_links/bitstreams${operation.path}/href`
|
||||
}))])
|
||||
)
|
||||
)))
|
||||
);
|
||||
|
||||
// Send out an immediate patch request for each bundle
|
||||
const patchResponses$ = observableCombineLatest(bundlesOnce$, moveOperations$).pipe(
|
||||
switchMap(([bundles, moveOperationList]: [Bundle[], Operation[][]]) =>
|
||||
observableZip(...bundles.map((bundle: Bundle, index: number) => {
|
||||
if (isNotEmpty(moveOperationList[index])) {
|
||||
return this.bundleService.patch(bundle, moveOperationList[index]);
|
||||
} else {
|
||||
return observableOf(undefined);
|
||||
}
|
||||
}))
|
||||
)
|
||||
);
|
||||
|
||||
// Fetch all removed bitstreams from the object update service
|
||||
const removedBitstreams$ = bundlesOnce$.pipe(
|
||||
switchMap((bundles: Bundle[]) => observableZip(
|
||||
@@ -201,19 +173,42 @@ export class ItemBitstreamsComponent extends AbstractItemUpdateComponent impleme
|
||||
);
|
||||
|
||||
// Perform the setup actions from above in order and display notifications
|
||||
patchResponses$.pipe(
|
||||
switchMap((responses: RestResponse[]) => {
|
||||
this.displayNotifications('item.edit.bitstreams.notifications.move', responses);
|
||||
return removedResponses$
|
||||
}),
|
||||
take(1)
|
||||
).subscribe((responses: RestResponse[]) => {
|
||||
removedResponses$.pipe(take(1)).subscribe((responses: RestResponse[]) => {
|
||||
this.displayNotifications('item.edit.bitstreams.notifications.remove', responses);
|
||||
this.reset();
|
||||
this.submitting = false;
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* A bitstream was dropped in a new location. Send out a Move Patch request to the REST API, display notifications,
|
||||
* refresh the bundle's cache (so the lists can properly reload) and call the event's callback function (which will
|
||||
* navigate the user to the correct page)
|
||||
* @param bundle The bundle to send patch requests to
|
||||
* @param event The event containing the index the bitstream came from and was dropped to
|
||||
*/
|
||||
dropBitstream(bundle: Bundle, event: any) {
|
||||
this.zone.runOutsideAngular(() => {
|
||||
if (hasValue(event) && hasValue(event.fromIndex) && hasValue(event.toIndex) && hasValue(event.finish)) {
|
||||
const moveOperation = Object.assign({
|
||||
op: 'move',
|
||||
from: `/_links/bitstreams/${event.fromIndex}/href`,
|
||||
path: `/_links/bitstreams/${event.toIndex}/href`
|
||||
});
|
||||
this.bundleService.patch(bundle, [moveOperation]).pipe(take(1)).subscribe((response: RestResponse) => {
|
||||
this.zone.run(() => {
|
||||
this.displayNotifications('item.edit.bitstreams.notifications.move', [response]);
|
||||
// Remove all cached requests from this bundle and call the event's callback when the requests are cleared
|
||||
this.requestService.removeByHrefSubstring(bundle.self).pipe(
|
||||
filter((isCached) => isCached),
|
||||
take(1)
|
||||
).subscribe(() => event.finish());
|
||||
});
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Display notifications
|
||||
* - Error notification for each failed response with their message
|
||||
|
@@ -17,5 +17,5 @@
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<ds-paginated-drag-and-drop-bitstream-list [bundle]="bundle" [columnSizes]="columnSizes"></ds-paginated-drag-and-drop-bitstream-list>
|
||||
<ds-paginated-drag-and-drop-bitstream-list [bundle]="bundle" [columnSizes]="columnSizes" (dropObject)="dropObject.emit($event)"></ds-paginated-drag-and-drop-bitstream-list>
|
||||
</ng-template>
|
||||
|
@@ -1,4 +1,4 @@
|
||||
import { Component, Input, OnInit, ViewChild, ViewContainerRef } from '@angular/core';
|
||||
import { Component, EventEmitter, Input, OnInit, Output, ViewChild, ViewContainerRef } from '@angular/core';
|
||||
import { Bundle } from '../../../../core/shared/bundle.model';
|
||||
import { Item } from '../../../../core/shared/item.model';
|
||||
import { ResponsiveColumnSizes } from '../../../../shared/responsive-table-sizes/responsive-column-sizes';
|
||||
@@ -36,6 +36,13 @@ export class ItemEditBitstreamBundleComponent implements OnInit {
|
||||
*/
|
||||
@Input() columnSizes: ResponsiveTableSizes;
|
||||
|
||||
/**
|
||||
* Send an event when the user drops an object on the pagination
|
||||
* The event contains details about the index the object came from and is dropped to (across the entirety of the list,
|
||||
* not just within a single page)
|
||||
*/
|
||||
@Output() dropObject: EventEmitter<any> = new EventEmitter<any>();
|
||||
|
||||
/**
|
||||
* The bootstrap sizes used for the Bundle Name column
|
||||
* This column stretches over the first 3 columns and thus is a combination of their sizes processed in ngOnInit
|
||||
|
@@ -7,24 +7,29 @@
|
||||
[collectionSize]="(objectsRD$ | async)?.payload?.totalElements"
|
||||
[disableRouteParameterUpdate]="true"
|
||||
(pageChange)="switchPage($event)">
|
||||
<div [id]="bundle.id" class="bundle-bitstreams-list"
|
||||
[ngClass]="{'mb-3': (objectsRD$ | async)?.payload?.totalElements > pageSize}"
|
||||
*ngVar="((updates$ | async) | dsObjectValues) as updateValues" cdkDropList (cdkDropListDropped)="drop($event)">
|
||||
<div class="row bitstream-row" *ngFor="let updateValue of updateValues" cdkDrag
|
||||
[id]="updateValue.field.uuid"
|
||||
[ngClass]="{
|
||||
'table-warning': updateValue.changeType === 0,
|
||||
'table-danger': updateValue.changeType === 2,
|
||||
'table-success': updateValue.changeType === 1,
|
||||
'bg-white': updateValue.changeType === undefined
|
||||
<ng-container *ngIf="!(loading$ | async)">
|
||||
<div [id]="bundle.id" class="bundle-bitstreams-list"
|
||||
[ngClass]="{'mb-3': (objectsRD$ | async)?.payload?.totalElements > pageSize}"
|
||||
*ngVar="(updates$ | async) as updates" cdkDropList (cdkDropListDropped)="drop($event)">
|
||||
<ng-container *ngIf="updates">
|
||||
<div class="row bitstream-row" *ngFor="let uuid of customOrder" cdkDrag
|
||||
[id]="uuid"
|
||||
[ngClass]="{
|
||||
'table-warning': updates[uuid].changeType === 0,
|
||||
'table-danger': updates[uuid].changeType === 2,
|
||||
'table-success': updates[uuid].changeType === 1,
|
||||
'bg-white': updates[uuid].changeType === undefined
|
||||
}">
|
||||
<ds-item-edit-bitstream [fieldUpdate]="updateValue"
|
||||
[bundleUrl]="bundle.self"
|
||||
[columnSizes]="columnSizes">
|
||||
<div class="d-flex align-items-center" slot="drag-handle" cdkDragHandle>
|
||||
<ds-item-edit-bitstream-drag-handle></ds-item-edit-bitstream-drag-handle>
|
||||
<ds-item-edit-bitstream [fieldUpdate]="updates[uuid]"
|
||||
[bundleUrl]="bundle.self"
|
||||
[columnSizes]="columnSizes">
|
||||
<div class="d-flex align-items-center" slot="drag-handle" cdkDragHandle>
|
||||
<ds-item-edit-bitstream-drag-handle></ds-item-edit-bitstream-drag-handle>
|
||||
</div>
|
||||
</ds-item-edit-bitstream>
|
||||
</div>
|
||||
</ds-item-edit-bitstream>
|
||||
</ng-container>
|
||||
</div>
|
||||
</div>
|
||||
</ng-container>
|
||||
<ds-loading *ngIf="(loading$ | async)" [message]="'loading.bitstreams' | translate"></ds-loading>
|
||||
</ds-pagination>
|
||||
|
@@ -16,12 +16,15 @@ import { ResponsiveTableSizes } from '../../../../../shared/responsive-table-siz
|
||||
import { ResponsiveColumnSizes } from '../../../../../shared/responsive-table-sizes/responsive-column-sizes';
|
||||
import { createSuccessfulRemoteDataObject$ } from '../../../../../shared/remote-data.utils';
|
||||
import { createPaginatedList } from '../../../../../shared/testing/utils.test';
|
||||
import { RequestService } from '../../../../../core/data/request.service';
|
||||
|
||||
describe('PaginatedDragAndDropBitstreamListComponent', () => {
|
||||
let comp: PaginatedDragAndDropBitstreamListComponent;
|
||||
let fixture: ComponentFixture<PaginatedDragAndDropBitstreamListComponent>;
|
||||
let objectUpdatesService: ObjectUpdatesService;
|
||||
let bundleService: BundleDataService;
|
||||
let objectValuesPipe: ObjectValuesPipe;
|
||||
let requestService: RequestService;
|
||||
|
||||
const columnSizes = new ResponsiveTableSizes([
|
||||
new ResponsiveColumnSizes(2, 2, 3, 4, 4),
|
||||
@@ -97,15 +100,24 @@ describe('PaginatedDragAndDropBitstreamListComponent', () => {
|
||||
);
|
||||
|
||||
bundleService = jasmine.createSpyObj('bundleService', {
|
||||
getBitstreams: createSuccessfulRemoteDataObject$(createPaginatedList([bitstream1, bitstream2]))
|
||||
getBitstreams: createSuccessfulRemoteDataObject$(createPaginatedList([bitstream1, bitstream2])),
|
||||
getBitstreamsEndpoint: observableOf('')
|
||||
});
|
||||
|
||||
objectValuesPipe = new ObjectValuesPipe();
|
||||
|
||||
requestService = jasmine.createSpyObj('requestService', {
|
||||
hasByHrefObservable: observableOf(true)
|
||||
});
|
||||
|
||||
TestBed.configureTestingModule({
|
||||
imports: [TranslateModule.forRoot()],
|
||||
declarations: [PaginatedDragAndDropBitstreamListComponent, VarDirective, ObjectValuesPipe],
|
||||
declarations: [PaginatedDragAndDropBitstreamListComponent, VarDirective],
|
||||
providers: [
|
||||
{ provide: ObjectUpdatesService, useValue: objectUpdatesService },
|
||||
{ provide: BundleDataService, useValue: bundleService }
|
||||
{ provide: BundleDataService, useValue: bundleService },
|
||||
{ provide: ObjectValuesPipe, useValue: objectValuesPipe },
|
||||
{ provide: RequestService, useValue: requestService }
|
||||
], schemas: [
|
||||
NO_ERRORS_SCHEMA
|
||||
]
|
||||
|
@@ -8,6 +8,8 @@ import { switchMap } from 'rxjs/operators';
|
||||
import { PaginatedSearchOptions } from '../../../../../shared/search/paginated-search-options.model';
|
||||
import { ResponsiveTableSizes } from '../../../../../shared/responsive-table-sizes/responsive-table-sizes';
|
||||
import { followLink } from '../../../../../shared/utils/follow-link-config.model';
|
||||
import { ObjectValuesPipe } from '../../../../../shared/utils/object-values-pipe';
|
||||
import { RequestService } from '../../../../../core/data/request.service';
|
||||
|
||||
@Component({
|
||||
selector: 'ds-paginated-drag-and-drop-bitstream-list',
|
||||
@@ -33,8 +35,10 @@ export class PaginatedDragAndDropBitstreamListComponent extends AbstractPaginate
|
||||
|
||||
constructor(protected objectUpdatesService: ObjectUpdatesService,
|
||||
protected elRef: ElementRef,
|
||||
protected bundleService: BundleDataService) {
|
||||
super(objectUpdatesService, elRef);
|
||||
protected objectValuesPipe: ObjectValuesPipe,
|
||||
protected bundleService: BundleDataService,
|
||||
protected requestService: RequestService) {
|
||||
super(objectUpdatesService, elRef, objectValuesPipe);
|
||||
}
|
||||
|
||||
ngOnInit() {
|
||||
@@ -46,11 +50,17 @@ export class PaginatedDragAndDropBitstreamListComponent extends AbstractPaginate
|
||||
*/
|
||||
initializeObjectsRD(): void {
|
||||
this.objectsRD$ = this.currentPage$.pipe(
|
||||
switchMap((page: number) => this.bundleService.getBitstreams(
|
||||
this.bundle.id,
|
||||
new PaginatedSearchOptions({pagination: Object.assign({}, this.options, { currentPage: page })}),
|
||||
followLink('format')
|
||||
))
|
||||
switchMap((page: number) => {
|
||||
const paginatedOptions = new PaginatedSearchOptions({pagination: Object.assign({}, this.options, { currentPage: page })});
|
||||
return this.bundleService.getBitstreamsEndpoint(this.bundle.id, paginatedOptions).pipe(
|
||||
switchMap((href) => this.requestService.hasByHrefObservable(href)),
|
||||
switchMap(() => this.bundleService.getBitstreams(
|
||||
this.bundle.id,
|
||||
paginatedOptions,
|
||||
followLink('format')
|
||||
))
|
||||
);
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
|
@@ -7,9 +7,9 @@
|
||||
|
||||
</div>
|
||||
<div class="add">
|
||||
<a class="btn btn-lg btn-primary mt-1 ml-2" [routerLink]="['/submit']" role="button">
|
||||
<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}}
|
||||
</a>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
@@ -1,5 +1,5 @@
|
||||
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 { Store } from '@ngrx/store';
|
||||
@@ -21,6 +21,8 @@ import { NotificationsServiceStub } from '../../shared/testing/notifications-ser
|
||||
import { SharedModule } from '../../shared/shared.module';
|
||||
import { getMockScrollToService } from '../../shared/mocks/scroll-to-service.mock';
|
||||
import { UploaderService } from '../../shared/uploader/uploader.service';
|
||||
import { By } from '@angular/platform-browser';
|
||||
import { NgbModal } from '@ng-bootstrap/ng-bootstrap';
|
||||
|
||||
describe('MyDSpaceNewSubmissionComponent test', () => {
|
||||
|
||||
@@ -54,6 +56,11 @@ describe('MyDSpaceNewSubmissionComponent test', () => {
|
||||
{ provide: ScrollToService, useValue: getMockScrollToService() },
|
||||
{ provide: Store, useValue: store },
|
||||
{ provide: TranslateService, useValue: translateService },
|
||||
{
|
||||
provide: NgbModal, useValue: {
|
||||
open: () => {/*comment*/}
|
||||
}
|
||||
},
|
||||
ChangeDetectorRef,
|
||||
MyDSpaceNewSubmissionComponent,
|
||||
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
|
||||
|
@@ -15,6 +15,9 @@ 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 { 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
|
||||
@@ -55,7 +58,9 @@ export class MyDSpaceNewSubmissionComponent implements OnDestroy, OnInit {
|
||||
private halService: HALEndpointService,
|
||||
private notificationsService: NotificationsService,
|
||||
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'));
|
||||
}
|
||||
|
||||
/**
|
||||
* 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
|
||||
*/
|
||||
|
@@ -1,3 +1,5 @@
|
||||
import { NgZone } from '@angular/core';
|
||||
import { FindListOptions } from '../core/data/request.models';
|
||||
import { CommunityListService, FlatNode } from './community-list-service';
|
||||
import { CollectionViewer, DataSource } from '@angular/cdk/typings/collections';
|
||||
import { BehaviorSubject, Observable, } from 'rxjs';
|
||||
@@ -14,21 +16,23 @@ export class CommunityListDatasource implements DataSource<FlatNode> {
|
||||
private communityList$ = new BehaviorSubject<FlatNode[]>([]);
|
||||
public loading$ = new BehaviorSubject<boolean>(false);
|
||||
|
||||
constructor(private communityListService: CommunityListService) {
|
||||
constructor(private communityListService: CommunityListService,
|
||||
private zone: NgZone) {
|
||||
}
|
||||
|
||||
connect(collectionViewer: CollectionViewer): Observable<FlatNode[]> {
|
||||
return this.communityList$.asObservable();
|
||||
}
|
||||
|
||||
loadCommunities(expandedNodes: FlatNode[]) {
|
||||
loadCommunities(findOptions: FindListOptions, expandedNodes: FlatNode[]) {
|
||||
this.loading$.next(true);
|
||||
|
||||
this.communityListService.loadCommunities(expandedNodes).pipe(
|
||||
take(1),
|
||||
finalize(() => this.loading$.next(false)),
|
||||
).subscribe((flatNodes: FlatNode[]) => {
|
||||
this.communityList$.next(flatNodes);
|
||||
this.zone.runOutsideAngular(() => {
|
||||
this.communityListService.loadCommunities(findOptions, expandedNodes).pipe(
|
||||
take(1),
|
||||
finalize(() => this.zone.run(() => this.loading$.next(false))),
|
||||
).subscribe((flatNodes: FlatNode[]) => {
|
||||
this.zone.run(() => this.communityList$.next(flatNodes));
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
|
@@ -1,21 +1,19 @@
|
||||
import { of as observableOf } from 'rxjs';
|
||||
import { TestBed, inject, async } from '@angular/core/testing';
|
||||
import { inject, TestBed } from '@angular/core/testing';
|
||||
import { Store } from '@ngrx/store';
|
||||
import { of as observableOf } from 'rxjs';
|
||||
import { take } from 'rxjs/operators';
|
||||
import { AppState } from '../app.reducer';
|
||||
import { SortDirection, SortOptions } from '../core/cache/models/sort-options.model';
|
||||
import { PaginatedList } from '../core/data/paginated-list';
|
||||
import { createFailedRemoteDataObject$, createSuccessfulRemoteDataObject$ } from '../shared/remote-data.utils';
|
||||
import { StoreMock } from '../shared/testing/store.mock';
|
||||
import { CommunityListService, FlatNode, toFlatNode } from './community-list-service';
|
||||
import { CollectionDataService } from '../core/data/collection-data.service';
|
||||
import { PaginatedList } from '../core/data/paginated-list';
|
||||
import { PageInfo } from '../core/shared/page-info.model';
|
||||
import { CommunityDataService } from '../core/data/community-data.service';
|
||||
import {
|
||||
createFailedRemoteDataObject$,
|
||||
createSuccessfulRemoteDataObject$
|
||||
} from '../shared/remote-data.utils';
|
||||
import { Community } from '../core/shared/community.model';
|
||||
import { Collection } from '../core/shared/collection.model';
|
||||
import { take } from 'rxjs/operators';
|
||||
import { FindListOptions } from '../core/data/request.models';
|
||||
import { PageInfo } from '../core/shared/page-info.model';
|
||||
|
||||
describe('CommunityListService', () => {
|
||||
let store: StoreMock<AppState>;
|
||||
@@ -210,13 +208,18 @@ describe('CommunityListService', () => {
|
||||
let flatNodeList;
|
||||
describe('None expanded: should return list containing only flatnodes of the test top communities page 1 and 2', () => {
|
||||
let findTopSpy;
|
||||
beforeEach(() => {
|
||||
beforeEach((done) => {
|
||||
findTopSpy = spyOn(communityDataServiceStub, 'findTop').and.callThrough();
|
||||
service.getNextPageTopCommunities();
|
||||
|
||||
const sub = service.loadCommunities(null)
|
||||
.subscribe((value) => flatNodeList = value);
|
||||
sub.unsubscribe();
|
||||
service.loadCommunities({
|
||||
currentPage: 2,
|
||||
sort: new SortOptions('dc.title', SortDirection.ASC)
|
||||
}, null)
|
||||
.pipe(take(1))
|
||||
.subscribe((value) => {
|
||||
flatNodeList = value;
|
||||
done();
|
||||
});
|
||||
});
|
||||
it('flatnode list should contain just flatnodes of top community list page 1 and 2', () => {
|
||||
expect(findTopSpy).toHaveBeenCalled();
|
||||
@@ -236,10 +239,16 @@ describe('CommunityListService', () => {
|
||||
describe('should transform all communities in a list of flatnodes with possible subcoms and collections as subflatnodes if they\'re expanded', () => {
|
||||
let flatNodeList;
|
||||
describe('None expanded: should return list containing only flatnodes of the test top communities', () => {
|
||||
beforeEach(() => {
|
||||
const sub = service.loadCommunities(null)
|
||||
.pipe(take(1)).subscribe((value) => flatNodeList = value);
|
||||
sub.unsubscribe();
|
||||
beforeEach((done) => {
|
||||
service.loadCommunities({
|
||||
currentPage: 1,
|
||||
sort: new SortOptions('dc.title', SortDirection.ASC)
|
||||
}, null)
|
||||
.pipe(take(1))
|
||||
.subscribe((value) => {
|
||||
flatNodeList = value;
|
||||
done();
|
||||
});
|
||||
});
|
||||
it('length of flatnode list should be as big as top community list', () => {
|
||||
expect(flatNodeList.length).toEqual(mockListOfTopCommunitiesPage1.length);
|
||||
@@ -256,7 +265,7 @@ describe('CommunityListService', () => {
|
||||
});
|
||||
});
|
||||
describe('All top expanded, all page 1: should return list containing flatnodes of the communities in the test list and all its possible page-limited children (subcommunities and collections)', () => {
|
||||
beforeEach(() => {
|
||||
beforeEach((done) => {
|
||||
const expandedNodes = [];
|
||||
mockListOfTopCommunitiesPage1.map((community: Community) => {
|
||||
const communityFlatNode = toFlatNode(community, observableOf(true), 0, true, null);
|
||||
@@ -264,9 +273,15 @@ describe('CommunityListService', () => {
|
||||
communityFlatNode.currentCommunityPage = 1;
|
||||
expandedNodes.push(communityFlatNode);
|
||||
});
|
||||
const sub = service.loadCommunities(expandedNodes)
|
||||
.pipe(take(1)).subscribe((value) => flatNodeList = value);
|
||||
sub.unsubscribe();
|
||||
service.loadCommunities({
|
||||
currentPage: 1,
|
||||
sort: new SortOptions('dc.title', SortDirection.ASC)
|
||||
}, expandedNodes)
|
||||
.pipe(take(1))
|
||||
.subscribe((value) => {
|
||||
flatNodeList = value;
|
||||
done();
|
||||
});
|
||||
});
|
||||
it('length of flatnode list should be as big as top community list and size of its possible page-limited children', () => {
|
||||
expect(flatNodeList.length).toEqual(mockListOfTopCommunitiesPage1.length + mockSubcommunities1Page1.length + mockSubcommunities1Page1.length);
|
||||
@@ -281,14 +296,20 @@ describe('CommunityListService', () => {
|
||||
});
|
||||
});
|
||||
describe('Just first top comm expanded, all page 1: should return list containing flatnodes of the communities in the test list and all its possible page-limited children (subcommunities and collections)', () => {
|
||||
beforeEach(() => {
|
||||
beforeEach((done) => {
|
||||
const communityFlatNode = toFlatNode(mockListOfTopCommunitiesPage1[0], observableOf(true), 0, true, null);
|
||||
communityFlatNode.currentCollectionPage = 1;
|
||||
communityFlatNode.currentCommunityPage = 1;
|
||||
const expandedNodes = [communityFlatNode];
|
||||
const sub = service.loadCommunities(expandedNodes)
|
||||
.pipe(take(1)).subscribe((value) => flatNodeList = value);
|
||||
sub.unsubscribe();
|
||||
service.loadCommunities({
|
||||
currentPage: 1,
|
||||
sort: new SortOptions('dc.title', SortDirection.ASC)
|
||||
}, expandedNodes)
|
||||
.pipe(take(1))
|
||||
.subscribe((value) => {
|
||||
flatNodeList = value;
|
||||
done();
|
||||
});
|
||||
});
|
||||
it('length of flatnode list should be as big as top community list and size of page-limited children of first top community', () => {
|
||||
expect(flatNodeList.length).toEqual(mockListOfTopCommunitiesPage1.length + mockSubcommunities1Page1.length);
|
||||
@@ -300,14 +321,20 @@ describe('CommunityListService', () => {
|
||||
});
|
||||
});
|
||||
describe('Just second top comm expanded, collections at page 2: should return list containing flatnodes of the communities in the test list and all its possible page-limited children (subcommunities and collections)', () => {
|
||||
beforeEach(() => {
|
||||
beforeEach((done) => {
|
||||
const communityFlatNode = toFlatNode(mockListOfTopCommunitiesPage1[1], observableOf(true), 0, true, null);
|
||||
communityFlatNode.currentCollectionPage = 2;
|
||||
communityFlatNode.currentCommunityPage = 1;
|
||||
const expandedNodes = [communityFlatNode];
|
||||
const sub = service.loadCommunities(expandedNodes)
|
||||
.pipe(take(1)).subscribe((value) => flatNodeList = value);
|
||||
sub.unsubscribe();
|
||||
service.loadCommunities({
|
||||
currentPage: 1,
|
||||
sort: new SortOptions('dc.title', SortDirection.ASC)
|
||||
}, expandedNodes)
|
||||
.pipe(take(1))
|
||||
.subscribe((value) => {
|
||||
flatNodeList = value;
|
||||
done();
|
||||
});
|
||||
});
|
||||
it('length of flatnode list should be as big as top community list and size of page-limited children of second top community', () => {
|
||||
expect(flatNodeList.length).toEqual(mockListOfTopCommunitiesPage1.length + mockCollectionsPage1.length + mockCollectionsPage2.length);
|
||||
@@ -333,10 +360,13 @@ describe('CommunityListService', () => {
|
||||
});
|
||||
let flatNodeList;
|
||||
describe('None expanded: should return list containing only flatnodes of the communities in the test list', () => {
|
||||
beforeEach(() => {
|
||||
const sub = service.transformListOfCommunities(new PaginatedList(new PageInfo(), listOfCommunities), 0, null, null)
|
||||
.pipe(take(1)).subscribe((value) => flatNodeList = value);
|
||||
sub.unsubscribe();
|
||||
beforeEach((done) => {
|
||||
service.transformListOfCommunities(new PaginatedList(new PageInfo(), listOfCommunities), 0, null, null)
|
||||
.pipe(take(1))
|
||||
.subscribe((value) => {
|
||||
flatNodeList = value;
|
||||
done();
|
||||
});
|
||||
});
|
||||
it('length of flatnode list should be as big as community test list', () => {
|
||||
expect(flatNodeList.length).toEqual(listOfCommunities.length);
|
||||
@@ -353,7 +383,7 @@ describe('CommunityListService', () => {
|
||||
});
|
||||
});
|
||||
describe('All top expanded, all page 1: should return list containing flatnodes of the communities in the test list and all its possible page-limited children (subcommunities and collections)', () => {
|
||||
beforeEach(() => {
|
||||
beforeEach((done) => {
|
||||
const expandedNodes = [];
|
||||
listOfCommunities.map((community: Community) => {
|
||||
const communityFlatNode = toFlatNode(community, observableOf(true), 0, true, null);
|
||||
@@ -361,9 +391,12 @@ describe('CommunityListService', () => {
|
||||
communityFlatNode.currentCommunityPage = 1;
|
||||
expandedNodes.push(communityFlatNode);
|
||||
});
|
||||
const sub = service.transformListOfCommunities(new PaginatedList(new PageInfo(), listOfCommunities), 0, null, expandedNodes)
|
||||
.pipe(take(1)).subscribe((value) => flatNodeList = value);
|
||||
sub.unsubscribe();
|
||||
service.transformListOfCommunities(new PaginatedList(new PageInfo(), listOfCommunities), 0, null, expandedNodes)
|
||||
.pipe(take(1))
|
||||
.subscribe((value) => {
|
||||
flatNodeList = value;
|
||||
done();
|
||||
});
|
||||
});
|
||||
it('length of flatnode list should be as big as community test list and size of its possible children', () => {
|
||||
expect(flatNodeList.length).toEqual(listOfCommunities.length + mockSubcommunities1Page1.length + mockSubcommunities1Page1.length);
|
||||
@@ -397,10 +430,13 @@ describe('CommunityListService', () => {
|
||||
});
|
||||
let flatNodeList;
|
||||
describe('should return list containing only flatnode corresponding to that community', () => {
|
||||
beforeEach(() => {
|
||||
const sub = service.transformCommunity(communityWithNoSubcomsOrColls, 0, null, null)
|
||||
.pipe(take(1)).subscribe((value) => flatNodeList = value);
|
||||
sub.unsubscribe();
|
||||
beforeEach((done) => {
|
||||
service.transformCommunity(communityWithNoSubcomsOrColls, 0, null, null)
|
||||
.pipe(take(1))
|
||||
.subscribe((value) => {
|
||||
flatNodeList = value;
|
||||
done();
|
||||
});
|
||||
});
|
||||
it('length of flatnode list should be 1', () => {
|
||||
expect(flatNodeList.length).toEqual(1);
|
||||
@@ -426,10 +462,14 @@ describe('CommunityListService', () => {
|
||||
});
|
||||
let flatNodeList;
|
||||
describe('should return list containing only flatnode corresponding to that community', () => {
|
||||
beforeAll(() => {
|
||||
const sub = service.transformCommunity(communityWithSubcoms, 0, null, null)
|
||||
.pipe(take(1)).subscribe((value) => flatNodeList = value);
|
||||
sub.unsubscribe();
|
||||
beforeAll((done) => {
|
||||
service.transformCommunity(communityWithSubcoms, 0, null, null)
|
||||
.pipe(take(1))
|
||||
.subscribe((value) => {
|
||||
flatNodeList = value;
|
||||
done();
|
||||
});
|
||||
|
||||
});
|
||||
it('length of flatnode list should be 1', () => {
|
||||
expect(flatNodeList.length).toEqual(1);
|
||||
@@ -455,14 +495,17 @@ describe('CommunityListService', () => {
|
||||
}
|
||||
});
|
||||
let flatNodeList;
|
||||
beforeEach(() => {
|
||||
beforeEach((done) => {
|
||||
const communityFlatNode = toFlatNode(communityWithSubcoms, observableOf(true), 0, true, null);
|
||||
communityFlatNode.currentCollectionPage = 1;
|
||||
communityFlatNode.currentCommunityPage = 1;
|
||||
const expandedNodes = [communityFlatNode];
|
||||
const sub = service.transformCommunity(communityWithSubcoms, 0, null, expandedNodes)
|
||||
.pipe(take(1)).subscribe((value) => flatNodeList = value);
|
||||
sub.unsubscribe();
|
||||
service.transformCommunity(communityWithSubcoms, 0, null, expandedNodes)
|
||||
.pipe(take(1))
|
||||
.subscribe((value) => {
|
||||
flatNodeList = value;
|
||||
done();
|
||||
});
|
||||
});
|
||||
it('list of flatnodes is length is 1 + nrOfSubcoms & first flatnode is of expanded test community', () => {
|
||||
expect(flatNodeList.length).toEqual(1 + mockSubcommunities1Page1.length);
|
||||
@@ -485,7 +528,7 @@ describe('CommunityListService', () => {
|
||||
describe('should return list containing flatnodes of that community, its collections of the first two pages', () => {
|
||||
let communityWithCollections;
|
||||
let flatNodeList;
|
||||
beforeEach(() => {
|
||||
beforeEach((done) => {
|
||||
communityWithCollections = Object.assign(new Community(), {
|
||||
id: '9076bd16-e69a-48d6-9e41-0238cb40d863',
|
||||
uuid: '9076bd16-e69a-48d6-9e41-0238cb40d863',
|
||||
@@ -500,9 +543,12 @@ describe('CommunityListService', () => {
|
||||
communityFlatNode.currentCollectionPage = 2;
|
||||
communityFlatNode.currentCommunityPage = 1;
|
||||
const expandedNodes = [communityFlatNode];
|
||||
const sub = service.transformCommunity(communityWithCollections, 0, null, expandedNodes)
|
||||
.pipe(take(1)).subscribe((value) => flatNodeList = value);
|
||||
sub.unsubscribe();
|
||||
service.transformCommunity(communityWithCollections, 0, null, expandedNodes)
|
||||
.pipe(take(1))
|
||||
.subscribe((value) => {
|
||||
flatNodeList = value;
|
||||
done();
|
||||
});
|
||||
});
|
||||
it('list of flatnodes is length is 1 + nrOfCollections & first flatnode is of expanded test community', () => {
|
||||
expect(flatNodeList.length).toEqual(1 + mockCollectionsPage1.length + mockCollectionsPage2.length);
|
||||
@@ -533,7 +579,7 @@ describe('CommunityListService', () => {
|
||||
|
||||
describe('getIsExpandable', () => {
|
||||
describe('should return true', () => {
|
||||
it('if community has subcommunities', () => {
|
||||
it('if community has subcommunities', (done) => {
|
||||
const communityWithSubcoms = Object.assign(new Community(), {
|
||||
id: '7669c72a-3f2a-451f-a3b9-9210e7a4c02f',
|
||||
uuid: '7669c72a-3f2a-451f-a3b9-9210e7a4c02f',
|
||||
@@ -546,9 +592,10 @@ describe('CommunityListService', () => {
|
||||
});
|
||||
service.getIsExpandable(communityWithSubcoms).pipe(take(1)).subscribe((result) => {
|
||||
expect(result).toEqual(true);
|
||||
done();
|
||||
});
|
||||
});
|
||||
it('if community has collections', () => {
|
||||
it('if community has collections', (done) => {
|
||||
const communityWithCollections = Object.assign(new Community(), {
|
||||
id: '9076bd16-e69a-48d6-9e41-0238cb40d863',
|
||||
uuid: '9076bd16-e69a-48d6-9e41-0238cb40d863',
|
||||
@@ -561,11 +608,12 @@ describe('CommunityListService', () => {
|
||||
});
|
||||
service.getIsExpandable(communityWithCollections).pipe(take(1)).subscribe((result) => {
|
||||
expect(result).toEqual(true);
|
||||
done();
|
||||
});
|
||||
});
|
||||
});
|
||||
describe('should return false', () => {
|
||||
it('if community has neither subcommunities nor collections', () => {
|
||||
it('if community has neither subcommunities nor collections', (done) => {
|
||||
const communityWithNoSubcomsOrColls = Object.assign(new Community(), {
|
||||
id: 'efbf25e1-2d8c-4c28-8f3e-2e04c215be24',
|
||||
uuid: 'efbf25e1-2d8c-4c28-8f3e-2e04c215be24',
|
||||
@@ -578,6 +626,7 @@ describe('CommunityListService', () => {
|
||||
});
|
||||
service.getIsExpandable(communityWithNoSubcomsOrColls).pipe(take(1)).subscribe((result) => {
|
||||
expect(result).toEqual(false);
|
||||
done();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
@@ -4,11 +4,12 @@ import { combineLatest as observableCombineLatest } from 'rxjs/internal/observab
|
||||
import { Observable, of as observableOf } from 'rxjs';
|
||||
import { AppState } from '../app.reducer';
|
||||
import { CommunityDataService } from '../core/data/community-data.service';
|
||||
import { PaginationComponentOptions } from '../shared/pagination/pagination-component-options.model';
|
||||
import { SortDirection, SortOptions } from '../core/cache/models/sort-options.model';
|
||||
import { catchError, filter, map, switchMap, take } from 'rxjs/operators';
|
||||
import { FindListOptions } from '../core/data/request.models';
|
||||
import { map, flatMap } from 'rxjs/operators';
|
||||
import { Community } from '../core/shared/community.model';
|
||||
import { Collection } from '../core/shared/collection.model';
|
||||
import { getSucceededRemoteData } from '../core/shared/operators';
|
||||
import { PageInfo } from '../core/shared/page-info.model';
|
||||
import { hasValue, isNotEmpty } from '../shared/empty.util';
|
||||
import { RemoteData } from '../core/data/remote-data';
|
||||
import { PaginatedList } from '../core/data/paginated-list';
|
||||
@@ -46,8 +47,7 @@ export class ShowMoreFlatNode {
|
||||
// Helper method to combine an flatten an array of observables of flatNode arrays
|
||||
export const combineAndFlatten = (obsList: Array<Observable<FlatNode[]>>): Observable<FlatNode[]> =>
|
||||
observableCombineLatest(...obsList).pipe(
|
||||
map((matrix: FlatNode[][]) =>
|
||||
matrix.reduce((combinedList, currentList: FlatNode[]) => [...combinedList, ...currentList]))
|
||||
map((matrix: any[][]) => [].concat(...matrix))
|
||||
);
|
||||
|
||||
/**
|
||||
@@ -99,6 +99,8 @@ const communityListStateSelector = (state: AppState) => state.communityList;
|
||||
const expandedNodesSelector = createSelector(communityListStateSelector, (communityList: CommunityListState) => communityList.expandedNodes);
|
||||
const loadingNodeSelector = createSelector(communityListStateSelector, (communityList: CommunityListState) => communityList.loadingNode);
|
||||
|
||||
export const MAX_COMCOLS_PER_PAGE = 50;
|
||||
|
||||
/**
|
||||
* Service class for the community list, responsible for the creating of the flat list used by communityList dataSource
|
||||
* and connection to the store to retrieve and save the state of the community list
|
||||
@@ -107,26 +109,8 @@ const loadingNodeSelector = createSelector(communityListStateSelector, (communit
|
||||
@Injectable()
|
||||
export class CommunityListService {
|
||||
|
||||
// page-limited list of top-level communities
|
||||
payloads$: Array<Observable<PaginatedList<Community>>>;
|
||||
|
||||
topCommunitiesConfig: PaginationComponentOptions;
|
||||
topCommunitiesSortConfig: SortOptions;
|
||||
|
||||
maxSubCommunitiesPerPage: number;
|
||||
maxCollectionsPerPage: number;
|
||||
|
||||
constructor(private communityDataService: CommunityDataService, private collectionDataService: CollectionDataService,
|
||||
private store: Store<any>) {
|
||||
this.topCommunitiesConfig = new PaginationComponentOptions();
|
||||
this.topCommunitiesConfig.id = 'top-level-pagination';
|
||||
this.topCommunitiesConfig.pageSize = 10;
|
||||
this.topCommunitiesConfig.currentPage = 1;
|
||||
this.topCommunitiesSortConfig = new SortOptions('dc.title', SortDirection.ASC);
|
||||
this.initTopCommunityList();
|
||||
|
||||
this.maxSubCommunitiesPerPage = 3;
|
||||
this.maxCollectionsPerPage = 3;
|
||||
}
|
||||
|
||||
saveCommunityListStateToStore(expandedNodes: FlatNode[], loadingNode: FlatNode): void {
|
||||
@@ -141,57 +125,46 @@ export class CommunityListService {
|
||||
return this.store.select(loadingNodeSelector);
|
||||
}
|
||||
|
||||
/**
|
||||
* Increases the payload so it contains the next page of top level communities
|
||||
*/
|
||||
getNextPageTopCommunities(): void {
|
||||
this.topCommunitiesConfig.currentPage = this.topCommunitiesConfig.currentPage + 1;
|
||||
this.payloads$ = [...this.payloads$, this.communityDataService.findTop({
|
||||
currentPage: this.topCommunitiesConfig.currentPage,
|
||||
elementsPerPage: this.topCommunitiesConfig.pageSize,
|
||||
sort: {
|
||||
field: this.topCommunitiesSortConfig.field,
|
||||
direction: this.topCommunitiesSortConfig.direction
|
||||
}
|
||||
}).pipe(
|
||||
take(1),
|
||||
map((results) => results.payload),
|
||||
)];
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets all top communities, limited by page, and transforms this in a list of flatNodes.
|
||||
* @param expandedNodes List of expanded nodes; if a node is not expanded its subCommunities and collections need
|
||||
* not be added to the list
|
||||
*/
|
||||
loadCommunities(expandedNodes: FlatNode[]): Observable<FlatNode[]> {
|
||||
const res = this.payloads$.map((payload) => {
|
||||
return payload.pipe(
|
||||
take(1),
|
||||
switchMap((result: PaginatedList<Community>) => {
|
||||
return this.transformListOfCommunities(result, 0, null, expandedNodes);
|
||||
}),
|
||||
catchError(() => observableOf([])),
|
||||
);
|
||||
});
|
||||
return combineAndFlatten(res);
|
||||
loadCommunities(findOptions: FindListOptions, expandedNodes: FlatNode[]): Observable<FlatNode[]> {
|
||||
const currentPage = findOptions.currentPage;
|
||||
const topCommunities = [];
|
||||
for (let i = 1; i <= currentPage; i++) {
|
||||
const pagination: FindListOptions = Object.assign({}, findOptions, { currentPage: i });
|
||||
topCommunities.push(this.getTopCommunities(pagination));
|
||||
}
|
||||
const topComs$ = observableCombineLatest(...topCommunities).pipe(
|
||||
map((coms: Array<PaginatedList<Community>>) => {
|
||||
const newPages: Community[][] = coms.map((unit: PaginatedList<Community>) => unit.page);
|
||||
const newPage: Community[] = [].concat(...newPages);
|
||||
let newPageInfo = new PageInfo();
|
||||
if (coms && coms.length > 0) {
|
||||
newPageInfo = Object.assign({}, coms[0].pageInfo, { currentPage })
|
||||
}
|
||||
return new PaginatedList(newPageInfo, newPage);
|
||||
})
|
||||
);
|
||||
return topComs$.pipe(flatMap((topComs: PaginatedList<Community>) => this.transformListOfCommunities(topComs, 0, null, expandedNodes)));
|
||||
};
|
||||
|
||||
/**
|
||||
* Puts the initial top level communities in a list to be called upon
|
||||
*/
|
||||
private initTopCommunityList(): void {
|
||||
this.payloads$ = [this.communityDataService.findTop({
|
||||
currentPage: this.topCommunitiesConfig.currentPage,
|
||||
elementsPerPage: this.topCommunitiesConfig.pageSize,
|
||||
private getTopCommunities(options: FindListOptions): Observable<PaginatedList<Community>> {
|
||||
return this.communityDataService.findTop({
|
||||
currentPage: options.currentPage,
|
||||
elementsPerPage: MAX_COMCOLS_PER_PAGE,
|
||||
sort: {
|
||||
field: this.topCommunitiesSortConfig.field,
|
||||
direction: this.topCommunitiesSortConfig.direction
|
||||
field: options.sort.field,
|
||||
direction: options.sort.direction
|
||||
}
|
||||
}).pipe(
|
||||
take(1),
|
||||
map((results) => results.payload),
|
||||
)];
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -206,16 +179,15 @@ export class CommunityListService {
|
||||
parent: FlatNode,
|
||||
expandedNodes: FlatNode[]): Observable<FlatNode[]> {
|
||||
if (isNotEmpty(listOfPaginatedCommunities.page)) {
|
||||
let currentPage = this.topCommunitiesConfig.currentPage;
|
||||
let currentPage = listOfPaginatedCommunities.currentPage;
|
||||
if (isNotEmpty(parent)) {
|
||||
currentPage = expandedNodes.find((node: FlatNode) => node.id === parent.id).currentCommunityPage;
|
||||
}
|
||||
const isNotAllCommunities = (listOfPaginatedCommunities.totalElements > (listOfPaginatedCommunities.elementsPerPage * currentPage));
|
||||
let obsList = listOfPaginatedCommunities.page
|
||||
.map((community: Community) => {
|
||||
return this.transformCommunity(community, level, parent, expandedNodes)
|
||||
});
|
||||
if (isNotAllCommunities && listOfPaginatedCommunities.currentPage > currentPage) {
|
||||
if (currentPage < listOfPaginatedCommunities.totalPages && currentPage === listOfPaginatedCommunities.currentPage) {
|
||||
obsList = [...obsList, observableOf([showMoreFlatNode('community', level, parent)])];
|
||||
}
|
||||
|
||||
@@ -252,13 +224,12 @@ export class CommunityListService {
|
||||
let subcoms = [];
|
||||
for (let i = 1; i <= currentCommunityPage; i++) {
|
||||
const nextSetOfSubcommunitiesPage = this.communityDataService.findByParent(community.uuid, {
|
||||
elementsPerPage: this.maxSubCommunitiesPerPage,
|
||||
elementsPerPage: MAX_COMCOLS_PER_PAGE,
|
||||
currentPage: i
|
||||
})
|
||||
.pipe(
|
||||
filter((rd: RemoteData<PaginatedList<Community>>) => rd.hasSucceeded),
|
||||
take(1),
|
||||
switchMap((rd: RemoteData<PaginatedList<Community>>) =>
|
||||
getSucceededRemoteData(),
|
||||
flatMap((rd: RemoteData<PaginatedList<Community>>) =>
|
||||
this.transformListOfCommunities(rd.payload, level + 1, communityFlatNode, expandedNodes))
|
||||
);
|
||||
|
||||
@@ -271,16 +242,15 @@ export class CommunityListService {
|
||||
let collections = [];
|
||||
for (let i = 1; i <= currentCollectionPage; i++) {
|
||||
const nextSetOfCollectionsPage = this.collectionDataService.findByParent(community.uuid, {
|
||||
elementsPerPage: this.maxCollectionsPerPage,
|
||||
elementsPerPage: MAX_COMCOLS_PER_PAGE,
|
||||
currentPage: i
|
||||
})
|
||||
.pipe(
|
||||
filter((rd: RemoteData<PaginatedList<Collection>>) => rd.hasSucceeded),
|
||||
take(1),
|
||||
getSucceededRemoteData(),
|
||||
map((rd: RemoteData<PaginatedList<Collection>>) => {
|
||||
let nodes = rd.payload.page
|
||||
.map((collection: Collection) => toFlatNode(collection, observableOf(false), level + 1, false, communityFlatNode));
|
||||
if ((rd.payload.elementsPerPage * currentCollectionPage) < rd.payload.totalElements && rd.payload.currentPage > currentCollectionPage) {
|
||||
if (currentCollectionPage < rd.payload.totalPages && currentCollectionPage === rd.payload.currentPage) {
|
||||
nodes = [...nodes, showMoreFlatNode('collection', level + 1, communityFlatNode)];
|
||||
}
|
||||
return nodes;
|
||||
@@ -305,21 +275,18 @@ export class CommunityListService {
|
||||
let hasColls$: Observable<boolean>;
|
||||
hasSubcoms$ = this.communityDataService.findByParent(community.uuid, { elementsPerPage: 1 })
|
||||
.pipe(
|
||||
filter((rd: RemoteData<PaginatedList<Community>>) => rd.hasSucceeded),
|
||||
take(1),
|
||||
getSucceededRemoteData(),
|
||||
map((results) => results.payload.totalElements > 0),
|
||||
);
|
||||
|
||||
hasColls$ = this.collectionDataService.findByParent(community.uuid, { elementsPerPage: 1 })
|
||||
.pipe(
|
||||
filter((rd: RemoteData<PaginatedList<Collection>>) => rd.hasSucceeded),
|
||||
take(1),
|
||||
getSucceededRemoteData(),
|
||||
map((results) => results.payload.totalElements > 0),
|
||||
);
|
||||
|
||||
let hasChildren$: Observable<boolean>;
|
||||
hasChildren$ = observableCombineLatest(hasSubcoms$, hasColls$).pipe(
|
||||
take(1),
|
||||
map(([hasSubcoms, hasColls]: [boolean, boolean]) => {
|
||||
if (hasSubcoms || hasColls) {
|
||||
return true;
|
||||
|
@@ -114,15 +114,9 @@ describe('CommunityListComponent', () => {
|
||||
|
||||
beforeEach(async(() => {
|
||||
communityListServiceStub = {
|
||||
topPageSize: 2,
|
||||
topCurrentPage: 1,
|
||||
collectionPageSize: 2,
|
||||
subcommunityPageSize: 2,
|
||||
pageSize: 2,
|
||||
expandedNodes: [],
|
||||
loadingNode: null,
|
||||
getNextPageTopCommunities() {
|
||||
this.topCurrentPage++;
|
||||
},
|
||||
getLoadingNodeFromStore() {
|
||||
return observableOf(this.loadingNode);
|
||||
},
|
||||
@@ -133,12 +127,12 @@ describe('CommunityListComponent', () => {
|
||||
this.expandedNodes = expandedNodes;
|
||||
this.loadingNode = loadingNode;
|
||||
},
|
||||
loadCommunities(expandedNodes) {
|
||||
loadCommunities(options, expandedNodes) {
|
||||
let flatnodes;
|
||||
let showMoreTopComNode = false;
|
||||
flatnodes = [...mockTopFlatnodesUnexpanded];
|
||||
const currentPage = this.topCurrentPage;
|
||||
const elementsPerPage = this.topPageSize;
|
||||
const currentPage = options.currentPage;
|
||||
const elementsPerPage = this.pageSize;
|
||||
let endPageIndex = (currentPage * elementsPerPage);
|
||||
if (endPageIndex >= flatnodes.length) {
|
||||
endPageIndex = flatnodes.length;
|
||||
@@ -171,14 +165,14 @@ describe('CommunityListComponent', () => {
|
||||
collFlatnodes = [...collFlatnodes, toFlatNode(coll, observableOf(false), topNode.level + 1, false, topNode)];
|
||||
});
|
||||
if (isNotEmpty(subComFlatnodes)) {
|
||||
const endSubComIndex = this.subcommunityPageSize * expandedParent.currentCommunityPage;
|
||||
const endSubComIndex = this.pageSize * expandedParent.currentCommunityPage;
|
||||
flatnodes = [...flatnodes, ...subComFlatnodes.slice(0, endSubComIndex)];
|
||||
if (subComFlatnodes.length > endSubComIndex) {
|
||||
flatnodes = [...flatnodes, showMoreFlatNode('community', topNode.level + 1, expandedParent)];
|
||||
}
|
||||
}
|
||||
if (isNotEmpty(collFlatnodes)) {
|
||||
const endColIndex = this.collectionPageSize * expandedParent.currentCollectionPage;
|
||||
const endColIndex = this.pageSize * expandedParent.currentCollectionPage;
|
||||
flatnodes = [...flatnodes, ...collFlatnodes.slice(0, endColIndex)];
|
||||
if (collFlatnodes.length > endColIndex) {
|
||||
flatnodes = [...flatnodes, showMoreFlatNode('collection', topNode.level + 1, expandedParent)];
|
||||
|
@@ -1,5 +1,7 @@
|
||||
import { Component, OnDestroy, OnInit } from '@angular/core';
|
||||
import { Component, NgZone, OnDestroy, OnInit } from '@angular/core';
|
||||
import { take } from 'rxjs/operators';
|
||||
import { SortDirection, SortOptions } from '../../core/cache/models/sort-options.model';
|
||||
import { FindListOptions } from '../../core/data/request.models';
|
||||
import { CommunityListService, FlatNode } from '../community-list-service';
|
||||
import { CommunityListDatasource } from '../community-list-datasource';
|
||||
import { FlatTreeControl } from '@angular/cdk/tree';
|
||||
@@ -27,17 +29,24 @@ export class CommunityListComponent implements OnInit, OnDestroy {
|
||||
|
||||
dataSource: CommunityListDatasource;
|
||||
|
||||
constructor(private communityListService: CommunityListService) {
|
||||
paginationConfig: FindListOptions;
|
||||
|
||||
constructor(private communityListService: CommunityListService,
|
||||
private zone: NgZone) {
|
||||
this.paginationConfig = new FindListOptions();
|
||||
this.paginationConfig.elementsPerPage = 2;
|
||||
this.paginationConfig.currentPage = 1;
|
||||
this.paginationConfig.sort = new SortOptions('dc.title', SortDirection.ASC);
|
||||
}
|
||||
|
||||
ngOnInit() {
|
||||
this.dataSource = new CommunityListDatasource(this.communityListService);
|
||||
this.dataSource = new CommunityListDatasource(this.communityListService, this.zone);
|
||||
this.communityListService.getLoadingNodeFromStore().pipe(take(1)).subscribe((result) => {
|
||||
this.loadingNode = result;
|
||||
});
|
||||
this.communityListService.getExpandedNodesFromStore().pipe(take(1)).subscribe((result) => {
|
||||
this.expandedNodes = [...result];
|
||||
this.dataSource.loadCommunities(this.expandedNodes);
|
||||
this.dataSource.loadCommunities(this.paginationConfig, this.expandedNodes);
|
||||
});
|
||||
}
|
||||
|
||||
@@ -74,7 +83,7 @@ export class CommunityListComponent implements OnInit, OnDestroy {
|
||||
node.currentCommunityPage = 1;
|
||||
}
|
||||
}
|
||||
this.dataSource.loadCommunities(this.expandedNodes);
|
||||
this.dataSource.loadCommunities(this.paginationConfig, this.expandedNodes);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -94,10 +103,10 @@ export class CommunityListComponent implements OnInit, OnDestroy {
|
||||
const parentNodeInExpandedNodes = this.expandedNodes.find((node2: FlatNode) => node.parent.id === node2.id);
|
||||
parentNodeInExpandedNodes.currentCommunityPage++;
|
||||
}
|
||||
this.dataSource.loadCommunities(this.expandedNodes);
|
||||
this.dataSource.loadCommunities(this.paginationConfig, this.expandedNodes);
|
||||
} else {
|
||||
this.communityListService.getNextPageTopCommunities();
|
||||
this.dataSource.loadCommunities(this.expandedNodes);
|
||||
this.paginationConfig.currentPage++;
|
||||
this.dataSource.loadCommunities(this.paginationConfig, this.expandedNodes);
|
||||
}
|
||||
}
|
||||
|
||||
|
@@ -150,12 +150,7 @@ export class RemoteDataBuildService {
|
||||
filterSuccessfulResponses(),
|
||||
map((response: DSOSuccessResponse) => {
|
||||
if (hasValue((response as DSOSuccessResponse).pageInfo)) {
|
||||
const resPageInfo = (response as DSOSuccessResponse).pageInfo;
|
||||
if (isNotEmpty(resPageInfo) && resPageInfo.currentPage >= 0) {
|
||||
return Object.assign({}, resPageInfo, { currentPage: resPageInfo.currentPage + 1 });
|
||||
} else {
|
||||
return resPageInfo;
|
||||
}
|
||||
return (response as DSOSuccessResponse).pageInfo;
|
||||
}
|
||||
})
|
||||
);
|
||||
|
@@ -88,10 +88,12 @@ export class BundleDataService extends DataService<Bundle> {
|
||||
/**
|
||||
* Get the bitstreams endpoint for a bundle
|
||||
* @param bundleId
|
||||
* @param searchOptions
|
||||
*/
|
||||
getBitstreamsEndpoint(bundleId: string): Observable<string> {
|
||||
getBitstreamsEndpoint(bundleId: string, searchOptions?: PaginatedSearchOptions): Observable<string> {
|
||||
return this.getBrowseEndpoint().pipe(
|
||||
switchMap((href: string) => this.halService.getEndpoint(this.bitstreamsEndpoint, `${href}/${bundleId}`))
|
||||
switchMap((href: string) => this.halService.getEndpoint(this.bitstreamsEndpoint, `${href}/${bundleId}`)),
|
||||
map((href) => searchOptions ? searchOptions.toRestUrl(href) : href)
|
||||
);
|
||||
}
|
||||
|
||||
@@ -102,9 +104,8 @@ export class BundleDataService extends DataService<Bundle> {
|
||||
* @param linksToFollow The {@link FollowLinkConfig}s for the request
|
||||
*/
|
||||
getBitstreams(bundleId: string, searchOptions?: PaginatedSearchOptions, ...linksToFollow: Array<FollowLinkConfig<Bitstream>>): Observable<RemoteData<PaginatedList<Bitstream>>> {
|
||||
const hrefObs = this.getBitstreamsEndpoint(bundleId).pipe(
|
||||
map((href) => searchOptions ? searchOptions.toRestUrl(href) : href)
|
||||
);
|
||||
const hrefObs = this.getBitstreamsEndpoint(bundleId, searchOptions);
|
||||
|
||||
hrefObs.pipe(
|
||||
take(1)
|
||||
).subscribe((href) => {
|
||||
|
@@ -13,13 +13,19 @@ import { RequestEntry } from './request.reducer';
|
||||
import { ErrorResponse, RestResponse } from '../cache/response.models';
|
||||
import { ObjectCacheService } from '../cache/object-cache.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 collectionId = 'fake-collection-id';
|
||||
|
||||
describe('CollectionDataService', () => {
|
||||
let service: CollectionDataService;
|
||||
|
||||
let scheduler: TestScheduler;
|
||||
let requestService: RequestService;
|
||||
let translate: TranslateService;
|
||||
let notificationsService: any;
|
||||
@@ -27,6 +33,44 @@ describe('CollectionDataService', () => {
|
||||
let objectCache: ObjectCacheService;
|
||||
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', () => {
|
||||
beforeEach(() => {
|
||||
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', () => {
|
||||
@@ -117,7 +198,9 @@ describe('CollectionDataService', () => {
|
||||
function createService(requestEntry$?) {
|
||||
requestService = getMockRequestService(requestEntry$);
|
||||
rdbService = jasmine.createSpyObj('rdbService', {
|
||||
buildList: jasmine.createSpy('buildList')
|
||||
buildList: hot('a|', {
|
||||
a: paginatedListRD
|
||||
})
|
||||
});
|
||||
objectCache = jasmine.createSpyObj('objectCache', {
|
||||
remove: jasmine.createSpy('remove')
|
||||
|
@@ -72,14 +72,18 @@ export class CollectionDataService extends ComColDataService<Collection> {
|
||||
/**
|
||||
* 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
|
||||
* @return Observable<RemoteData<PaginatedList<Collection>>>
|
||||
* collection list
|
||||
*/
|
||||
getAuthorizedCollection(options: FindListOptions = {}): Observable<RemoteData<PaginatedList<Collection>>> {
|
||||
const searchHref = 'findAuthorized';
|
||||
getAuthorizedCollection(query: string, options: FindListOptions = {}, ...linksToFollow: Array<FollowLinkConfig<Collection>>): Observable<RemoteData<PaginatedList<Collection>>> {
|
||||
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));
|
||||
}
|
||||
|
||||
@@ -87,14 +91,18 @@ export class CollectionDataService extends ComColDataService<Collection> {
|
||||
* 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
|
||||
* @return Observable<RemoteData<PaginatedList<Collection>>>
|
||||
* collection list
|
||||
*/
|
||||
getAuthorizedCollectionByCommunity(communityId: string, options: FindListOptions = {}): Observable<RemoteData<PaginatedList<Collection>>> {
|
||||
const searchHref = 'findAuthorizedByCommunity';
|
||||
getAuthorizedCollectionByCommunity(communityId: string, query: string, options: FindListOptions = {}): Observable<RemoteData<PaginatedList<Collection>>> {
|
||||
const searchHref = 'findSubmitAuthorizedByCommunity';
|
||||
options = Object.assign({}, options, {
|
||||
searchParams: [new RequestParam('uuid', communityId)]
|
||||
searchParams: [
|
||||
new RequestParam('uuid', communityId),
|
||||
new RequestParam('query', query)
|
||||
]
|
||||
});
|
||||
|
||||
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
|
||||
*/
|
||||
hasAuthorizedCollection(): Observable<boolean> {
|
||||
const searchHref = 'findAuthorized';
|
||||
const searchHref = 'findSubmitAuthorized';
|
||||
const options = new FindListOptions();
|
||||
options.elementsPerPage = 1;
|
||||
|
||||
|
@@ -8,7 +8,6 @@ import {INotification} from '../../../shared/notifications/models/notification.m
|
||||
*/
|
||||
export const ObjectUpdatesActionTypes = {
|
||||
INITIALIZE_FIELDS: type('dspace/core/cache/object-updates/INITIALIZE_FIELDS'),
|
||||
ADD_PAGE_TO_CUSTOM_ORDER: type('dspace/core/cache/object-updates/ADD_PAGE_TO_CUSTOM_ORDER'),
|
||||
SET_EDITABLE_FIELD: type('dspace/core/cache/object-updates/SET_EDITABLE_FIELD'),
|
||||
SET_VALID_FIELD: type('dspace/core/cache/object-updates/SET_VALID_FIELD'),
|
||||
ADD_FIELD: type('dspace/core/cache/object-updates/ADD_FIELD'),
|
||||
@@ -17,8 +16,7 @@ export const ObjectUpdatesActionTypes = {
|
||||
REINSTATE: type('dspace/core/cache/object-updates/REINSTATE'),
|
||||
REMOVE: type('dspace/core/cache/object-updates/REMOVE'),
|
||||
REMOVE_ALL: type('dspace/core/cache/object-updates/REMOVE_ALL'),
|
||||
REMOVE_FIELD: type('dspace/core/cache/object-updates/REMOVE_FIELD'),
|
||||
MOVE: type('dspace/core/cache/object-updates/MOVE'),
|
||||
REMOVE_FIELD: type('dspace/core/cache/object-updates/REMOVE_FIELD')
|
||||
};
|
||||
|
||||
/* tslint:disable:max-classes-per-file */
|
||||
@@ -29,8 +27,7 @@ export const ObjectUpdatesActionTypes = {
|
||||
export enum FieldChangeType {
|
||||
UPDATE = 0,
|
||||
ADD = 1,
|
||||
REMOVE = 2,
|
||||
MOVE = 3
|
||||
REMOVE = 2
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -41,10 +38,7 @@ export class InitializeFieldsAction implements Action {
|
||||
payload: {
|
||||
url: string,
|
||||
fields: Identifiable[],
|
||||
lastModified: Date,
|
||||
order: string[],
|
||||
pageSize: number,
|
||||
page: number
|
||||
lastModified: Date
|
||||
};
|
||||
|
||||
/**
|
||||
@@ -61,42 +55,9 @@ export class InitializeFieldsAction implements Action {
|
||||
constructor(
|
||||
url: string,
|
||||
fields: Identifiable[],
|
||||
lastModified: Date,
|
||||
order: string[] = [],
|
||||
pageSize: number = 9999,
|
||||
page: number = 0
|
||||
lastModified: Date
|
||||
) {
|
||||
this.payload = { url, fields, lastModified, order, pageSize, page };
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* An ngrx action to initialize a new page's fields in the ObjectUpdates state
|
||||
*/
|
||||
export class AddPageToCustomOrderAction implements Action {
|
||||
type = ObjectUpdatesActionTypes.ADD_PAGE_TO_CUSTOM_ORDER;
|
||||
payload: {
|
||||
url: string,
|
||||
fields: Identifiable[],
|
||||
order: string[],
|
||||
page: number
|
||||
};
|
||||
|
||||
/**
|
||||
* Create a new AddPageToCustomOrderAction
|
||||
*
|
||||
* @param url The unique url of the page for which the fields are being added
|
||||
* @param fields The identifiable fields of which the updates are kept track of
|
||||
* @param order A custom order to keep track of objects moving around
|
||||
* @param page The page to populate in the custom order
|
||||
*/
|
||||
constructor(
|
||||
url: string,
|
||||
fields: Identifiable[],
|
||||
order: string[] = [],
|
||||
page: number = 0
|
||||
) {
|
||||
this.payload = { url, fields, order, page };
|
||||
this.payload = { url, fields, lastModified };
|
||||
}
|
||||
}
|
||||
|
||||
@@ -320,43 +281,6 @@ export class RemoveFieldUpdateAction implements Action {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* An ngrx action to remove a single field update in the ObjectUpdates state for a certain page url and field uuid
|
||||
*/
|
||||
export class MoveFieldUpdateAction implements Action {
|
||||
type = ObjectUpdatesActionTypes.MOVE;
|
||||
payload: {
|
||||
url: string,
|
||||
from: number,
|
||||
to: number,
|
||||
fromPage: number,
|
||||
toPage: number,
|
||||
field?: Identifiable
|
||||
};
|
||||
|
||||
/**
|
||||
* Create a new RemoveObjectUpdatesAction
|
||||
*
|
||||
* @param url
|
||||
* the unique url of the page for which a field's change should be removed
|
||||
* @param from The index of the object to move
|
||||
* @param to The index to move the object to
|
||||
* @param fromPage The page to move the object from
|
||||
* @param toPage The page to move the object to
|
||||
* @param field Optional field to add to the fieldUpdates list (useful when we want to track updates across multiple pages)
|
||||
*/
|
||||
constructor(
|
||||
url: string,
|
||||
from: number,
|
||||
to: number,
|
||||
fromPage: number,
|
||||
toPage: number,
|
||||
field?: Identifiable
|
||||
) {
|
||||
this.payload = { url, from, to, fromPage, toPage, field };
|
||||
}
|
||||
}
|
||||
|
||||
/* tslint:enable:max-classes-per-file */
|
||||
|
||||
/**
|
||||
@@ -369,8 +293,6 @@ export type ObjectUpdatesAction
|
||||
| ReinstateObjectUpdatesAction
|
||||
| RemoveObjectUpdatesAction
|
||||
| RemoveFieldUpdateAction
|
||||
| MoveFieldUpdateAction
|
||||
| AddPageToCustomOrderAction
|
||||
| RemoveAllObjectUpdatesAction
|
||||
| SelectVirtualMetadataAction
|
||||
| SetEditableFieldUpdateAction
|
||||
|
@@ -1,9 +1,9 @@
|
||||
import * as deepFreeze from 'deep-freeze';
|
||||
import {
|
||||
AddFieldUpdateAction, AddPageToCustomOrderAction,
|
||||
AddFieldUpdateAction,
|
||||
DiscardObjectUpdatesAction,
|
||||
FieldChangeType,
|
||||
InitializeFieldsAction, MoveFieldUpdateAction,
|
||||
InitializeFieldsAction,
|
||||
ReinstateObjectUpdatesAction, RemoveAllObjectUpdatesAction,
|
||||
RemoveFieldUpdateAction, RemoveObjectUpdatesAction, SelectVirtualMetadataAction,
|
||||
SetEditableFieldUpdateAction, SetValidFieldUpdateAction
|
||||
@@ -85,16 +85,6 @@ describe('objectUpdatesReducer', () => {
|
||||
virtualMetadataSources: {
|
||||
[relationship.uuid]: {[identifiable1.uuid]: true}
|
||||
},
|
||||
customOrder: {
|
||||
initialOrderPages: [
|
||||
{ order: [identifiable1.uuid, identifiable2.uuid, identifiable3.uuid] }
|
||||
],
|
||||
newOrderPages: [
|
||||
{ order: [identifiable1.uuid, identifiable2.uuid, identifiable3.uuid] }
|
||||
],
|
||||
pageSize: 10,
|
||||
changed: false
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
@@ -121,16 +111,6 @@ describe('objectUpdatesReducer', () => {
|
||||
virtualMetadataSources: {
|
||||
[relationship.uuid]: {[identifiable1.uuid]: true}
|
||||
},
|
||||
customOrder: {
|
||||
initialOrderPages: [
|
||||
{ order: [identifiable1.uuid, identifiable2.uuid, identifiable3.uuid] }
|
||||
],
|
||||
newOrderPages: [
|
||||
{ order: [identifiable1.uuid, identifiable2.uuid, identifiable3.uuid] }
|
||||
],
|
||||
pageSize: 10,
|
||||
changed: false
|
||||
}
|
||||
},
|
||||
[url + OBJECT_UPDATES_TRASH_PATH]: {
|
||||
fieldStates: {
|
||||
@@ -165,16 +145,6 @@ describe('objectUpdatesReducer', () => {
|
||||
virtualMetadataSources: {
|
||||
[relationship.uuid]: {[identifiable1.uuid]: true}
|
||||
},
|
||||
customOrder: {
|
||||
initialOrderPages: [
|
||||
{ order: [identifiable1.uuid, identifiable2.uuid, identifiable3.uuid] }
|
||||
],
|
||||
newOrderPages: [
|
||||
{ order: [identifiable1.uuid, identifiable2.uuid, identifiable3.uuid] }
|
||||
],
|
||||
pageSize: 10,
|
||||
changed: false
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
@@ -243,7 +213,7 @@ describe('objectUpdatesReducer', () => {
|
||||
});
|
||||
|
||||
it('should initialize all fields when the INITIALIZE action is dispatched, based on the payload', () => {
|
||||
const action = new InitializeFieldsAction(url, [identifiable1, identifiable3], modDate, [identifiable1.uuid, identifiable3.uuid], 10, 0);
|
||||
const action = new InitializeFieldsAction(url, [identifiable1, identifiable3], modDate);
|
||||
|
||||
const expectedState = {
|
||||
[url]: {
|
||||
@@ -261,17 +231,7 @@ describe('objectUpdatesReducer', () => {
|
||||
},
|
||||
fieldUpdates: {},
|
||||
virtualMetadataSources: {},
|
||||
lastModified: modDate,
|
||||
customOrder: {
|
||||
initialOrderPages: [
|
||||
{ order: [identifiable1.uuid, identifiable3.uuid] }
|
||||
],
|
||||
newOrderPages: [
|
||||
{ order: [identifiable1.uuid, identifiable3.uuid] }
|
||||
],
|
||||
pageSize: 10,
|
||||
changed: false
|
||||
}
|
||||
lastModified: modDate
|
||||
}
|
||||
};
|
||||
const newState = objectUpdatesReducer(testState, action);
|
||||
@@ -337,30 +297,4 @@ describe('objectUpdatesReducer', () => {
|
||||
const newState = objectUpdatesReducer(testState, action);
|
||||
expect(newState[url].fieldUpdates[uuid]).toBeUndefined();
|
||||
});
|
||||
|
||||
it('should move the custom order from the state when the MOVE action is dispatched', () => {
|
||||
const action = new MoveFieldUpdateAction(url, 0, 1, 0, 0);
|
||||
|
||||
const newState = objectUpdatesReducer(testState, action);
|
||||
expect(newState[url].customOrder.newOrderPages[0].order[0]).toEqual(testState[url].customOrder.newOrderPages[0].order[1]);
|
||||
expect(newState[url].customOrder.newOrderPages[0].order[1]).toEqual(testState[url].customOrder.newOrderPages[0].order[0]);
|
||||
expect(newState[url].customOrder.changed).toEqual(true);
|
||||
});
|
||||
|
||||
it('should add a new page to the custom order and add empty pages in between when the ADD_PAGE_TO_CUSTOM_ORDER action is dispatched', () => {
|
||||
const identifiable4 = {
|
||||
uuid: 'a23eae5a-7857-4ef9-8e52-989436ad2955',
|
||||
key: 'dc.description.abstract',
|
||||
language: null,
|
||||
value: 'Extra value'
|
||||
};
|
||||
const action = new AddPageToCustomOrderAction(url, [identifiable4], [identifiable4.uuid], 2);
|
||||
|
||||
const newState = objectUpdatesReducer(testState, action);
|
||||
// Confirm the page in between the two pages (index 1) has been filled with 10 (page size) undefined values
|
||||
expect(newState[url].customOrder.newOrderPages[1].order.length).toEqual(10);
|
||||
expect(newState[url].customOrder.newOrderPages[1].order[0]).toBeUndefined();
|
||||
// Verify the new page is correct
|
||||
expect(newState[url].customOrder.newOrderPages[2].order[0]).toEqual(identifiable4.uuid);
|
||||
});
|
||||
});
|
||||
|
@@ -1,8 +1,8 @@
|
||||
import {
|
||||
AddFieldUpdateAction, AddPageToCustomOrderAction,
|
||||
AddFieldUpdateAction,
|
||||
DiscardObjectUpdatesAction,
|
||||
FieldChangeType,
|
||||
InitializeFieldsAction, MoveFieldUpdateAction,
|
||||
InitializeFieldsAction,
|
||||
ObjectUpdatesAction,
|
||||
ObjectUpdatesActionTypes,
|
||||
ReinstateObjectUpdatesAction,
|
||||
@@ -12,9 +12,7 @@ import {
|
||||
SetValidFieldUpdateAction,
|
||||
SelectVirtualMetadataAction,
|
||||
} from './object-updates.actions';
|
||||
import { hasNoValue, hasValue, isEmpty, isNotEmpty } from '../../../shared/empty.util';
|
||||
import { moveItemInArray, transferArrayItem } from '@angular/cdk/drag-drop';
|
||||
import { from } from 'rxjs/internal/observable/from';
|
||||
import { hasNoValue, hasValue } from '../../../shared/empty.util';
|
||||
import {Relationship} from '../../shared/item-relationships/relationship.model';
|
||||
|
||||
/**
|
||||
@@ -83,20 +81,6 @@ export interface DeleteRelationship extends Relationship {
|
||||
keepRightVirtualMetadata: boolean,
|
||||
}
|
||||
|
||||
/**
|
||||
* A custom order given to the list of objects
|
||||
*/
|
||||
export interface CustomOrder {
|
||||
initialOrderPages: OrderPage[],
|
||||
newOrderPages: OrderPage[],
|
||||
pageSize: number;
|
||||
changed: boolean
|
||||
}
|
||||
|
||||
export interface OrderPage {
|
||||
order: string[]
|
||||
}
|
||||
|
||||
/**
|
||||
* The updated state of a single page
|
||||
*/
|
||||
@@ -105,7 +89,6 @@ export interface ObjectUpdatesEntry {
|
||||
fieldUpdates: FieldUpdates;
|
||||
virtualMetadataSources: VirtualMetadataSources;
|
||||
lastModified: Date;
|
||||
customOrder: CustomOrder
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -138,9 +121,6 @@ export function objectUpdatesReducer(state = initialState, action: ObjectUpdates
|
||||
case ObjectUpdatesActionTypes.INITIALIZE_FIELDS: {
|
||||
return initializeFieldsUpdate(state, action as InitializeFieldsAction);
|
||||
}
|
||||
case ObjectUpdatesActionTypes.ADD_PAGE_TO_CUSTOM_ORDER: {
|
||||
return addPageToCustomOrder(state, action as AddPageToCustomOrderAction);
|
||||
}
|
||||
case ObjectUpdatesActionTypes.ADD_FIELD: {
|
||||
return addFieldUpdate(state, action as AddFieldUpdateAction);
|
||||
}
|
||||
@@ -168,9 +148,6 @@ export function objectUpdatesReducer(state = initialState, action: ObjectUpdates
|
||||
case ObjectUpdatesActionTypes.SET_VALID_FIELD: {
|
||||
return setValidFieldUpdate(state, action as SetValidFieldUpdateAction);
|
||||
}
|
||||
case ObjectUpdatesActionTypes.MOVE: {
|
||||
return moveFieldUpdate(state, action as MoveFieldUpdateAction);
|
||||
}
|
||||
default: {
|
||||
return state;
|
||||
}
|
||||
@@ -186,50 +163,18 @@ function initializeFieldsUpdate(state: any, action: InitializeFieldsAction) {
|
||||
const url: string = action.payload.url;
|
||||
const fields: Identifiable[] = action.payload.fields;
|
||||
const lastModifiedServer: Date = action.payload.lastModified;
|
||||
const order = action.payload.order;
|
||||
const pageSize = action.payload.pageSize;
|
||||
const page = action.payload.page;
|
||||
const fieldStates = createInitialFieldStates(fields);
|
||||
const initialOrderPages = addOrderToPages([], order, pageSize, page);
|
||||
const newPageState = Object.assign(
|
||||
{},
|
||||
state[url],
|
||||
{ fieldStates: fieldStates },
|
||||
{ fieldUpdates: {} },
|
||||
{ virtualMetadataSources: {} },
|
||||
{ lastModified: lastModifiedServer },
|
||||
{ customOrder: {
|
||||
initialOrderPages: initialOrderPages,
|
||||
newOrderPages: initialOrderPages,
|
||||
pageSize: pageSize,
|
||||
changed: false }
|
||||
}
|
||||
{ lastModified: lastModifiedServer }
|
||||
);
|
||||
return Object.assign({}, state, { [url]: newPageState });
|
||||
}
|
||||
|
||||
/**
|
||||
* Add a page of objects to the state of a specific url and update a specific page of the custom order
|
||||
* @param state The current state
|
||||
* @param action The action to perform on the current state
|
||||
*/
|
||||
function addPageToCustomOrder(state: any, action: AddPageToCustomOrderAction) {
|
||||
const url: string = action.payload.url;
|
||||
const fields: Identifiable[] = action.payload.fields;
|
||||
const fieldStates = createInitialFieldStates(fields);
|
||||
const order = action.payload.order;
|
||||
const page = action.payload.page;
|
||||
const pageState: ObjectUpdatesEntry = state[url] || {};
|
||||
const newPageState = Object.assign({}, pageState, {
|
||||
fieldStates: Object.assign({}, pageState.fieldStates, fieldStates),
|
||||
customOrder: Object.assign({}, pageState.customOrder, {
|
||||
newOrderPages: addOrderToPages(pageState.customOrder.newOrderPages, order, pageState.customOrder.pageSize, page),
|
||||
initialOrderPages: addOrderToPages(pageState.customOrder.initialOrderPages, order, pageState.customOrder.pageSize, page)
|
||||
})
|
||||
});
|
||||
return Object.assign({}, state, { [url]: newPageState });
|
||||
}
|
||||
|
||||
/**
|
||||
* Add a new update for a specific field to the store
|
||||
* @param state The current state
|
||||
@@ -338,19 +283,9 @@ function discardObjectUpdatesFor(url: string, state: any) {
|
||||
}
|
||||
});
|
||||
|
||||
const newCustomOrder = Object.assign({}, pageState.customOrder);
|
||||
if (pageState.customOrder.changed) {
|
||||
const initialOrder = pageState.customOrder.initialOrderPages;
|
||||
if (isNotEmpty(initialOrder)) {
|
||||
newCustomOrder.newOrderPages = initialOrder;
|
||||
newCustomOrder.changed = false;
|
||||
}
|
||||
}
|
||||
|
||||
const discardedPageState = Object.assign({}, pageState, {
|
||||
fieldUpdates: {},
|
||||
fieldStates: newFieldStates,
|
||||
customOrder: newCustomOrder
|
||||
fieldStates: newFieldStates
|
||||
});
|
||||
return Object.assign({}, state, { [url]: discardedPageState }, { [url + OBJECT_UPDATES_TRASH_PATH]: pageState });
|
||||
}
|
||||
@@ -504,121 +439,3 @@ function createInitialFieldStates(fields: Identifiable[]) {
|
||||
uuids.forEach((uuid: string) => fieldStates[uuid] = initialFieldState);
|
||||
return fieldStates;
|
||||
}
|
||||
|
||||
/**
|
||||
* Method to add a list of objects to an existing FieldStates object
|
||||
* @param fieldStates FieldStates to add states to
|
||||
* @param fields Identifiable objects The list of objects to add to the FieldStates
|
||||
*/
|
||||
function addFieldStates(fieldStates: FieldStates, fields: Identifiable[]) {
|
||||
const uuids = fields.map((field: Identifiable) => field.uuid);
|
||||
uuids.forEach((uuid: string) => fieldStates[uuid] = initialFieldState);
|
||||
return fieldStates;
|
||||
}
|
||||
|
||||
/**
|
||||
* Move an object within the custom order of a page state
|
||||
* @param state The current state
|
||||
* @param action The move action to perform
|
||||
*/
|
||||
function moveFieldUpdate(state: any, action: MoveFieldUpdateAction) {
|
||||
const url = action.payload.url;
|
||||
const fromIndex = action.payload.from;
|
||||
const toIndex = action.payload.to;
|
||||
const fromPage = action.payload.fromPage;
|
||||
const toPage = action.payload.toPage;
|
||||
const field = action.payload.field;
|
||||
|
||||
const pageState: ObjectUpdatesEntry = state[url];
|
||||
const initialOrderPages = pageState.customOrder.initialOrderPages;
|
||||
const customOrderPages = [...pageState.customOrder.newOrderPages];
|
||||
|
||||
// Create a copy of the custom orders for the from- and to-pages
|
||||
const fromPageOrder = [...customOrderPages[fromPage].order];
|
||||
const toPageOrder = [...customOrderPages[toPage].order];
|
||||
if (fromPage === toPage) {
|
||||
if (isNotEmpty(customOrderPages[fromPage]) && isNotEmpty(customOrderPages[fromPage].order[fromIndex]) && isNotEmpty(customOrderPages[fromPage].order[toIndex])) {
|
||||
// Move an item from one index to another within the same page
|
||||
moveItemInArray(fromPageOrder, fromIndex, toIndex);
|
||||
// Update the custom order for this page
|
||||
customOrderPages[fromPage] = { order: fromPageOrder };
|
||||
}
|
||||
} else {
|
||||
if (isNotEmpty(customOrderPages[fromPage]) && hasValue(customOrderPages[toPage]) && isNotEmpty(customOrderPages[fromPage].order[fromIndex])) {
|
||||
// Move an item from one index of one page to an index in another page
|
||||
transferArrayItem(fromPageOrder, toPageOrder, fromIndex, toIndex);
|
||||
// Update the custom order for both pages
|
||||
customOrderPages[fromPage] = { order: fromPageOrder };
|
||||
customOrderPages[toPage] = { order: toPageOrder };
|
||||
}
|
||||
}
|
||||
|
||||
// Create a field update if it doesn't exist for this field yet
|
||||
let fieldUpdate = {};
|
||||
if (hasValue(field)) {
|
||||
fieldUpdate = pageState.fieldUpdates[field.uuid];
|
||||
if (hasNoValue(fieldUpdate)) {
|
||||
fieldUpdate = { field: field, changeType: undefined }
|
||||
}
|
||||
}
|
||||
|
||||
// Update the store's state with new values and return
|
||||
return Object.assign({}, state, { [url]: Object.assign({}, pageState, {
|
||||
fieldUpdates: Object.assign({}, pageState.fieldUpdates, hasValue(field) ? { [field.uuid]: fieldUpdate } : {}),
|
||||
customOrder: Object.assign({}, pageState.customOrder, { newOrderPages: customOrderPages, changed: checkForOrderChanges(initialOrderPages, customOrderPages) })
|
||||
})})
|
||||
}
|
||||
|
||||
/**
|
||||
* Compare two lists of OrderPage objects and return whether there's at least one change in the order of objects within
|
||||
* @param initialOrderPages The initial list of OrderPages
|
||||
* @param customOrderPages The changed list of OrderPages
|
||||
*/
|
||||
function checkForOrderChanges(initialOrderPages: OrderPage[], customOrderPages: OrderPage[]) {
|
||||
let changed = false;
|
||||
initialOrderPages.forEach((orderPage: OrderPage, page: number) => {
|
||||
if (isNotEmpty(orderPage) && isNotEmpty(orderPage.order) && isNotEmpty(customOrderPages[page]) && isNotEmpty(customOrderPages[page].order)) {
|
||||
orderPage.order.forEach((id: string, index: number) => {
|
||||
if (id !== customOrderPages[page].order[index]) {
|
||||
changed = true;
|
||||
return;
|
||||
}
|
||||
});
|
||||
if (changed) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
});
|
||||
return changed;
|
||||
}
|
||||
|
||||
/**
|
||||
* Initialize a custom order page by providing the list of all pages, a list of UUIDs, pageSize and the page to populate
|
||||
* @param initialPages The initial list of OrderPage objects
|
||||
* @param order The list of UUIDs to create a page for
|
||||
* @param pageSize The pageSize used to populate empty spacer pages
|
||||
* @param page The index of the page to add
|
||||
*/
|
||||
function addOrderToPages(initialPages: OrderPage[], order: string[], pageSize: number, page: number): OrderPage[] {
|
||||
const result = [...initialPages];
|
||||
const orderPage: OrderPage = { order: order };
|
||||
if (page < result.length) {
|
||||
// The page we're trying to add already exists in the list. Overwrite it.
|
||||
result[page] = orderPage;
|
||||
} else if (page === result.length) {
|
||||
// The page we're trying to add is the next page in the list, add it.
|
||||
result.push(orderPage);
|
||||
} else {
|
||||
// The page we're trying to add is at least one page ahead of the list, fill the list with empty pages before adding the page.
|
||||
const emptyOrder = [];
|
||||
for (let i = 0; i < pageSize; i++) {
|
||||
emptyOrder.push(undefined);
|
||||
}
|
||||
const emptyOrderPage: OrderPage = { order: emptyOrder };
|
||||
for (let i = result.length; i < page; i++) {
|
||||
result.push(emptyOrderPage);
|
||||
}
|
||||
result.push(orderPage);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
@@ -2,7 +2,6 @@ import { Store } from '@ngrx/store';
|
||||
import { CoreState } from '../../core.reducers';
|
||||
import { ObjectUpdatesService } from './object-updates.service';
|
||||
import {
|
||||
AddPageToCustomOrderAction,
|
||||
DiscardObjectUpdatesAction,
|
||||
FieldChangeType,
|
||||
InitializeFieldsAction, ReinstateObjectUpdatesAction, RemoveFieldUpdateAction, SelectVirtualMetadataAction,
|
||||
@@ -13,8 +12,6 @@ import { Notification } from '../../../shared/notifications/models/notification.
|
||||
import { NotificationType } from '../../../shared/notifications/models/notification-type';
|
||||
import { OBJECT_UPDATES_TRASH_PATH } from './object-updates.reducer';
|
||||
import {Relationship} from '../../shared/item-relationships/relationship.model';
|
||||
import { MoveOperation } from 'fast-json-patch/lib/core';
|
||||
import { ArrayMoveChangeAnalyzer } from '../array-move-change-analyzer.service';
|
||||
|
||||
describe('ObjectUpdatesService', () => {
|
||||
let service: ObjectUpdatesService;
|
||||
@@ -47,7 +44,7 @@ describe('ObjectUpdatesService', () => {
|
||||
};
|
||||
store = new Store<CoreState>(undefined, undefined, undefined);
|
||||
spyOn(store, 'dispatch');
|
||||
service = new ObjectUpdatesService(store, new ArrayMoveChangeAnalyzer<string>());
|
||||
service = new ObjectUpdatesService(store);
|
||||
|
||||
spyOn(service as any, 'getObjectEntry').and.returnValue(observableOf(objectEntry));
|
||||
spyOn(service as any, 'getFieldState').and.callFake((uuid) => {
|
||||
@@ -63,25 +60,6 @@ describe('ObjectUpdatesService', () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe('initializeWithCustomOrder', () => {
|
||||
const pageSize = 20;
|
||||
const page = 0;
|
||||
|
||||
it('should dispatch an INITIALIZE action with the correct URL, initial identifiables, last modified , custom order, page size and page', () => {
|
||||
service.initializeWithCustomOrder(url, identifiables, modDate, pageSize, page);
|
||||
expect(store.dispatch).toHaveBeenCalledWith(new InitializeFieldsAction(url, identifiables, modDate, identifiables.map((identifiable) => identifiable.uuid), pageSize, page));
|
||||
});
|
||||
});
|
||||
|
||||
describe('addPageToCustomOrder', () => {
|
||||
const page = 2;
|
||||
|
||||
it('should dispatch an ADD_PAGE_TO_CUSTOM_ORDER action with the correct URL, identifiables, custom order and page number to add', () => {
|
||||
service.addPageToCustomOrder(url, identifiables, page);
|
||||
expect(store.dispatch).toHaveBeenCalledWith(new AddPageToCustomOrderAction(url, identifiables, identifiables.map((identifiable) => identifiable.uuid), page));
|
||||
});
|
||||
});
|
||||
|
||||
describe('getFieldUpdates', () => {
|
||||
it('should return the list of all fields, including their update if there is one', () => {
|
||||
const result$ = service.getFieldUpdates(url, identifiables);
|
||||
@@ -116,49 +94,6 @@ describe('ObjectUpdatesService', () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe('getFieldUpdatesByCustomOrder', () => {
|
||||
beforeEach(() => {
|
||||
const fieldStates = {
|
||||
[identifiable1.uuid]: { editable: false, isNew: false, isValid: true },
|
||||
[identifiable2.uuid]: { editable: true, isNew: false, isValid: false },
|
||||
[identifiable3.uuid]: { editable: true, isNew: true, isValid: true },
|
||||
};
|
||||
|
||||
const customOrder = {
|
||||
initialOrderPages: [{
|
||||
order: [identifiable1.uuid, identifiable2.uuid, identifiable3.uuid]
|
||||
}],
|
||||
newOrderPages: [{
|
||||
order: [identifiable2.uuid, identifiable3.uuid, identifiable1.uuid]
|
||||
}],
|
||||
pageSize: 20,
|
||||
changed: true
|
||||
};
|
||||
|
||||
const objectEntry = {
|
||||
fieldStates, fieldUpdates, lastModified: modDate, virtualMetadataSources: {}, customOrder
|
||||
};
|
||||
|
||||
(service as any).getObjectEntry.and.returnValue(observableOf(objectEntry))
|
||||
});
|
||||
|
||||
it('should return the list of all fields, including their update if there is one, ordered by their custom order', (done) => {
|
||||
const result$ = service.getFieldUpdatesByCustomOrder(url, identifiables);
|
||||
expect((service as any).getObjectEntry).toHaveBeenCalledWith(url);
|
||||
|
||||
const expectedResult = {
|
||||
[identifiable2.uuid]: { field: identifiable2, changeType: undefined },
|
||||
[identifiable3.uuid]: { field: identifiable3, changeType: FieldChangeType.ADD },
|
||||
[identifiable1.uuid]: { field: identifiable1Updated, changeType: FieldChangeType.UPDATE }
|
||||
};
|
||||
|
||||
result$.subscribe((result) => {
|
||||
expect(result).toEqual(expectedResult);
|
||||
done();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('isEditable', () => {
|
||||
it('should return false if this identifiable is currently not editable in the store', () => {
|
||||
const result$ = service.isEditable(url, identifiable1.uuid);
|
||||
@@ -274,11 +209,7 @@ describe('ObjectUpdatesService', () => {
|
||||
});
|
||||
describe('when updates are emtpy', () => {
|
||||
beforeEach(() => {
|
||||
(service as any).getObjectEntry.and.returnValue(observableOf({
|
||||
customOrder: {
|
||||
changed: false
|
||||
}
|
||||
}))
|
||||
(service as any).getObjectEntry.and.returnValue(observableOf({}))
|
||||
});
|
||||
|
||||
it('should return false when there are no updates', () => {
|
||||
@@ -346,44 +277,4 @@ describe('ObjectUpdatesService', () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe('getMoveOperations', () => {
|
||||
beforeEach(() => {
|
||||
const fieldStates = {
|
||||
[identifiable1.uuid]: { editable: false, isNew: false, isValid: true },
|
||||
[identifiable2.uuid]: { editable: true, isNew: false, isValid: false },
|
||||
[identifiable3.uuid]: { editable: true, isNew: true, isValid: true },
|
||||
};
|
||||
|
||||
const customOrder = {
|
||||
initialOrderPages: [{
|
||||
order: [identifiable1.uuid, identifiable2.uuid, identifiable3.uuid]
|
||||
}],
|
||||
newOrderPages: [{
|
||||
order: [identifiable2.uuid, identifiable3.uuid, identifiable1.uuid]
|
||||
}],
|
||||
pageSize: 20,
|
||||
changed: true
|
||||
};
|
||||
|
||||
const objectEntry = {
|
||||
fieldStates, fieldUpdates, lastModified: modDate, virtualMetadataSources: {}, customOrder
|
||||
};
|
||||
|
||||
(service as any).getObjectEntry.and.returnValue(observableOf(objectEntry))
|
||||
});
|
||||
|
||||
it('should return the expected move operations', (done) => {
|
||||
const result$ = service.getMoveOperations(url);
|
||||
|
||||
const expectedResult = [
|
||||
{ op: 'move', from: '/0', path: '/2' }
|
||||
] as MoveOperation[];
|
||||
|
||||
result$.subscribe((result) => {
|
||||
expect(result).toEqual(expectedResult);
|
||||
done();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
});
|
||||
|
@@ -8,16 +8,15 @@ import {
|
||||
Identifiable,
|
||||
OBJECT_UPDATES_TRASH_PATH,
|
||||
ObjectUpdatesEntry,
|
||||
ObjectUpdatesState, OrderPage,
|
||||
ObjectUpdatesState,
|
||||
VirtualMetadataSource
|
||||
} from './object-updates.reducer';
|
||||
import { Observable } from 'rxjs';
|
||||
import {
|
||||
AddFieldUpdateAction, AddPageToCustomOrderAction,
|
||||
AddFieldUpdateAction,
|
||||
DiscardObjectUpdatesAction,
|
||||
FieldChangeType,
|
||||
InitializeFieldsAction,
|
||||
MoveFieldUpdateAction,
|
||||
ReinstateObjectUpdatesAction,
|
||||
RemoveFieldUpdateAction,
|
||||
SelectVirtualMetadataAction,
|
||||
@@ -25,11 +24,8 @@ import {
|
||||
SetValidFieldUpdateAction
|
||||
} from './object-updates.actions';
|
||||
import { distinctUntilChanged, filter, map, switchMap } from 'rxjs/operators';
|
||||
import { hasNoValue, hasValue, isEmpty, isNotEmpty } from '../../../shared/empty.util';
|
||||
import { hasNoValue, hasValue, isEmpty, isNotEmpty, isNotEmptyOperator } from '../../../shared/empty.util';
|
||||
import { INotification } from '../../../shared/notifications/models/notification.model';
|
||||
import { ArrayMoveChangeAnalyzer } from '../array-move-change-analyzer.service';
|
||||
import { MoveOperation } from 'fast-json-patch/lib/core';
|
||||
import { flatten } from '@angular/compiler';
|
||||
|
||||
function objectUpdatesStateSelector(): MemoizedSelector<CoreState, ObjectUpdatesState> {
|
||||
return createSelector(coreSelector, (state: CoreState) => state['cache/object-updates']);
|
||||
@@ -52,9 +48,7 @@ function virtualMetadataSourceSelector(url: string, source: string): MemoizedSel
|
||||
*/
|
||||
@Injectable()
|
||||
export class ObjectUpdatesService {
|
||||
constructor(private store: Store<CoreState>,
|
||||
private comparator: ArrayMoveChangeAnalyzer<string>) {
|
||||
|
||||
constructor(private store: Store<CoreState>) {
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -67,28 +61,6 @@ export class ObjectUpdatesService {
|
||||
this.store.dispatch(new InitializeFieldsAction(url, fields, lastModified));
|
||||
}
|
||||
|
||||
/**
|
||||
* Method to dispatch an InitializeFieldsAction to the store and keeping track of the order objects are stored
|
||||
* @param url The page's URL for which the changes are being mapped
|
||||
* @param fields The initial fields for the page's object
|
||||
* @param lastModified The date the object was last modified
|
||||
* @param pageSize The page size to use for adding pages to the custom order
|
||||
* @param page The first page to populate the custom order with
|
||||
*/
|
||||
initializeWithCustomOrder(url, fields: Identifiable[], lastModified: Date, pageSize = 9999, page = 0): void {
|
||||
this.store.dispatch(new InitializeFieldsAction(url, fields, lastModified, fields.map((field) => field.uuid), pageSize, page));
|
||||
}
|
||||
|
||||
/**
|
||||
* Method to dispatch an AddPageToCustomOrderAction, adding a new page to an already existing custom order tracking
|
||||
* @param url The URL for which the changes are being mapped
|
||||
* @param fields The fields to add a new page for
|
||||
* @param page The page number (starting from index 0)
|
||||
*/
|
||||
addPageToCustomOrder(url, fields: Identifiable[], page: number): void {
|
||||
this.store.dispatch(new AddPageToCustomOrderAction(url, fields, fields.map((field) => field.uuid), page));
|
||||
}
|
||||
|
||||
/**
|
||||
* Method to dispatch an AddFieldUpdateAction to the store
|
||||
* @param url The page's URL for which the changes are saved
|
||||
@@ -153,7 +125,7 @@ export class ObjectUpdatesService {
|
||||
*/
|
||||
getFieldUpdatesExclusive(url: string, initialFields: Identifiable[]): Observable<FieldUpdates> {
|
||||
const objectUpdates = this.getObjectEntry(url);
|
||||
return objectUpdates.pipe(map((objectEntry) => {
|
||||
return objectUpdates.pipe(isNotEmptyOperator(), map((objectEntry) => {
|
||||
const fieldUpdates: FieldUpdates = {};
|
||||
for (const object of initialFields) {
|
||||
let fieldUpdate = objectEntry.fieldUpdates[object.uuid];
|
||||
@@ -166,31 +138,6 @@ export class ObjectUpdatesService {
|
||||
}))
|
||||
}
|
||||
|
||||
/**
|
||||
* Method that combines the state's updates with the initial values (when there's no update),
|
||||
* sorted by their custom order to create a FieldUpdates object
|
||||
* @param url The URL of the page for which the FieldUpdates should be requested
|
||||
* @param initialFields The initial values of the fields
|
||||
* @param page The page to retrieve
|
||||
*/
|
||||
getFieldUpdatesByCustomOrder(url: string, initialFields: Identifiable[], page = 0): Observable<FieldUpdates> {
|
||||
const objectUpdates = this.getObjectEntry(url);
|
||||
return objectUpdates.pipe(map((objectEntry) => {
|
||||
const fieldUpdates: FieldUpdates = {};
|
||||
if (hasValue(objectEntry) && hasValue(objectEntry.customOrder) && isNotEmpty(objectEntry.customOrder.newOrderPages) && page < objectEntry.customOrder.newOrderPages.length) {
|
||||
for (const uuid of objectEntry.customOrder.newOrderPages[page].order) {
|
||||
let fieldUpdate = objectEntry.fieldUpdates[uuid];
|
||||
if (isEmpty(fieldUpdate)) {
|
||||
const identifiable = initialFields.find((object: Identifiable) => object.uuid === uuid);
|
||||
fieldUpdate = {field: identifiable, changeType: undefined};
|
||||
}
|
||||
fieldUpdates[uuid] = fieldUpdate;
|
||||
}
|
||||
}
|
||||
return fieldUpdates;
|
||||
}))
|
||||
}
|
||||
|
||||
/**
|
||||
* Method to check if a specific field is currently editable in the store
|
||||
* @param url The URL of the page on which the field resides
|
||||
@@ -260,19 +207,6 @@ export class ObjectUpdatesService {
|
||||
this.saveFieldUpdate(url, field, FieldChangeType.UPDATE);
|
||||
}
|
||||
|
||||
/**
|
||||
* Dispatches a MoveFieldUpdateAction
|
||||
* @param url The page's URL for which the changes are saved
|
||||
* @param from The index of the object to move
|
||||
* @param to The index to move the object to
|
||||
* @param fromPage The page to move the object from
|
||||
* @param toPage The page to move the object to
|
||||
* @param field Optional field to add to the fieldUpdates list (useful if we want to track updates across multiple pages)
|
||||
*/
|
||||
saveMoveFieldUpdate(url: string, from: number, to: number, fromPage = 0, toPage = 0, field?: Identifiable) {
|
||||
this.store.dispatch(new MoveFieldUpdateAction(url, from, to, fromPage, toPage, field));
|
||||
}
|
||||
|
||||
/**
|
||||
* Check whether the virtual metadata of a given item is selected to be saved as real metadata
|
||||
* @param url The URL of the page on which the field resides
|
||||
@@ -387,7 +321,7 @@ export class ObjectUpdatesService {
|
||||
* @param url The page's url to check for in the store
|
||||
*/
|
||||
hasUpdates(url: string): Observable<boolean> {
|
||||
return this.getObjectEntry(url).pipe(map((objectEntry) => hasValue(objectEntry) && (isNotEmpty(objectEntry.fieldUpdates) || objectEntry.customOrder.changed)));
|
||||
return this.getObjectEntry(url).pipe(map((objectEntry) => hasValue(objectEntry) && isNotEmpty(objectEntry.fieldUpdates)));
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -405,19 +339,4 @@ export class ObjectUpdatesService {
|
||||
getLastModified(url: string): Observable<Date> {
|
||||
return this.getObjectEntry(url).pipe(map((entry: ObjectUpdatesEntry) => entry.lastModified));
|
||||
}
|
||||
|
||||
/**
|
||||
* Get move operations based on the custom order
|
||||
* @param url The page's url
|
||||
*/
|
||||
getMoveOperations(url: string): Observable<MoveOperation[]> {
|
||||
return this.getObjectEntry(url).pipe(
|
||||
map((objectEntry) => objectEntry.customOrder),
|
||||
map((customOrder) => this.comparator.diff(
|
||||
flatten(customOrder.initialOrderPages.map((orderPage: OrderPage) => orderPage.order)),
|
||||
flatten(customOrder.newOrderPages.map((orderPage: OrderPage) => orderPage.order)))
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
}
|
||||
|
@@ -201,8 +201,9 @@ export class RequestService {
|
||||
* Remove all request cache providing (part of) the href
|
||||
* This also includes href-to-uuid index cache
|
||||
* @param href A substring of the request(s) href
|
||||
* @return Returns an observable emitting whether or not the cache is removed
|
||||
*/
|
||||
removeByHrefSubstring(href: string) {
|
||||
removeByHrefSubstring(href: string): Observable<boolean> {
|
||||
this.store.pipe(
|
||||
select(uuidsFromHrefSubstringSelector(requestIndexSelector, href)),
|
||||
take(1)
|
||||
@@ -213,6 +214,11 @@ export class RequestService {
|
||||
});
|
||||
this.requestsOnTheirWayToTheStore = this.requestsOnTheirWayToTheStore.filter((reqHref: string) => reqHref.indexOf(href) < 0);
|
||||
this.indexStore.dispatch(new RemoveFromIndexBySubstringAction(IndexName.REQUEST, href));
|
||||
|
||||
return this.store.pipe(
|
||||
select(uuidsFromHrefSubstringSelector(requestIndexSelector, href)),
|
||||
map((uuids) => isEmpty(uuids))
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
|
@@ -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, 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({
|
||||
selector: 'ds-create-item-parent-selector',
|
||||
// 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 {
|
||||
objectType = DSpaceObjectType.ITEM;
|
||||
|
@@ -11,7 +11,7 @@ export function getMockRequestService(requestEntry$: Observable<RequestEntry> =
|
||||
getByUUID: requestEntry$,
|
||||
uriEncodeBody: jasmine.createSpy('uriEncodeBody'),
|
||||
isCachedOrPending: false,
|
||||
removeByHrefSubstring: jasmine.createSpy('removeByHrefSubstring'),
|
||||
removeByHrefSubstring: observableOf(true),
|
||||
hasByHrefObservable: observableOf(false)
|
||||
});
|
||||
}
|
||||
|
@@ -75,7 +75,7 @@ export class NotificationsService {
|
||||
this.translate.get(hrefTranslateLabel)
|
||||
.pipe(first())
|
||||
.subscribe((hrefMsg) => {
|
||||
const anchor = `<a class="btn btn-link p-0 m-0" href="${href}" >
|
||||
const anchor = `<a class="align-baseline btn btn-link p-0 m-0" href="${href}" >
|
||||
<strong>${hrefMsg}</strong>
|
||||
</a>`;
|
||||
const interpolateParams = Object.create({});
|
||||
|
@@ -12,14 +12,16 @@ import { take } from 'rxjs/operators';
|
||||
import { PaginationComponent } from '../pagination/pagination.component';
|
||||
import { createSuccessfulRemoteDataObject } from '../remote-data.utils';
|
||||
import { createPaginatedList } from '../testing/utils.test';
|
||||
import { ObjectValuesPipe } from '../utils/object-values-pipe';
|
||||
|
||||
class MockAbstractPaginatedDragAndDropListComponent extends AbstractPaginatedDragAndDropListComponent<DSpaceObject> {
|
||||
|
||||
constructor(protected objectUpdatesService: ObjectUpdatesService,
|
||||
protected elRef: ElementRef,
|
||||
protected objectValuesPipe: ObjectValuesPipe,
|
||||
protected mockUrl: string,
|
||||
protected mockObjectsRD$: Observable<RemoteData<PaginatedList<DSpaceObject>>>) {
|
||||
super(objectUpdatesService, elRef);
|
||||
super(objectUpdatesService, elRef, objectValuesPipe);
|
||||
}
|
||||
|
||||
initializeObjectsRD(): void {
|
||||
@@ -35,6 +37,7 @@ describe('AbstractPaginatedDragAndDropListComponent', () => {
|
||||
let component: MockAbstractPaginatedDragAndDropListComponent;
|
||||
let objectUpdatesService: ObjectUpdatesService;
|
||||
let elRef: ElementRef;
|
||||
let objectValuesPipe: ObjectValuesPipe;
|
||||
|
||||
const url = 'mock-abstract-paginated-drag-and-drop-list-component';
|
||||
|
||||
@@ -52,32 +55,26 @@ describe('AbstractPaginatedDragAndDropListComponent', () => {
|
||||
|
||||
beforeEach(() => {
|
||||
objectUpdatesService = jasmine.createSpyObj('objectUpdatesService', {
|
||||
initializeWithCustomOrder: {},
|
||||
addPageToCustomOrder: {},
|
||||
getFieldUpdatesByCustomOrder: observableOf(updates),
|
||||
saveMoveFieldUpdate: {}
|
||||
initialize: {},
|
||||
getFieldUpdatesExclusive: observableOf(updates)
|
||||
});
|
||||
elRef = {
|
||||
nativeElement: jasmine.createSpyObj('nativeElement', {
|
||||
querySelector: {}
|
||||
})
|
||||
};
|
||||
objectValuesPipe = new ObjectValuesPipe();
|
||||
paginationComponent = jasmine.createSpyObj('paginationComponent', {
|
||||
doPageChange: {}
|
||||
});
|
||||
objectsRD$ = new BehaviorSubject(objectsRD);
|
||||
component = new MockAbstractPaginatedDragAndDropListComponent(objectUpdatesService, elRef, url, objectsRD$);
|
||||
component = new MockAbstractPaginatedDragAndDropListComponent(objectUpdatesService, elRef, objectValuesPipe, url, objectsRD$);
|
||||
component.paginationComponent = paginationComponent;
|
||||
component.ngOnInit();
|
||||
});
|
||||
|
||||
it('should call initializeWithCustomOrder to initialize the first page and add it to initializedPages', (done) => {
|
||||
expect(component.initializedPages.indexOf(0)).toBeLessThan(0);
|
||||
component.updates$.pipe(take(1)).subscribe(() => {
|
||||
expect(objectUpdatesService.initializeWithCustomOrder).toHaveBeenCalled();
|
||||
expect(component.initializedPages.indexOf(0)).toBeGreaterThanOrEqual(0);
|
||||
done();
|
||||
});
|
||||
it('should call initialize to initialize the objects in the store', () => {
|
||||
expect(objectUpdatesService.initialize).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should initialize the updates correctly', (done) => {
|
||||
@@ -87,43 +84,6 @@ describe('AbstractPaginatedDragAndDropListComponent', () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe('when a new page is loaded', () => {
|
||||
const page = 5;
|
||||
|
||||
beforeEach((done) => {
|
||||
component.updates$.pipe(take(1)).subscribe(() => {
|
||||
component.currentPage$.next(page);
|
||||
objectsRD$.next(objectsRD);
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('should call addPageToCustomOrder to initialize the new page and add it to initializedPages', (done) => {
|
||||
component.updates$.pipe(take(1)).subscribe(() => {
|
||||
expect(objectUpdatesService.addPageToCustomOrder).toHaveBeenCalled();
|
||||
expect(component.initializedPages.indexOf(page - 1)).toBeGreaterThanOrEqual(0);
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
describe('twice', () => {
|
||||
beforeEach((done) => {
|
||||
component.updates$.pipe(take(1)).subscribe(() => {
|
||||
component.currentPage$.next(page);
|
||||
objectsRD$.next(objectsRD);
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('shouldn\'t call addPageToCustomOrder again, as the page has already been initialized', (done) => {
|
||||
component.updates$.pipe(take(1)).subscribe(() => {
|
||||
expect(objectUpdatesService.addPageToCustomOrder).toHaveBeenCalledTimes(1);
|
||||
done();
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('switchPage', () => {
|
||||
const page = 3;
|
||||
|
||||
@@ -149,30 +109,31 @@ describe('AbstractPaginatedDragAndDropListComponent', () => {
|
||||
|
||||
beforeEach(() => {
|
||||
elRef.nativeElement.querySelector.and.returnValue(hoverElement);
|
||||
component.initializedPages.push(hoverPage - 1);
|
||||
spyOn(component.dropObject, 'emit');
|
||||
component.drop(event);
|
||||
});
|
||||
|
||||
it('should detect the page and set currentPage$ to its value', () => {
|
||||
expect(component.currentPage$.value).toEqual(hoverPage);
|
||||
});
|
||||
|
||||
it('should detect the page and update the pagination component with its value', () => {
|
||||
expect(paginationComponent.doPageChange).toHaveBeenCalledWith(hoverPage);
|
||||
});
|
||||
|
||||
it('should send out a saveMoveFieldUpdate with the correct values', () => {
|
||||
expect(objectUpdatesService.saveMoveFieldUpdate).toHaveBeenCalledWith(url, event.previousIndex, 0, 0, hoverPage - 1, object1);
|
||||
it('should send out a dropObject event with the expected processed paginated indexes', () => {
|
||||
expect(component.dropObject.emit).toHaveBeenCalledWith(Object.assign({
|
||||
fromIndex: ((component.currentPage$.value - 1) * component.pageSize) + event.previousIndex,
|
||||
toIndex: ((hoverPage - 1) * component.pageSize),
|
||||
finish: jasmine.anything()
|
||||
}));
|
||||
});
|
||||
});
|
||||
|
||||
describe('when the user is not hovering over a new page', () => {
|
||||
beforeEach(() => {
|
||||
spyOn(component.dropObject, 'emit');
|
||||
component.drop(event);
|
||||
});
|
||||
|
||||
it('should send out a saveMoveFieldUpdate with the correct values', () => {
|
||||
expect(objectUpdatesService.saveMoveFieldUpdate).toHaveBeenCalledWith(url, event.previousIndex, event.currentIndex, 0, 0);
|
||||
it('should send out a dropObject event with the expected properties', () => {
|
||||
expect(component.dropObject.emit).toHaveBeenCalledWith(Object.assign({
|
||||
fromIndex: event.previousIndex,
|
||||
toIndex: event.currentIndex,
|
||||
finish: jasmine.anything()
|
||||
}));
|
||||
});
|
||||
});
|
||||
});
|
||||
|
@@ -1,23 +1,33 @@
|
||||
import { FieldUpdates } from '../../core/data/object-updates/object-updates.reducer';
|
||||
import { FieldUpdate, FieldUpdates } from '../../core/data/object-updates/object-updates.reducer';
|
||||
import { Observable } from 'rxjs/internal/Observable';
|
||||
import { RemoteData } from '../../core/data/remote-data';
|
||||
import { PaginatedList } from '../../core/data/paginated-list';
|
||||
import { PaginationComponentOptions } from '../pagination/pagination-component-options.model';
|
||||
import { BehaviorSubject } from 'rxjs/internal/BehaviorSubject';
|
||||
import { ObjectUpdatesService } from '../../core/data/object-updates/object-updates.service';
|
||||
import { switchMap, take, tap } from 'rxjs/operators';
|
||||
import { hasValue, isEmpty, isNotEmpty } from '../empty.util';
|
||||
import { distinctUntilChanged, map, switchMap, take } from 'rxjs/operators';
|
||||
import { hasValue, isNotEmpty } from '../empty.util';
|
||||
import { paginatedListToArray } from '../../core/shared/operators';
|
||||
import { DSpaceObject } from '../../core/shared/dspace-object.model';
|
||||
import { CdkDragDrop } from '@angular/cdk/drag-drop';
|
||||
import { ElementRef, ViewChild } from '@angular/core';
|
||||
import { CdkDragDrop, moveItemInArray } from '@angular/cdk/drag-drop';
|
||||
import { ElementRef, EventEmitter, OnDestroy, Output, ViewChild } from '@angular/core';
|
||||
import { PaginationComponent } from '../pagination/pagination.component';
|
||||
import { ObjectValuesPipe } from '../utils/object-values-pipe';
|
||||
import { compareArraysUsing } from '../../+item-page/simple/item-types/shared/item-relationships-utils';
|
||||
import { Subscription } from 'rxjs/internal/Subscription';
|
||||
|
||||
/**
|
||||
* Operator used for comparing {@link FieldUpdate}s by their field's UUID
|
||||
*/
|
||||
export const compareArraysUsingFieldUuids = () =>
|
||||
compareArraysUsing((fieldUpdate: FieldUpdate) => (hasValue(fieldUpdate) && hasValue(fieldUpdate.field)) ? fieldUpdate.field.uuid : undefined);
|
||||
|
||||
/**
|
||||
* An abstract component containing general methods and logic to be able to drag and drop objects within a paginated
|
||||
* list. This implementation supports being able to drag and drop objects between pages.
|
||||
* Dragging an object on top of a page number will automatically detect the page it's being dropped on, send an update
|
||||
* to the store and add the object on top of that page.
|
||||
* Dragging an object on top of a page number will automatically detect the page it's being dropped on and send a
|
||||
* dropObject event to the parent component containing detailed information about the indexes the object was dropped from
|
||||
* and to.
|
||||
*
|
||||
* To extend this component, it is important to make sure to:
|
||||
* - Initialize objectsRD$ within the initializeObjectsRD() method
|
||||
@@ -28,12 +38,19 @@ import { PaginationComponent } from '../pagination/pagination.component';
|
||||
*
|
||||
* An example component extending from this abstract component: PaginatedDragAndDropBitstreamListComponent
|
||||
*/
|
||||
export abstract class AbstractPaginatedDragAndDropListComponent<T extends DSpaceObject> {
|
||||
export abstract class AbstractPaginatedDragAndDropListComponent<T extends DSpaceObject> implements OnDestroy {
|
||||
/**
|
||||
* A view on the child pagination component
|
||||
*/
|
||||
@ViewChild(PaginationComponent, {static: false}) paginationComponent: PaginationComponent;
|
||||
|
||||
/**
|
||||
* Send an event when the user drops an object on the pagination
|
||||
* The event contains details about the index the object came from and is dropped to (across the entirety of the list,
|
||||
* not just within a single page)
|
||||
*/
|
||||
@Output() dropObject: EventEmitter<any> = new EventEmitter<any>();
|
||||
|
||||
/**
|
||||
* The URL to use for accessing the object updates from this list
|
||||
*/
|
||||
@@ -49,6 +66,12 @@ export abstract class AbstractPaginatedDragAndDropListComponent<T extends DSpace
|
||||
*/
|
||||
updates$: Observable<FieldUpdates>;
|
||||
|
||||
/**
|
||||
* A list of object UUIDs
|
||||
* This is the order the objects will be displayed in
|
||||
*/
|
||||
customOrder: string[];
|
||||
|
||||
/**
|
||||
* The amount of objects to display per page
|
||||
*/
|
||||
@@ -70,23 +93,21 @@ export abstract class AbstractPaginatedDragAndDropListComponent<T extends DSpace
|
||||
currentPage$ = new BehaviorSubject<number>(1);
|
||||
|
||||
/**
|
||||
* A list of pages that have been initialized in the field-update store
|
||||
* Whether or not we should display a loading animation
|
||||
* This is used to display a loading page when the user drops a bitstream onto a new page. The loading animation
|
||||
* should stop once the bitstream has moved to the new page and the new page's response has loaded and contains the
|
||||
* dropped object on top (see this.stopLoadingWhenFirstIs below)
|
||||
*/
|
||||
initializedPages: number[] = [];
|
||||
loading$: BehaviorSubject<boolean> = new BehaviorSubject<boolean>(false);
|
||||
|
||||
/**
|
||||
* An object storing information about an update that should be fired whenever fireToUpdate is called
|
||||
* List of subscriptions
|
||||
*/
|
||||
toUpdate: {
|
||||
fromIndex: number,
|
||||
toIndex: number,
|
||||
fromPage: number,
|
||||
toPage: number,
|
||||
field?: T
|
||||
};
|
||||
subs: Subscription[] = [];
|
||||
|
||||
protected constructor(protected objectUpdatesService: ObjectUpdatesService,
|
||||
protected elRef: ElementRef) {
|
||||
protected elRef: ElementRef,
|
||||
protected objectValuesPipe: ObjectValuesPipe) {
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -110,28 +131,29 @@ export abstract class AbstractPaginatedDragAndDropListComponent<T extends DSpace
|
||||
|
||||
/**
|
||||
* Initialize the field-updates in the store
|
||||
* This method ensures (new) pages displayed are automatically added to the field-update store when the objectsRD updates
|
||||
*/
|
||||
initializeUpdates(): void {
|
||||
this.objectsRD$.pipe(
|
||||
paginatedListToArray(),
|
||||
take(1)
|
||||
).subscribe((objects: T[]) => {
|
||||
this.objectUpdatesService.initialize(this.url, objects, new Date());
|
||||
});
|
||||
this.updates$ = this.objectsRD$.pipe(
|
||||
paginatedListToArray(),
|
||||
tap((objects: T[]) => {
|
||||
// Pages in the field-update store are indexed starting at 0 (because they're stored in an array of pages)
|
||||
const updatesPage = this.currentPage$.value - 1;
|
||||
if (isEmpty(this.initializedPages)) {
|
||||
// No updates have been initialized yet for this list, initialize the first page
|
||||
this.objectUpdatesService.initializeWithCustomOrder(this.url, objects, new Date(), this.pageSize, updatesPage);
|
||||
this.initializedPages.push(updatesPage);
|
||||
} else if (this.initializedPages.indexOf(updatesPage) < 0) {
|
||||
// Updates were initialized for this list, but not the page we're on. Add the current page to the field-update store for this list
|
||||
this.objectUpdatesService.addPageToCustomOrder(this.url, objects, updatesPage);
|
||||
this.initializedPages.push(updatesPage);
|
||||
}
|
||||
|
||||
// The new page is loaded into the store, check if there are any updates waiting and fire those as well
|
||||
this.fireToUpdate();
|
||||
switchMap((objects: T[]) => this.objectUpdatesService.getFieldUpdatesExclusive(this.url, objects))
|
||||
);
|
||||
this.subs.push(
|
||||
this.updates$.pipe(
|
||||
map((fieldUpdates) => this.objectValuesPipe.transform(fieldUpdates)),
|
||||
distinctUntilChanged(compareArraysUsingFieldUuids())
|
||||
).subscribe((updateValues) => {
|
||||
this.customOrder = updateValues.map((fieldUpdate) => fieldUpdate.field.uuid);
|
||||
// We received new values, stop displaying a loading indicator if it's present
|
||||
this.loading$.next(false);
|
||||
}),
|
||||
switchMap((objects: T[]) => this.objectUpdatesService.getFieldUpdatesByCustomOrder(this.url, objects, this.currentPage$.value - 1))
|
||||
// Disable the pagination when objects are loading
|
||||
this.loading$.subscribe((loading) => this.options.disabled = loading)
|
||||
);
|
||||
}
|
||||
|
||||
@@ -144,52 +166,60 @@ export abstract class AbstractPaginatedDragAndDropListComponent<T extends DSpace
|
||||
}
|
||||
|
||||
/**
|
||||
* An object was moved, send updates to the store.
|
||||
* An object was moved, send updates to the dropObject EventEmitter
|
||||
* When the object is dropped on a page within the pagination of this component, the object moves to the top of that
|
||||
* page and the pagination automatically loads and switches the view to that page.
|
||||
* page and the pagination automatically loads and switches the view to that page (this is done by calling the event's
|
||||
* finish() method after sending patch requests to the REST API)
|
||||
* @param event
|
||||
*/
|
||||
drop(event: CdkDragDrop<any>) {
|
||||
const dragIndex = event.previousIndex;
|
||||
let dropIndex = event.currentIndex;
|
||||
const dragPage = this.currentPage$.value - 1;
|
||||
let dropPage = this.currentPage$.value - 1;
|
||||
|
||||
// Check if the user is hovering over any of the pagination's pages at the time of dropping the object
|
||||
const droppedOnElement = this.elRef.nativeElement.querySelector('.page-item:hover');
|
||||
if (hasValue(droppedOnElement) && hasValue(droppedOnElement.textContent)) {
|
||||
// The user is hovering over a page, fetch the page's number from the element
|
||||
const page = Number(droppedOnElement.textContent);
|
||||
if (hasValue(page) && !Number.isNaN(page)) {
|
||||
const id = event.item.element.nativeElement.id;
|
||||
this.updates$.pipe(take(1)).subscribe((updates: FieldUpdates) => {
|
||||
const field = hasValue(updates[id]) ? updates[id].field : undefined;
|
||||
this.toUpdate = Object.assign({
|
||||
fromIndex: event.previousIndex,
|
||||
toIndex: 0,
|
||||
fromPage: this.currentPage$.value - 1,
|
||||
toPage: page - 1,
|
||||
field
|
||||
});
|
||||
// Switch to the dropped-on page and force a page update for the pagination component
|
||||
this.currentPage$.next(page);
|
||||
this.paginationComponent.doPageChange(page);
|
||||
if (this.initializedPages.indexOf(page - 1) >= 0) {
|
||||
// The page the object is being dropped to has already been loaded before, directly fire an update to the store.
|
||||
// For pages that haven't been loaded before, the updates$ observable will call fireToUpdate after the new page
|
||||
// has loaded
|
||||
this.fireToUpdate();
|
||||
}
|
||||
});
|
||||
const droppedPage = Number(droppedOnElement.textContent);
|
||||
if (hasValue(droppedPage) && !Number.isNaN(droppedPage)) {
|
||||
dropPage = droppedPage - 1;
|
||||
dropIndex = 0;
|
||||
}
|
||||
} else {
|
||||
this.objectUpdatesService.saveMoveFieldUpdate(this.url, event.previousIndex, event.currentIndex, this.currentPage$.value - 1, this.currentPage$.value - 1);
|
||||
}
|
||||
|
||||
const isNewPage = dragPage !== dropPage;
|
||||
// Move the object in the custom order array if the drop happened within the same page
|
||||
// This allows us to instantly display a change in the order, instead of waiting for the REST API's response first
|
||||
if (!isNewPage && dragIndex !== dropIndex) {
|
||||
moveItemInArray(this.customOrder, dragIndex, dropIndex);
|
||||
}
|
||||
|
||||
const redirectPage = dropPage + 1;
|
||||
const fromIndex = (dragPage * this.pageSize) + dragIndex;
|
||||
const toIndex = (dropPage * this.pageSize) + dropIndex;
|
||||
// Send out a drop event (and navigate to the new page) when the "from" and "to" indexes are different from each other
|
||||
if (fromIndex !== toIndex) {
|
||||
if (isNewPage) {
|
||||
this.loading$.next(true);
|
||||
}
|
||||
this.dropObject.emit(Object.assign({
|
||||
fromIndex,
|
||||
toIndex,
|
||||
finish: () => {
|
||||
if (isNewPage) {
|
||||
this.paginationComponent.doPageChange(redirectPage);
|
||||
}
|
||||
}
|
||||
}));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Method checking if there's an update ready to be fired. Send out a MoveFieldUpdate to the store if there's an
|
||||
* update present and clear the update afterwards.
|
||||
* unsub all subscriptions
|
||||
*/
|
||||
fireToUpdate() {
|
||||
if (hasValue(this.toUpdate)) {
|
||||
this.objectUpdatesService.saveMoveFieldUpdate(this.url, this.toUpdate.fromIndex, this.toUpdate.toIndex, this.toUpdate.fromPage, this.toUpdate.toPage, this.toUpdate.field);
|
||||
this.toUpdate = undefined;
|
||||
}
|
||||
ngOnDestroy(): void {
|
||||
this.subs.filter((sub) => hasValue(sub)).forEach((sub) => sub.unsubscribe());
|
||||
}
|
||||
}
|
||||
|
@@ -203,6 +203,7 @@ import { ResourcePolicyResolver } from './resource-policies/resolvers/resource-p
|
||||
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 { FileDownloadLinkComponent } from './file-download-link/file-download-link.component';
|
||||
import { CollectionDropdownComponent } from './collection-dropdown/collection-dropdown.component';
|
||||
|
||||
const MODULES = [
|
||||
// Do NOT include UniversalModule, HttpModule, or JsonpModule here
|
||||
@@ -389,6 +390,7 @@ const COMPONENTS = [
|
||||
EpersonSearchBoxComponent,
|
||||
GroupSearchBoxComponent,
|
||||
FileDownloadLinkComponent,
|
||||
CollectionDropdownComponent
|
||||
];
|
||||
|
||||
const ENTRY_COMPONENTS = [
|
||||
@@ -507,8 +509,7 @@ const DIRECTIVES = [
|
||||
...COMPONENTS,
|
||||
...DIRECTIVES,
|
||||
...ENTRY_COMPONENTS,
|
||||
...SHARED_ITEM_PAGE_COMPONENTS,
|
||||
|
||||
...SHARED_ITEM_PAGE_COMPONENTS
|
||||
],
|
||||
providers: [
|
||||
...PROVIDERS
|
||||
|
@@ -1,5 +1,20 @@
|
||||
<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">
|
||||
<span id="collectionControlsMenuLabel" class="input-group-text">
|
||||
{{ 'submission.sections.general.collection' | translate }}
|
||||
@@ -10,7 +25,7 @@
|
||||
class="btn btn-outline-primary"
|
||||
(blur)="onClose()"
|
||||
(click)="onClose()"
|
||||
[disabled]="(disabled$ | async) || (processingChange$ | async)"
|
||||
[disabled]="(processingChange$ | async)"
|
||||
ngbDropdownToggle>
|
||||
<span *ngIf="(processingChange$ | async)"><i class='fas fa-circle-notch fa-spin'></i></span>
|
||||
<span *ngIf="!(processingChange$ | async)">{{ selectedCollectionName$ | async }}</span>
|
||||
@@ -20,31 +35,9 @@
|
||||
class="dropdown-menu"
|
||||
id="collectionControlsDropdownMenu"
|
||||
aria-labelledby="collectionControlsMenuButton">
|
||||
<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">
|
||||
</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>
|
||||
<ds-collection-dropdown
|
||||
(selectionChange)="onSelect($event)">
|
||||
</ds-collection-dropdown>
|
||||
</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 { By } from '@angular/platform-browser';
|
||||
import { FormsModule, ReactiveFormsModule } from '@angular/forms';
|
||||
|
||||
import { of as observableOf } from 'rxjs';
|
||||
import { filter } from 'rxjs/operators';
|
||||
import { TranslateModule } from '@ngx-translate/core';
|
||||
import { NgbModule } from '@ng-bootstrap/ng-bootstrap';
|
||||
import { Store } from '@ngrx/store';
|
||||
import { cold } from 'jasmine-marbles';
|
||||
|
||||
import { SubmissionServiceStub } from '../../../shared/testing/submission-service.stub';
|
||||
import { mockSubmissionId, mockSubmissionRestResponse } from '../../../shared/mocks/submission.mock';
|
||||
@@ -19,172 +16,13 @@ import { SubmissionJsonPatchOperationsService } from '../../../core/submission/s
|
||||
import { SubmissionJsonPatchOperationsServiceStub } from '../../../shared/testing/submission-json-patch-operations-service.stub';
|
||||
import { JsonPatchOperationsBuilder } from '../../../core/json-patch/builder/json-patch-operations-builder';
|
||||
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 { CollectionDataService } from '../../../core/data/collection-data.service';
|
||||
|
||||
const subcommunities = [Object.assign(new Community(), {
|
||||
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(), {
|
||||
name: 'Community 1-Collection 1',
|
||||
id: '1234567890-1',
|
||||
metadata: [
|
||||
{
|
||||
key: 'dc.title',
|
||||
language: 'en_US',
|
||||
value: 'Community 1-Collection 1'
|
||||
}]
|
||||
});
|
||||
|
||||
const mockCommunity1Collection2 = Object.assign(new Collection(), {
|
||||
name: 'Community 1-Collection 2',
|
||||
id: '1234567890-2',
|
||||
metadata: [
|
||||
{
|
||||
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 = [
|
||||
{
|
||||
communities: [
|
||||
{
|
||||
id: '123456789-1',
|
||||
name: 'Community 1'
|
||||
}
|
||||
],
|
||||
collection: {
|
||||
id: '1234567890-1',
|
||||
name: 'Community 1-Collection 1'
|
||||
}
|
||||
},
|
||||
{
|
||||
communities: [
|
||||
{
|
||||
id: '123456789-1',
|
||||
name: 'Community 1'
|
||||
}
|
||||
],
|
||||
collection: {
|
||||
id: '1234567890-2',
|
||||
name: 'Community 1-Collection 2'
|
||||
}
|
||||
},
|
||||
{
|
||||
communities: [
|
||||
{
|
||||
id: '123456789-2',
|
||||
name: 'Community 2'
|
||||
}
|
||||
],
|
||||
collection: {
|
||||
id: '1234567890-3',
|
||||
name: 'Community 2-Collection 1'
|
||||
}
|
||||
},
|
||||
{
|
||||
communities: [
|
||||
{
|
||||
id: '123456789-2',
|
||||
name: 'Community 2'
|
||||
}
|
||||
],
|
||||
collection: {
|
||||
id: '1234567890-4',
|
||||
name: 'Community 2-Collection 2'
|
||||
}
|
||||
}
|
||||
];
|
||||
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';
|
||||
|
||||
describe('SubmissionFormCollectionComponent Component', () => {
|
||||
|
||||
@@ -198,7 +36,57 @@ describe('SubmissionFormCollectionComponent Component', () => {
|
||||
const collectionId = '1234567890-1';
|
||||
const definition = 'traditional';
|
||||
const submissionRestResponse = mockSubmissionRestResponse;
|
||||
const searchedCollection = 'Community 2-Collection 2';
|
||||
|
||||
const mockCollectionList = [
|
||||
{
|
||||
communities: [
|
||||
{
|
||||
id: '123456789-1',
|
||||
name: 'Community 1'
|
||||
}
|
||||
],
|
||||
collection: {
|
||||
id: '1234567890-1',
|
||||
name: 'Community 1-Collection 1'
|
||||
}
|
||||
},
|
||||
{
|
||||
communities: [
|
||||
{
|
||||
id: '123456789-1',
|
||||
name: 'Community 1'
|
||||
}
|
||||
],
|
||||
collection: {
|
||||
id: '1234567890-2',
|
||||
name: 'Community 1-Collection 2'
|
||||
}
|
||||
},
|
||||
{
|
||||
communities: [
|
||||
{
|
||||
id: '123456789-2',
|
||||
name: 'Community 2'
|
||||
}
|
||||
],
|
||||
collection: {
|
||||
id: '1234567890-3',
|
||||
name: 'Community 2-Collection 1'
|
||||
}
|
||||
},
|
||||
{
|
||||
communities: [
|
||||
{
|
||||
id: '123456789-2',
|
||||
name: 'Community 2'
|
||||
}
|
||||
],
|
||||
collection: {
|
||||
id: '1234567890-4',
|
||||
name: 'Community 2-Collection 2'
|
||||
}
|
||||
}
|
||||
];
|
||||
|
||||
const communityDataService: any = jasmine.createSpyObj('communityDataService', {
|
||||
findAll: jasmine.createSpy('findAll')
|
||||
@@ -217,6 +105,10 @@ describe('SubmissionFormCollectionComponent Component', () => {
|
||||
replace: jasmine.createSpy('replace')
|
||||
});
|
||||
|
||||
const sectionsService: any = jasmine.createSpyObj('sectionsService', {
|
||||
isSectionAvailable: of(true)
|
||||
});
|
||||
|
||||
beforeEach(async(() => {
|
||||
TestBed.configureTestingModule({
|
||||
imports: [
|
||||
@@ -236,6 +128,7 @@ describe('SubmissionFormCollectionComponent Component', () => {
|
||||
{ provide: CommunityDataService, useValue: communityDataService },
|
||||
{ provide: JsonPatchOperationsBuilder, useValue: jsonPatchOpBuilder },
|
||||
{ provide: Store, useValue: store },
|
||||
{ provide: SectionsService, useValue: sectionsService },
|
||||
ChangeDetectorRef,
|
||||
SubmissionFormCollectionComponent
|
||||
],
|
||||
@@ -299,72 +192,11 @@ describe('SubmissionFormCollectionComponent Component', () => {
|
||||
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('', () => {
|
||||
let dropdowBtn: DebugElement;
|
||||
let dropdownMenu: DebugElement;
|
||||
|
||||
beforeEach(() => {
|
||||
|
||||
comp.searchListCollection$ = observableOf(mockCollectionList);
|
||||
fixture.detectChanges();
|
||||
dropdowBtn = fixture.debugElement.query(By.css('#collectionControlsMenuButton'));
|
||||
dropdownMenu = fixture.debugElement.query(By.css('#collectionControlsDropdownMenu'));
|
||||
@@ -387,49 +219,46 @@ describe('SubmissionFormCollectionComponent Component', () => {
|
||||
fixture.whenStable().then(() => {
|
||||
expect(comp.onClose).toHaveBeenCalled();
|
||||
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'));
|
||||
expect(dropDown).toBeTruthy();
|
||||
});
|
||||
|
||||
spyOn(comp, 'onSelect');
|
||||
dropdowBtn.triggerEventHandler('click', null);
|
||||
tick();
|
||||
it('the dropdown menu should be disabled', () => {
|
||||
comp.available$ = of(false);
|
||||
fixture.detectChanges();
|
||||
const dropDown = fixture.debugElement.query(By.css('#collectionControlsDropdownMenu'));
|
||||
expect(dropDown).toBeFalsy();
|
||||
});
|
||||
|
||||
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();
|
||||
|
||||
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(() => {
|
||||
|
||||
dropdowBtn.triggerEventHandler('click', null);
|
||||
tick();
|
||||
fixture.detectChanges();
|
||||
|
||||
fixture.whenStable().then(() => {
|
||||
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);
|
||||
});
|
||||
}));
|
||||
|
||||
expect(submissionServiceStub.changeSubmissionCollection).toHaveBeenCalled();
|
||||
expect(comp.selectedCollectionId).toBe(mockCollectionList[1].collection.id);
|
||||
expect(comp.selectedCollectionName$).toBeObservable(cold('(a|)', {
|
||||
a: mockCollectionList[1].collection.name
|
||||
}));
|
||||
});
|
||||
});
|
||||
|
||||
});
|
||||
|
@@ -2,57 +2,31 @@ import {
|
||||
ChangeDetectorRef,
|
||||
Component,
|
||||
EventEmitter,
|
||||
HostListener,
|
||||
Input,
|
||||
OnChanges,
|
||||
OnInit,
|
||||
Output,
|
||||
SimpleChanges
|
||||
SimpleChanges,
|
||||
ViewChild
|
||||
} 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 {
|
||||
debounceTime,
|
||||
distinctUntilChanged,
|
||||
filter,
|
||||
find,
|
||||
flatMap,
|
||||
map,
|
||||
mergeMap,
|
||||
reduce,
|
||||
startWith
|
||||
map
|
||||
} from 'rxjs/operators';
|
||||
|
||||
import { Collection } from '../../../core/shared/collection.model';
|
||||
import { CommunityDataService } from '../../../core/data/community-data.service';
|
||||
import { Community } from '../../../core/shared/community.model';
|
||||
import { hasValue, isEmpty, isNotEmpty } from '../../../shared/empty.util';
|
||||
import { hasValue, isNotEmpty } from '../../../shared/empty.util';
|
||||
import { RemoteData } from '../../../core/data/remote-data';
|
||||
import { JsonPatchOperationPathCombiner } from '../../../core/json-patch/builder/json-patch-operation-path-combiner';
|
||||
import { JsonPatchOperationsBuilder } from '../../../core/json-patch/builder/json-patch-operations-builder';
|
||||
import { PaginatedList } from '../../../core/data/paginated-list';
|
||||
import { SubmissionService } from '../../submission.service';
|
||||
import { SubmissionObject } from '../../../core/submission/models/submission-object.model';
|
||||
import { SubmissionJsonPatchOperationsService } from '../../../core/submission/submission-json-patch-operations.service';
|
||||
import { CollectionDataService } from '../../../core/data/collection-data.service';
|
||||
import { FindListOptions } from '../../../core/data/request.models';
|
||||
|
||||
/**
|
||||
* 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
|
||||
}
|
||||
import { CollectionDropdownComponent } from 'src/app/shared/collection-dropdown/collection-dropdown.component';
|
||||
import { SectionsService } from '../../sections/sections.service';
|
||||
|
||||
/**
|
||||
* This component allows to show the current collection the submission belonging to and to change it.
|
||||
@@ -88,30 +62,12 @@ export class SubmissionFormCollectionComponent implements OnChanges, OnInit {
|
||||
*/
|
||||
@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
|
||||
* @type {BehaviorSubject<boolean>}
|
||||
*/
|
||||
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
|
||||
* @type {string}
|
||||
@@ -130,24 +86,23 @@ export class SubmissionFormCollectionComponent implements OnChanges, OnInit {
|
||||
*/
|
||||
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
|
||||
* @type {Array}
|
||||
*/
|
||||
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
|
||||
*
|
||||
@@ -159,37 +114,11 @@ export class SubmissionFormCollectionComponent implements OnChanges, OnInit {
|
||||
* @param {SubmissionService} submissionService
|
||||
*/
|
||||
constructor(protected cdr: ChangeDetectorRef,
|
||||
private communityDataService: CommunityDataService,
|
||||
private collectionDataService: CollectionDataService,
|
||||
private operationsBuilder: JsonPatchOperationsBuilder,
|
||||
private operationsService: SubmissionJsonPatchOperationsService,
|
||||
private submissionService: SubmissionService) {
|
||||
}
|
||||
|
||||
/**
|
||||
* 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);
|
||||
private submissionService: SubmissionService,
|
||||
private sectionsService: SectionsService) {
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -204,51 +133,6 @@ export class SubmissionFormCollectionComponent implements OnChanges, OnInit {
|
||||
find((collectionRD: RemoteData<Collection>) => isNotEmpty(collectionRD.payload)),
|
||||
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 +141,7 @@ export class SubmissionFormCollectionComponent implements OnChanges, OnInit {
|
||||
*/
|
||||
ngOnInit() {
|
||||
this.pathCombiner = new JsonPatchOperationPathCombiner('sections', 'collection');
|
||||
this.available$ = this.sectionsService.isSectionAvailable(this.submissionId, 'collection');
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -273,7 +158,6 @@ export class SubmissionFormCollectionComponent implements OnChanges, OnInit {
|
||||
* the selected [CollectionListEntryItem]
|
||||
*/
|
||||
onSelect(event) {
|
||||
this.searchField.reset();
|
||||
this.processingChange$.next(true);
|
||||
this.operationsBuilder.replace(this.pathCombiner.getPath(), event.collection.id, true);
|
||||
this.subs.push(this.operationsService.jsonPatchByResourceID(
|
||||
@@ -296,7 +180,7 @@ export class SubmissionFormCollectionComponent implements OnChanges, OnInit {
|
||||
* Reset search form control on dropdown menu close
|
||||
*/
|
||||
onClose() {
|
||||
this.searchField.reset();
|
||||
this.collectionDropdown.reset();
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -307,7 +191,7 @@ export class SubmissionFormCollectionComponent implements OnChanges, OnInit {
|
||||
*/
|
||||
toggled(isOpen: boolean) {
|
||||
if (!isOpen) {
|
||||
this.searchField.reset();
|
||||
this.collectionDropdown.reset();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@@ -1751,7 +1751,7 @@
|
||||
|
||||
"mydspace.description": "",
|
||||
|
||||
"mydspace.general.text-here": "HERE",
|
||||
"mydspace.general.text-here": "here",
|
||||
|
||||
"mydspace.messages.controller-help": "Select this option to send a message to item's submitter.",
|
||||
|
||||
|
Reference in New Issue
Block a user