Merge branch 'master' into CST-3091

This commit is contained in:
Danilo Di Nuzzo
2020-06-30 10:25:01 +02:00
20 changed files with 361 additions and 798 deletions

View File

@@ -32,6 +32,7 @@ import { ItemVersionHistoryComponent } from './item-version-history/item-version
import { ItemAuthorizationsComponent } from './item-authorizations/item-authorizations.component'; import { ItemAuthorizationsComponent } from './item-authorizations/item-authorizations.component';
import { ResourcePolicyEditComponent } from '../../shared/resource-policies/edit/resource-policy-edit.component'; import { ResourcePolicyEditComponent } from '../../shared/resource-policies/edit/resource-policy-edit.component';
import { ResourcePolicyCreateComponent } from '../../shared/resource-policies/create/resource-policy-create.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 * 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, ResourcePolicyCreateComponent,
], ],
providers: [ providers: [
BundleDataService BundleDataService,
ObjectValuesPipe
] ]
}) })
export class EditItemPageModule { export class EditItemPageModule {

View File

@@ -36,7 +36,8 @@
<ds-item-edit-bitstream-bundle *ngFor="let bundle of bundles" <ds-item-edit-bitstream-bundle *ngFor="let bundle of bundles"
[bundle]="bundle" [bundle]="bundle"
[item]="item" [item]="item"
[columnSizes]="columnSizes"> [columnSizes]="columnSizes"
(dropObject)="dropBitstream(bundle, $event)">
</ds-item-edit-bitstream-bundle> </ds-item-edit-bitstream-bundle>
</div> </div>
<div *ngIf="bundles?.length === 0" <div *ngIf="bundles?.length === 0"

View File

@@ -188,8 +188,21 @@ describe('ItemBitstreamsComponent', () => {
it('should not call deleteAndReturnResponse on the bitstreamService for the unmarked field', () => { it('should not call deleteAndReturnResponse on the bitstreamService for the unmarked field', () => {
expect(bitstreamService.deleteAndReturnResponse).not.toHaveBeenCalledWith(bitstream1.id); 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(); expect(bundleService.patch).toHaveBeenCalled();
}); });
}); });

View File

@@ -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 { 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 { Observable } from 'rxjs/internal/Observable';
import { Subscription } from 'rxjs/internal/Subscription'; import { Subscription } from 'rxjs/internal/Subscription';
import { ItemDataService } from '../../../core/data/item-data.service'; 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 { NotificationsService } from '../../../shared/notifications/notifications.service';
import { TranslateService } from '@ngx-translate/core'; import { TranslateService } from '@ngx-translate/core';
import { BitstreamDataService } from '../../../core/data/bitstream-data.service'; import { BitstreamDataService } from '../../../core/data/bitstream-data.service';
import { hasValue, isNotEmpty, isNotEmptyOperator } from '../../../shared/empty.util'; import { hasValue, isNotEmpty } from '../../../shared/empty.util';
import { zip as observableZip, combineLatest as observableCombineLatest, of as observableOf } from 'rxjs'; import { zip as observableZip, of as observableOf } from 'rxjs';
import { ErrorResponse, RestResponse } from '../../../core/cache/response.models'; import { ErrorResponse, RestResponse } from '../../../core/cache/response.models';
import { ObjectCacheService } from '../../../core/cache/object-cache.service'; import { ObjectCacheService } from '../../../core/cache/object-cache.service';
import { RequestService } from '../../../core/data/request.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 { FieldUpdate, FieldUpdates } from '../../../core/data/object-updates/object-updates.reducer';
import { Bitstream } from '../../../core/shared/bitstream.model'; import { Bitstream } from '../../../core/shared/bitstream.model';
import { FieldChangeType } from '../../../core/data/object-updates/object-updates.actions'; 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 { BundleDataService } from '../../../core/data/bundle-data.service';
import { PaginatedSearchOptions } from '../../../shared/search/paginated-search-options.model'; import { PaginatedSearchOptions } from '../../../shared/search/paginated-search-options.model';
import { ResponsiveColumnSizes } from '../../../shared/responsive-table-sizes/responsive-column-sizes'; import { ResponsiveColumnSizes } from '../../../shared/responsive-table-sizes/responsive-column-sizes';
@@ -90,7 +88,8 @@ export class ItemBitstreamsComponent extends AbstractItemUpdateComponent impleme
public objectCache: ObjectCacheService, public objectCache: ObjectCacheService,
public requestService: RequestService, public requestService: RequestService,
public cdRef: ChangeDetectorRef, public cdRef: ChangeDetectorRef,
public bundleService: BundleDataService public bundleService: BundleDataService,
public zone: NgZone
) { ) {
super(itemService, objectUpdatesService, router, notificationsService, translateService, route); super(itemService, objectUpdatesService, router, notificationsService, translateService, route);
} }
@@ -143,7 +142,6 @@ export class ItemBitstreamsComponent extends AbstractItemUpdateComponent impleme
/** /**
* Submit the current changes * 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 * Bitstreams marked as deleted send out a delete request to the rest API
* Display notifications and reset the current item/updates * Display notifications and reset the current item/updates
*/ */
@@ -151,32 +149,6 @@ export class ItemBitstreamsComponent extends AbstractItemUpdateComponent impleme
this.submitting = true; this.submitting = true;
const bundlesOnce$ = this.bundles$.pipe(take(1)); 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 // Fetch all removed bitstreams from the object update service
const removedBitstreams$ = bundlesOnce$.pipe( const removedBitstreams$ = bundlesOnce$.pipe(
switchMap((bundles: Bundle[]) => observableZip( 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 // Perform the setup actions from above in order and display notifications
patchResponses$.pipe( removedResponses$.pipe(take(1)).subscribe((responses: RestResponse[]) => {
switchMap((responses: RestResponse[]) => {
this.displayNotifications('item.edit.bitstreams.notifications.move', responses);
return removedResponses$
}),
take(1)
).subscribe((responses: RestResponse[]) => {
this.displayNotifications('item.edit.bitstreams.notifications.remove', responses); this.displayNotifications('item.edit.bitstreams.notifications.remove', responses);
this.reset(); this.reset();
this.submitting = false; 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 * Display notifications
* - Error notification for each failed response with their message * - Error notification for each failed response with their message

View File

@@ -17,5 +17,5 @@
</div> </div>
</div> </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> </ng-template>

View File

@@ -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 { Bundle } from '../../../../core/shared/bundle.model';
import { Item } from '../../../../core/shared/item.model'; import { Item } from '../../../../core/shared/item.model';
import { ResponsiveColumnSizes } from '../../../../shared/responsive-table-sizes/responsive-column-sizes'; import { ResponsiveColumnSizes } from '../../../../shared/responsive-table-sizes/responsive-column-sizes';
@@ -36,6 +36,13 @@ export class ItemEditBitstreamBundleComponent implements OnInit {
*/ */
@Input() columnSizes: ResponsiveTableSizes; @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 * 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 * This column stretches over the first 3 columns and thus is a combination of their sizes processed in ngOnInit

View File

@@ -7,18 +7,20 @@
[collectionSize]="(objectsRD$ | async)?.payload?.totalElements" [collectionSize]="(objectsRD$ | async)?.payload?.totalElements"
[disableRouteParameterUpdate]="true" [disableRouteParameterUpdate]="true"
(pageChange)="switchPage($event)"> (pageChange)="switchPage($event)">
<ng-container *ngIf="!(loading$ | async)">
<div [id]="bundle.id" class="bundle-bitstreams-list" <div [id]="bundle.id" class="bundle-bitstreams-list"
[ngClass]="{'mb-3': (objectsRD$ | async)?.payload?.totalElements > pageSize}" [ngClass]="{'mb-3': (objectsRD$ | async)?.payload?.totalElements > pageSize}"
*ngVar="((updates$ | async) | dsObjectValues) as updateValues" cdkDropList (cdkDropListDropped)="drop($event)"> *ngVar="(updates$ | async) as updates" cdkDropList (cdkDropListDropped)="drop($event)">
<div class="row bitstream-row" *ngFor="let updateValue of updateValues" cdkDrag <ng-container *ngIf="updates">
[id]="updateValue.field.uuid" <div class="row bitstream-row" *ngFor="let uuid of customOrder" cdkDrag
[id]="uuid"
[ngClass]="{ [ngClass]="{
'table-warning': updateValue.changeType === 0, 'table-warning': updates[uuid].changeType === 0,
'table-danger': updateValue.changeType === 2, 'table-danger': updates[uuid].changeType === 2,
'table-success': updateValue.changeType === 1, 'table-success': updates[uuid].changeType === 1,
'bg-white': updateValue.changeType === undefined 'bg-white': updates[uuid].changeType === undefined
}"> }">
<ds-item-edit-bitstream [fieldUpdate]="updateValue" <ds-item-edit-bitstream [fieldUpdate]="updates[uuid]"
[bundleUrl]="bundle.self" [bundleUrl]="bundle.self"
[columnSizes]="columnSizes"> [columnSizes]="columnSizes">
<div class="d-flex align-items-center" slot="drag-handle" cdkDragHandle> <div class="d-flex align-items-center" slot="drag-handle" cdkDragHandle>
@@ -26,5 +28,8 @@
</div> </div>
</ds-item-edit-bitstream> </ds-item-edit-bitstream>
</div> </div>
</ng-container>
</div> </div>
</ng-container>
<ds-loading *ngIf="(loading$ | async)" [message]="'loading.bitstreams' | translate"></ds-loading>
</ds-pagination> </ds-pagination>

View File

@@ -16,12 +16,15 @@ import { ResponsiveTableSizes } from '../../../../../shared/responsive-table-siz
import { ResponsiveColumnSizes } from '../../../../../shared/responsive-table-sizes/responsive-column-sizes'; import { ResponsiveColumnSizes } from '../../../../../shared/responsive-table-sizes/responsive-column-sizes';
import { createSuccessfulRemoteDataObject$ } from '../../../../../shared/remote-data.utils'; import { createSuccessfulRemoteDataObject$ } from '../../../../../shared/remote-data.utils';
import { createPaginatedList } from '../../../../../shared/testing/utils.test'; import { createPaginatedList } from '../../../../../shared/testing/utils.test';
import { RequestService } from '../../../../../core/data/request.service';
describe('PaginatedDragAndDropBitstreamListComponent', () => { describe('PaginatedDragAndDropBitstreamListComponent', () => {
let comp: PaginatedDragAndDropBitstreamListComponent; let comp: PaginatedDragAndDropBitstreamListComponent;
let fixture: ComponentFixture<PaginatedDragAndDropBitstreamListComponent>; let fixture: ComponentFixture<PaginatedDragAndDropBitstreamListComponent>;
let objectUpdatesService: ObjectUpdatesService; let objectUpdatesService: ObjectUpdatesService;
let bundleService: BundleDataService; let bundleService: BundleDataService;
let objectValuesPipe: ObjectValuesPipe;
let requestService: RequestService;
const columnSizes = new ResponsiveTableSizes([ const columnSizes = new ResponsiveTableSizes([
new ResponsiveColumnSizes(2, 2, 3, 4, 4), new ResponsiveColumnSizes(2, 2, 3, 4, 4),
@@ -97,15 +100,24 @@ describe('PaginatedDragAndDropBitstreamListComponent', () => {
); );
bundleService = jasmine.createSpyObj('bundleService', { 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({ TestBed.configureTestingModule({
imports: [TranslateModule.forRoot()], imports: [TranslateModule.forRoot()],
declarations: [PaginatedDragAndDropBitstreamListComponent, VarDirective, ObjectValuesPipe], declarations: [PaginatedDragAndDropBitstreamListComponent, VarDirective],
providers: [ providers: [
{ provide: ObjectUpdatesService, useValue: objectUpdatesService }, { provide: ObjectUpdatesService, useValue: objectUpdatesService },
{ provide: BundleDataService, useValue: bundleService } { provide: BundleDataService, useValue: bundleService },
{ provide: ObjectValuesPipe, useValue: objectValuesPipe },
{ provide: RequestService, useValue: requestService }
], schemas: [ ], schemas: [
NO_ERRORS_SCHEMA NO_ERRORS_SCHEMA
] ]

View File

@@ -8,6 +8,8 @@ import { switchMap } from 'rxjs/operators';
import { PaginatedSearchOptions } from '../../../../../shared/search/paginated-search-options.model'; import { PaginatedSearchOptions } from '../../../../../shared/search/paginated-search-options.model';
import { ResponsiveTableSizes } from '../../../../../shared/responsive-table-sizes/responsive-table-sizes'; import { ResponsiveTableSizes } from '../../../../../shared/responsive-table-sizes/responsive-table-sizes';
import { followLink } from '../../../../../shared/utils/follow-link-config.model'; 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({ @Component({
selector: 'ds-paginated-drag-and-drop-bitstream-list', selector: 'ds-paginated-drag-and-drop-bitstream-list',
@@ -33,8 +35,10 @@ export class PaginatedDragAndDropBitstreamListComponent extends AbstractPaginate
constructor(protected objectUpdatesService: ObjectUpdatesService, constructor(protected objectUpdatesService: ObjectUpdatesService,
protected elRef: ElementRef, protected elRef: ElementRef,
protected bundleService: BundleDataService) { protected objectValuesPipe: ObjectValuesPipe,
super(objectUpdatesService, elRef); protected bundleService: BundleDataService,
protected requestService: RequestService) {
super(objectUpdatesService, elRef, objectValuesPipe);
} }
ngOnInit() { ngOnInit() {
@@ -46,12 +50,18 @@ export class PaginatedDragAndDropBitstreamListComponent extends AbstractPaginate
*/ */
initializeObjectsRD(): void { initializeObjectsRD(): void {
this.objectsRD$ = this.currentPage$.pipe( this.objectsRD$ = this.currentPage$.pipe(
switchMap((page: number) => this.bundleService.getBitstreams( 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, this.bundle.id,
new PaginatedSearchOptions({pagination: Object.assign({}, this.options, { currentPage: page })}), paginatedOptions,
followLink('format') followLink('format')
)) ))
); );
})
);
} }
/** /**

View File

@@ -210,13 +210,16 @@ describe('CommunityListService', () => {
let flatNodeList; let flatNodeList;
describe('None expanded: should return list containing only flatnodes of the test top communities page 1 and 2', () => { describe('None expanded: should return list containing only flatnodes of the test top communities page 1 and 2', () => {
let findTopSpy; let findTopSpy;
beforeEach(() => { beforeEach((done) => {
findTopSpy = spyOn(communityDataServiceStub, 'findTop').and.callThrough(); findTopSpy = spyOn(communityDataServiceStub, 'findTop').and.callThrough();
service.getNextPageTopCommunities(); service.getNextPageTopCommunities();
const sub = service.loadCommunities(null) service.loadCommunities(null)
.subscribe((value) => flatNodeList = value); .pipe(take(1))
sub.unsubscribe(); .subscribe((value) => {
flatNodeList = value;
done();
});
}); });
it('flatnode list should contain just flatnodes of top community list page 1 and 2', () => { it('flatnode list should contain just flatnodes of top community list page 1 and 2', () => {
expect(findTopSpy).toHaveBeenCalled(); expect(findTopSpy).toHaveBeenCalled();
@@ -236,10 +239,13 @@ describe('CommunityListService', () => {
describe('should transform all communities in a list of flatnodes with possible subcoms and collections as subflatnodes if they\'re expanded', () => { describe('should transform all communities in a list of flatnodes with possible subcoms and collections as subflatnodes if they\'re expanded', () => {
let flatNodeList; let flatNodeList;
describe('None expanded: should return list containing only flatnodes of the test top communities', () => { describe('None expanded: should return list containing only flatnodes of the test top communities', () => {
beforeEach(() => { beforeEach((done) => {
const sub = service.loadCommunities(null) service.loadCommunities(null)
.pipe(take(1)).subscribe((value) => flatNodeList = value); .pipe(take(1))
sub.unsubscribe(); .subscribe((value) => {
flatNodeList = value;
done();
});
}); });
it('length of flatnode list should be as big as top community list', () => { it('length of flatnode list should be as big as top community list', () => {
expect(flatNodeList.length).toEqual(mockListOfTopCommunitiesPage1.length); expect(flatNodeList.length).toEqual(mockListOfTopCommunitiesPage1.length);
@@ -256,7 +262,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)', () => { 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 = []; const expandedNodes = [];
mockListOfTopCommunitiesPage1.map((community: Community) => { mockListOfTopCommunitiesPage1.map((community: Community) => {
const communityFlatNode = toFlatNode(community, observableOf(true), 0, true, null); const communityFlatNode = toFlatNode(community, observableOf(true), 0, true, null);
@@ -264,9 +270,12 @@ describe('CommunityListService', () => {
communityFlatNode.currentCommunityPage = 1; communityFlatNode.currentCommunityPage = 1;
expandedNodes.push(communityFlatNode); expandedNodes.push(communityFlatNode);
}); });
const sub = service.loadCommunities(expandedNodes) service.loadCommunities(expandedNodes)
.pipe(take(1)).subscribe((value) => flatNodeList = value); .pipe(take(1))
sub.unsubscribe(); .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', () => { 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); expect(flatNodeList.length).toEqual(mockListOfTopCommunitiesPage1.length + mockSubcommunities1Page1.length + mockSubcommunities1Page1.length);
@@ -281,14 +290,17 @@ 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)', () => { 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); const communityFlatNode = toFlatNode(mockListOfTopCommunitiesPage1[0], observableOf(true), 0, true, null);
communityFlatNode.currentCollectionPage = 1; communityFlatNode.currentCollectionPage = 1;
communityFlatNode.currentCommunityPage = 1; communityFlatNode.currentCommunityPage = 1;
const expandedNodes = [communityFlatNode]; const expandedNodes = [communityFlatNode];
const sub = service.loadCommunities(expandedNodes) service.loadCommunities(expandedNodes)
.pipe(take(1)).subscribe((value) => flatNodeList = value); .pipe(take(1))
sub.unsubscribe(); .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', () => { 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); expect(flatNodeList.length).toEqual(mockListOfTopCommunitiesPage1.length + mockSubcommunities1Page1.length);
@@ -300,14 +312,17 @@ 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)', () => { 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); const communityFlatNode = toFlatNode(mockListOfTopCommunitiesPage1[1], observableOf(true), 0, true, null);
communityFlatNode.currentCollectionPage = 2; communityFlatNode.currentCollectionPage = 2;
communityFlatNode.currentCommunityPage = 1; communityFlatNode.currentCommunityPage = 1;
const expandedNodes = [communityFlatNode]; const expandedNodes = [communityFlatNode];
const sub = service.loadCommunities(expandedNodes) service.loadCommunities(expandedNodes)
.pipe(take(1)).subscribe((value) => flatNodeList = value); .pipe(take(1))
sub.unsubscribe(); .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', () => { 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); expect(flatNodeList.length).toEqual(mockListOfTopCommunitiesPage1.length + mockCollectionsPage1.length + mockCollectionsPage2.length);
@@ -333,10 +348,13 @@ describe('CommunityListService', () => {
}); });
let flatNodeList; let flatNodeList;
describe('None expanded: should return list containing only flatnodes of the communities in the test list', () => { describe('None expanded: should return list containing only flatnodes of the communities in the test list', () => {
beforeEach(() => { beforeEach((done) => {
const sub = service.transformListOfCommunities(new PaginatedList(new PageInfo(), listOfCommunities), 0, null, null) service.transformListOfCommunities(new PaginatedList(new PageInfo(), listOfCommunities), 0, null, null)
.pipe(take(1)).subscribe((value) => flatNodeList = value); .pipe(take(1))
sub.unsubscribe(); .subscribe((value) => {
flatNodeList = value;
done();
});
}); });
it('length of flatnode list should be as big as community test list', () => { it('length of flatnode list should be as big as community test list', () => {
expect(flatNodeList.length).toEqual(listOfCommunities.length); expect(flatNodeList.length).toEqual(listOfCommunities.length);
@@ -353,7 +371,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)', () => { 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 = []; const expandedNodes = [];
listOfCommunities.map((community: Community) => { listOfCommunities.map((community: Community) => {
const communityFlatNode = toFlatNode(community, observableOf(true), 0, true, null); const communityFlatNode = toFlatNode(community, observableOf(true), 0, true, null);
@@ -361,9 +379,12 @@ describe('CommunityListService', () => {
communityFlatNode.currentCommunityPage = 1; communityFlatNode.currentCommunityPage = 1;
expandedNodes.push(communityFlatNode); expandedNodes.push(communityFlatNode);
}); });
const sub = service.transformListOfCommunities(new PaginatedList(new PageInfo(), listOfCommunities), 0, null, expandedNodes) service.transformListOfCommunities(new PaginatedList(new PageInfo(), listOfCommunities), 0, null, expandedNodes)
.pipe(take(1)).subscribe((value) => flatNodeList = value); .pipe(take(1))
sub.unsubscribe(); .subscribe((value) => {
flatNodeList = value;
done();
});
}); });
it('length of flatnode list should be as big as community test list and size of its possible children', () => { 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); expect(flatNodeList.length).toEqual(listOfCommunities.length + mockSubcommunities1Page1.length + mockSubcommunities1Page1.length);
@@ -397,10 +418,13 @@ describe('CommunityListService', () => {
}); });
let flatNodeList; let flatNodeList;
describe('should return list containing only flatnode corresponding to that community', () => { describe('should return list containing only flatnode corresponding to that community', () => {
beforeEach(() => { beforeEach((done) => {
const sub = service.transformCommunity(communityWithNoSubcomsOrColls, 0, null, null) service.transformCommunity(communityWithNoSubcomsOrColls, 0, null, null)
.pipe(take(1)).subscribe((value) => flatNodeList = value); .pipe(take(1))
sub.unsubscribe(); .subscribe((value) => {
flatNodeList = value;
done();
});
}); });
it('length of flatnode list should be 1', () => { it('length of flatnode list should be 1', () => {
expect(flatNodeList.length).toEqual(1); expect(flatNodeList.length).toEqual(1);
@@ -426,10 +450,14 @@ describe('CommunityListService', () => {
}); });
let flatNodeList; let flatNodeList;
describe('should return list containing only flatnode corresponding to that community', () => { describe('should return list containing only flatnode corresponding to that community', () => {
beforeAll(() => { beforeAll((done) => {
const sub = service.transformCommunity(communityWithSubcoms, 0, null, null) service.transformCommunity(communityWithSubcoms, 0, null, null)
.pipe(take(1)).subscribe((value) => flatNodeList = value); .pipe(take(1))
sub.unsubscribe(); .subscribe((value) => {
flatNodeList = value;
done();
});
}); });
it('length of flatnode list should be 1', () => { it('length of flatnode list should be 1', () => {
expect(flatNodeList.length).toEqual(1); expect(flatNodeList.length).toEqual(1);
@@ -455,14 +483,17 @@ describe('CommunityListService', () => {
} }
}); });
let flatNodeList; let flatNodeList;
beforeEach(() => { beforeEach((done) => {
const communityFlatNode = toFlatNode(communityWithSubcoms, observableOf(true), 0, true, null); const communityFlatNode = toFlatNode(communityWithSubcoms, observableOf(true), 0, true, null);
communityFlatNode.currentCollectionPage = 1; communityFlatNode.currentCollectionPage = 1;
communityFlatNode.currentCommunityPage = 1; communityFlatNode.currentCommunityPage = 1;
const expandedNodes = [communityFlatNode]; const expandedNodes = [communityFlatNode];
const sub = service.transformCommunity(communityWithSubcoms, 0, null, expandedNodes) service.transformCommunity(communityWithSubcoms, 0, null, expandedNodes)
.pipe(take(1)).subscribe((value) => flatNodeList = value); .pipe(take(1))
sub.unsubscribe(); .subscribe((value) => {
flatNodeList = value;
done();
});
}); });
it('list of flatnodes is length is 1 + nrOfSubcoms & first flatnode is of expanded test community', () => { it('list of flatnodes is length is 1 + nrOfSubcoms & first flatnode is of expanded test community', () => {
expect(flatNodeList.length).toEqual(1 + mockSubcommunities1Page1.length); expect(flatNodeList.length).toEqual(1 + mockSubcommunities1Page1.length);
@@ -485,7 +516,7 @@ describe('CommunityListService', () => {
describe('should return list containing flatnodes of that community, its collections of the first two pages', () => { describe('should return list containing flatnodes of that community, its collections of the first two pages', () => {
let communityWithCollections; let communityWithCollections;
let flatNodeList; let flatNodeList;
beforeEach(() => { beforeEach((done) => {
communityWithCollections = Object.assign(new Community(), { communityWithCollections = Object.assign(new Community(), {
id: '9076bd16-e69a-48d6-9e41-0238cb40d863', id: '9076bd16-e69a-48d6-9e41-0238cb40d863',
uuid: '9076bd16-e69a-48d6-9e41-0238cb40d863', uuid: '9076bd16-e69a-48d6-9e41-0238cb40d863',
@@ -500,9 +531,12 @@ describe('CommunityListService', () => {
communityFlatNode.currentCollectionPage = 2; communityFlatNode.currentCollectionPage = 2;
communityFlatNode.currentCommunityPage = 1; communityFlatNode.currentCommunityPage = 1;
const expandedNodes = [communityFlatNode]; const expandedNodes = [communityFlatNode];
const sub = service.transformCommunity(communityWithCollections, 0, null, expandedNodes) service.transformCommunity(communityWithCollections, 0, null, expandedNodes)
.pipe(take(1)).subscribe((value) => flatNodeList = value); .pipe(take(1))
sub.unsubscribe(); .subscribe((value) => {
flatNodeList = value;
done();
});
}); });
it('list of flatnodes is length is 1 + nrOfCollections & first flatnode is of expanded test community', () => { 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); expect(flatNodeList.length).toEqual(1 + mockCollectionsPage1.length + mockCollectionsPage2.length);
@@ -533,7 +567,7 @@ describe('CommunityListService', () => {
describe('getIsExpandable', () => { describe('getIsExpandable', () => {
describe('should return true', () => { describe('should return true', () => {
it('if community has subcommunities', () => { it('if community has subcommunities', (done) => {
const communityWithSubcoms = Object.assign(new Community(), { const communityWithSubcoms = Object.assign(new Community(), {
id: '7669c72a-3f2a-451f-a3b9-9210e7a4c02f', id: '7669c72a-3f2a-451f-a3b9-9210e7a4c02f',
uuid: '7669c72a-3f2a-451f-a3b9-9210e7a4c02f', uuid: '7669c72a-3f2a-451f-a3b9-9210e7a4c02f',
@@ -546,9 +580,10 @@ describe('CommunityListService', () => {
}); });
service.getIsExpandable(communityWithSubcoms).pipe(take(1)).subscribe((result) => { service.getIsExpandable(communityWithSubcoms).pipe(take(1)).subscribe((result) => {
expect(result).toEqual(true); expect(result).toEqual(true);
done();
}); });
}); });
it('if community has collections', () => { it('if community has collections', (done) => {
const communityWithCollections = Object.assign(new Community(), { const communityWithCollections = Object.assign(new Community(), {
id: '9076bd16-e69a-48d6-9e41-0238cb40d863', id: '9076bd16-e69a-48d6-9e41-0238cb40d863',
uuid: '9076bd16-e69a-48d6-9e41-0238cb40d863', uuid: '9076bd16-e69a-48d6-9e41-0238cb40d863',
@@ -561,11 +596,12 @@ describe('CommunityListService', () => {
}); });
service.getIsExpandable(communityWithCollections).pipe(take(1)).subscribe((result) => { service.getIsExpandable(communityWithCollections).pipe(take(1)).subscribe((result) => {
expect(result).toEqual(true); expect(result).toEqual(true);
done();
}); });
}); });
}); });
describe('should return false', () => { 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(), { const communityWithNoSubcomsOrColls = Object.assign(new Community(), {
id: 'efbf25e1-2d8c-4c28-8f3e-2e04c215be24', id: 'efbf25e1-2d8c-4c28-8f3e-2e04c215be24',
uuid: 'efbf25e1-2d8c-4c28-8f3e-2e04c215be24', uuid: 'efbf25e1-2d8c-4c28-8f3e-2e04c215be24',
@@ -578,6 +614,7 @@ describe('CommunityListService', () => {
}); });
service.getIsExpandable(communityWithNoSubcomsOrColls).pipe(take(1)).subscribe((result) => { service.getIsExpandable(communityWithNoSubcomsOrColls).pipe(take(1)).subscribe((result) => {
expect(result).toEqual(false); expect(result).toEqual(false);
done();
}); });
}); });
}); });

View File

@@ -88,10 +88,12 @@ export class BundleDataService extends DataService<Bundle> {
/** /**
* Get the bitstreams endpoint for a bundle * Get the bitstreams endpoint for a bundle
* @param bundleId * @param bundleId
* @param searchOptions
*/ */
getBitstreamsEndpoint(bundleId: string): Observable<string> { getBitstreamsEndpoint(bundleId: string, searchOptions?: PaginatedSearchOptions): Observable<string> {
return this.getBrowseEndpoint().pipe( 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 * @param linksToFollow The {@link FollowLinkConfig}s for the request
*/ */
getBitstreams(bundleId: string, searchOptions?: PaginatedSearchOptions, ...linksToFollow: Array<FollowLinkConfig<Bitstream>>): Observable<RemoteData<PaginatedList<Bitstream>>> { getBitstreams(bundleId: string, searchOptions?: PaginatedSearchOptions, ...linksToFollow: Array<FollowLinkConfig<Bitstream>>): Observable<RemoteData<PaginatedList<Bitstream>>> {
const hrefObs = this.getBitstreamsEndpoint(bundleId).pipe( const hrefObs = this.getBitstreamsEndpoint(bundleId, searchOptions);
map((href) => searchOptions ? searchOptions.toRestUrl(href) : href)
);
hrefObs.pipe( hrefObs.pipe(
take(1) take(1)
).subscribe((href) => { ).subscribe((href) => {

View File

@@ -8,7 +8,6 @@ import {INotification} from '../../../shared/notifications/models/notification.m
*/ */
export const ObjectUpdatesActionTypes = { export const ObjectUpdatesActionTypes = {
INITIALIZE_FIELDS: type('dspace/core/cache/object-updates/INITIALIZE_FIELDS'), 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_EDITABLE_FIELD: type('dspace/core/cache/object-updates/SET_EDITABLE_FIELD'),
SET_VALID_FIELD: type('dspace/core/cache/object-updates/SET_VALID_FIELD'), SET_VALID_FIELD: type('dspace/core/cache/object-updates/SET_VALID_FIELD'),
ADD_FIELD: type('dspace/core/cache/object-updates/ADD_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'), REINSTATE: type('dspace/core/cache/object-updates/REINSTATE'),
REMOVE: type('dspace/core/cache/object-updates/REMOVE'), REMOVE: type('dspace/core/cache/object-updates/REMOVE'),
REMOVE_ALL: type('dspace/core/cache/object-updates/REMOVE_ALL'), REMOVE_ALL: type('dspace/core/cache/object-updates/REMOVE_ALL'),
REMOVE_FIELD: type('dspace/core/cache/object-updates/REMOVE_FIELD'), REMOVE_FIELD: type('dspace/core/cache/object-updates/REMOVE_FIELD')
MOVE: type('dspace/core/cache/object-updates/MOVE'),
}; };
/* tslint:disable:max-classes-per-file */ /* tslint:disable:max-classes-per-file */
@@ -29,8 +27,7 @@ export const ObjectUpdatesActionTypes = {
export enum FieldChangeType { export enum FieldChangeType {
UPDATE = 0, UPDATE = 0,
ADD = 1, ADD = 1,
REMOVE = 2, REMOVE = 2
MOVE = 3
} }
/** /**
@@ -41,10 +38,7 @@ export class InitializeFieldsAction implements Action {
payload: { payload: {
url: string, url: string,
fields: Identifiable[], fields: Identifiable[],
lastModified: Date, lastModified: Date
order: string[],
pageSize: number,
page: number
}; };
/** /**
@@ -61,42 +55,9 @@ export class InitializeFieldsAction implements Action {
constructor( constructor(
url: string, url: string,
fields: Identifiable[], fields: Identifiable[],
lastModified: Date, lastModified: Date
order: string[] = [],
pageSize: number = 9999,
page: number = 0
) { ) {
this.payload = { url, fields, lastModified, order, pageSize, page }; this.payload = { url, fields, lastModified };
}
}
/**
* 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 };
} }
} }
@@ -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 */ /* tslint:enable:max-classes-per-file */
/** /**
@@ -369,8 +293,6 @@ export type ObjectUpdatesAction
| ReinstateObjectUpdatesAction | ReinstateObjectUpdatesAction
| RemoveObjectUpdatesAction | RemoveObjectUpdatesAction
| RemoveFieldUpdateAction | RemoveFieldUpdateAction
| MoveFieldUpdateAction
| AddPageToCustomOrderAction
| RemoveAllObjectUpdatesAction | RemoveAllObjectUpdatesAction
| SelectVirtualMetadataAction | SelectVirtualMetadataAction
| SetEditableFieldUpdateAction | SetEditableFieldUpdateAction

View File

@@ -1,9 +1,9 @@
import * as deepFreeze from 'deep-freeze'; import * as deepFreeze from 'deep-freeze';
import { import {
AddFieldUpdateAction, AddPageToCustomOrderAction, AddFieldUpdateAction,
DiscardObjectUpdatesAction, DiscardObjectUpdatesAction,
FieldChangeType, FieldChangeType,
InitializeFieldsAction, MoveFieldUpdateAction, InitializeFieldsAction,
ReinstateObjectUpdatesAction, RemoveAllObjectUpdatesAction, ReinstateObjectUpdatesAction, RemoveAllObjectUpdatesAction,
RemoveFieldUpdateAction, RemoveObjectUpdatesAction, SelectVirtualMetadataAction, RemoveFieldUpdateAction, RemoveObjectUpdatesAction, SelectVirtualMetadataAction,
SetEditableFieldUpdateAction, SetValidFieldUpdateAction SetEditableFieldUpdateAction, SetValidFieldUpdateAction
@@ -85,16 +85,6 @@ describe('objectUpdatesReducer', () => {
virtualMetadataSources: { virtualMetadataSources: {
[relationship.uuid]: {[identifiable1.uuid]: true} [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: { virtualMetadataSources: {
[relationship.uuid]: {[identifiable1.uuid]: true} [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]: { [url + OBJECT_UPDATES_TRASH_PATH]: {
fieldStates: { fieldStates: {
@@ -165,16 +145,6 @@ describe('objectUpdatesReducer', () => {
virtualMetadataSources: { virtualMetadataSources: {
[relationship.uuid]: {[identifiable1.uuid]: true} [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', () => { 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 = { const expectedState = {
[url]: { [url]: {
@@ -261,17 +231,7 @@ describe('objectUpdatesReducer', () => {
}, },
fieldUpdates: {}, fieldUpdates: {},
virtualMetadataSources: {}, virtualMetadataSources: {},
lastModified: modDate, lastModified: modDate
customOrder: {
initialOrderPages: [
{ order: [identifiable1.uuid, identifiable3.uuid] }
],
newOrderPages: [
{ order: [identifiable1.uuid, identifiable3.uuid] }
],
pageSize: 10,
changed: false
}
} }
}; };
const newState = objectUpdatesReducer(testState, action); const newState = objectUpdatesReducer(testState, action);
@@ -337,30 +297,4 @@ describe('objectUpdatesReducer', () => {
const newState = objectUpdatesReducer(testState, action); const newState = objectUpdatesReducer(testState, action);
expect(newState[url].fieldUpdates[uuid]).toBeUndefined(); 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);
});
}); });

View File

@@ -1,8 +1,8 @@
import { import {
AddFieldUpdateAction, AddPageToCustomOrderAction, AddFieldUpdateAction,
DiscardObjectUpdatesAction, DiscardObjectUpdatesAction,
FieldChangeType, FieldChangeType,
InitializeFieldsAction, MoveFieldUpdateAction, InitializeFieldsAction,
ObjectUpdatesAction, ObjectUpdatesAction,
ObjectUpdatesActionTypes, ObjectUpdatesActionTypes,
ReinstateObjectUpdatesAction, ReinstateObjectUpdatesAction,
@@ -12,9 +12,7 @@ import {
SetValidFieldUpdateAction, SetValidFieldUpdateAction,
SelectVirtualMetadataAction, SelectVirtualMetadataAction,
} from './object-updates.actions'; } from './object-updates.actions';
import { hasNoValue, hasValue, isEmpty, isNotEmpty } from '../../../shared/empty.util'; import { hasNoValue, hasValue } from '../../../shared/empty.util';
import { moveItemInArray, transferArrayItem } from '@angular/cdk/drag-drop';
import { from } from 'rxjs/internal/observable/from';
import {Relationship} from '../../shared/item-relationships/relationship.model'; import {Relationship} from '../../shared/item-relationships/relationship.model';
/** /**
@@ -83,20 +81,6 @@ export interface DeleteRelationship extends Relationship {
keepRightVirtualMetadata: boolean, 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 * The updated state of a single page
*/ */
@@ -105,7 +89,6 @@ export interface ObjectUpdatesEntry {
fieldUpdates: FieldUpdates; fieldUpdates: FieldUpdates;
virtualMetadataSources: VirtualMetadataSources; virtualMetadataSources: VirtualMetadataSources;
lastModified: Date; lastModified: Date;
customOrder: CustomOrder
} }
/** /**
@@ -138,9 +121,6 @@ export function objectUpdatesReducer(state = initialState, action: ObjectUpdates
case ObjectUpdatesActionTypes.INITIALIZE_FIELDS: { case ObjectUpdatesActionTypes.INITIALIZE_FIELDS: {
return initializeFieldsUpdate(state, action as InitializeFieldsAction); return initializeFieldsUpdate(state, action as InitializeFieldsAction);
} }
case ObjectUpdatesActionTypes.ADD_PAGE_TO_CUSTOM_ORDER: {
return addPageToCustomOrder(state, action as AddPageToCustomOrderAction);
}
case ObjectUpdatesActionTypes.ADD_FIELD: { case ObjectUpdatesActionTypes.ADD_FIELD: {
return addFieldUpdate(state, action as AddFieldUpdateAction); return addFieldUpdate(state, action as AddFieldUpdateAction);
} }
@@ -168,9 +148,6 @@ export function objectUpdatesReducer(state = initialState, action: ObjectUpdates
case ObjectUpdatesActionTypes.SET_VALID_FIELD: { case ObjectUpdatesActionTypes.SET_VALID_FIELD: {
return setValidFieldUpdate(state, action as SetValidFieldUpdateAction); return setValidFieldUpdate(state, action as SetValidFieldUpdateAction);
} }
case ObjectUpdatesActionTypes.MOVE: {
return moveFieldUpdate(state, action as MoveFieldUpdateAction);
}
default: { default: {
return state; return state;
} }
@@ -186,50 +163,18 @@ function initializeFieldsUpdate(state: any, action: InitializeFieldsAction) {
const url: string = action.payload.url; const url: string = action.payload.url;
const fields: Identifiable[] = action.payload.fields; const fields: Identifiable[] = action.payload.fields;
const lastModifiedServer: Date = action.payload.lastModified; 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 fieldStates = createInitialFieldStates(fields);
const initialOrderPages = addOrderToPages([], order, pageSize, page);
const newPageState = Object.assign( const newPageState = Object.assign(
{}, {},
state[url], state[url],
{ fieldStates: fieldStates }, { fieldStates: fieldStates },
{ fieldUpdates: {} }, { fieldUpdates: {} },
{ virtualMetadataSources: {} }, { virtualMetadataSources: {} },
{ lastModified: lastModifiedServer }, { lastModified: lastModifiedServer }
{ customOrder: {
initialOrderPages: initialOrderPages,
newOrderPages: initialOrderPages,
pageSize: pageSize,
changed: false }
}
); );
return Object.assign({}, state, { [url]: newPageState }); 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 * Add a new update for a specific field to the store
* @param state The current state * @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, { const discardedPageState = Object.assign({}, pageState, {
fieldUpdates: {}, fieldUpdates: {},
fieldStates: newFieldStates, fieldStates: newFieldStates
customOrder: newCustomOrder
}); });
return Object.assign({}, state, { [url]: discardedPageState }, { [url + OBJECT_UPDATES_TRASH_PATH]: pageState }); 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); uuids.forEach((uuid: string) => fieldStates[uuid] = initialFieldState);
return fieldStates; 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;
}

View File

@@ -2,7 +2,6 @@ import { Store } from '@ngrx/store';
import { CoreState } from '../../core.reducers'; import { CoreState } from '../../core.reducers';
import { ObjectUpdatesService } from './object-updates.service'; import { ObjectUpdatesService } from './object-updates.service';
import { import {
AddPageToCustomOrderAction,
DiscardObjectUpdatesAction, DiscardObjectUpdatesAction,
FieldChangeType, FieldChangeType,
InitializeFieldsAction, ReinstateObjectUpdatesAction, RemoveFieldUpdateAction, SelectVirtualMetadataAction, InitializeFieldsAction, ReinstateObjectUpdatesAction, RemoveFieldUpdateAction, SelectVirtualMetadataAction,
@@ -13,8 +12,6 @@ import { Notification } from '../../../shared/notifications/models/notification.
import { NotificationType } from '../../../shared/notifications/models/notification-type'; import { NotificationType } from '../../../shared/notifications/models/notification-type';
import { OBJECT_UPDATES_TRASH_PATH } from './object-updates.reducer'; import { OBJECT_UPDATES_TRASH_PATH } from './object-updates.reducer';
import {Relationship} from '../../shared/item-relationships/relationship.model'; 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', () => { describe('ObjectUpdatesService', () => {
let service: ObjectUpdatesService; let service: ObjectUpdatesService;
@@ -47,7 +44,7 @@ describe('ObjectUpdatesService', () => {
}; };
store = new Store<CoreState>(undefined, undefined, undefined); store = new Store<CoreState>(undefined, undefined, undefined);
spyOn(store, 'dispatch'); 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, 'getObjectEntry').and.returnValue(observableOf(objectEntry));
spyOn(service as any, 'getFieldState').and.callFake((uuid) => { 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', () => { describe('getFieldUpdates', () => {
it('should return the list of all fields, including their update if there is one', () => { it('should return the list of all fields, including their update if there is one', () => {
const result$ = service.getFieldUpdates(url, identifiables); 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', () => { describe('isEditable', () => {
it('should return false if this identifiable is currently not editable in the store', () => { it('should return false if this identifiable is currently not editable in the store', () => {
const result$ = service.isEditable(url, identifiable1.uuid); const result$ = service.isEditable(url, identifiable1.uuid);
@@ -274,11 +209,7 @@ describe('ObjectUpdatesService', () => {
}); });
describe('when updates are emtpy', () => { describe('when updates are emtpy', () => {
beforeEach(() => { beforeEach(() => {
(service as any).getObjectEntry.and.returnValue(observableOf({ (service as any).getObjectEntry.and.returnValue(observableOf({}))
customOrder: {
changed: false
}
}))
}); });
it('should return false when there are no updates', () => { 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();
});
});
});
}); });

View File

@@ -8,16 +8,15 @@ import {
Identifiable, Identifiable,
OBJECT_UPDATES_TRASH_PATH, OBJECT_UPDATES_TRASH_PATH,
ObjectUpdatesEntry, ObjectUpdatesEntry,
ObjectUpdatesState, OrderPage, ObjectUpdatesState,
VirtualMetadataSource VirtualMetadataSource
} from './object-updates.reducer'; } from './object-updates.reducer';
import { Observable } from 'rxjs'; import { Observable } from 'rxjs';
import { import {
AddFieldUpdateAction, AddPageToCustomOrderAction, AddFieldUpdateAction,
DiscardObjectUpdatesAction, DiscardObjectUpdatesAction,
FieldChangeType, FieldChangeType,
InitializeFieldsAction, InitializeFieldsAction,
MoveFieldUpdateAction,
ReinstateObjectUpdatesAction, ReinstateObjectUpdatesAction,
RemoveFieldUpdateAction, RemoveFieldUpdateAction,
SelectVirtualMetadataAction, SelectVirtualMetadataAction,
@@ -25,11 +24,8 @@ import {
SetValidFieldUpdateAction SetValidFieldUpdateAction
} from './object-updates.actions'; } from './object-updates.actions';
import { distinctUntilChanged, filter, map, switchMap } from 'rxjs/operators'; 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 { 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> { function objectUpdatesStateSelector(): MemoizedSelector<CoreState, ObjectUpdatesState> {
return createSelector(coreSelector, (state: CoreState) => state['cache/object-updates']); return createSelector(coreSelector, (state: CoreState) => state['cache/object-updates']);
@@ -52,9 +48,7 @@ function virtualMetadataSourceSelector(url: string, source: string): MemoizedSel
*/ */
@Injectable() @Injectable()
export class ObjectUpdatesService { export class ObjectUpdatesService {
constructor(private store: Store<CoreState>, constructor(private store: Store<CoreState>) {
private comparator: ArrayMoveChangeAnalyzer<string>) {
} }
/** /**
@@ -67,28 +61,6 @@ export class ObjectUpdatesService {
this.store.dispatch(new InitializeFieldsAction(url, fields, lastModified)); 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 * Method to dispatch an AddFieldUpdateAction to the store
* @param url The page's URL for which the changes are saved * @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> { getFieldUpdatesExclusive(url: string, initialFields: Identifiable[]): Observable<FieldUpdates> {
const objectUpdates = this.getObjectEntry(url); const objectUpdates = this.getObjectEntry(url);
return objectUpdates.pipe(map((objectEntry) => { return objectUpdates.pipe(isNotEmptyOperator(), map((objectEntry) => {
const fieldUpdates: FieldUpdates = {}; const fieldUpdates: FieldUpdates = {};
for (const object of initialFields) { for (const object of initialFields) {
let fieldUpdate = objectEntry.fieldUpdates[object.uuid]; 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 * 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 * @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); 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 * 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 * @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 * @param url The page's url to check for in the store
*/ */
hasUpdates(url: string): Observable<boolean> { 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> { getLastModified(url: string): Observable<Date> {
return this.getObjectEntry(url).pipe(map((entry: ObjectUpdatesEntry) => entry.lastModified)); 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)))
)
);
}
} }

View File

@@ -201,8 +201,9 @@ export class RequestService {
* Remove all request cache providing (part of) the href * Remove all request cache providing (part of) the href
* This also includes href-to-uuid index cache * This also includes href-to-uuid index cache
* @param href A substring of the request(s) href * @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( this.store.pipe(
select(uuidsFromHrefSubstringSelector(requestIndexSelector, href)), select(uuidsFromHrefSubstringSelector(requestIndexSelector, href)),
take(1) take(1)
@@ -213,6 +214,11 @@ export class RequestService {
}); });
this.requestsOnTheirWayToTheStore = this.requestsOnTheirWayToTheStore.filter((reqHref: string) => reqHref.indexOf(href) < 0); this.requestsOnTheirWayToTheStore = this.requestsOnTheirWayToTheStore.filter((reqHref: string) => reqHref.indexOf(href) < 0);
this.indexStore.dispatch(new RemoveFromIndexBySubstringAction(IndexName.REQUEST, href)); this.indexStore.dispatch(new RemoveFromIndexBySubstringAction(IndexName.REQUEST, href));
return this.store.pipe(
select(uuidsFromHrefSubstringSelector(requestIndexSelector, href)),
map((uuids) => isEmpty(uuids))
);
} }
/** /**

View File

@@ -11,7 +11,7 @@ export function getMockRequestService(requestEntry$: Observable<RequestEntry> =
getByUUID: requestEntry$, getByUUID: requestEntry$,
uriEncodeBody: jasmine.createSpy('uriEncodeBody'), uriEncodeBody: jasmine.createSpy('uriEncodeBody'),
isCachedOrPending: false, isCachedOrPending: false,
removeByHrefSubstring: jasmine.createSpy('removeByHrefSubstring'), removeByHrefSubstring: observableOf(true),
hasByHrefObservable: observableOf(false) hasByHrefObservable: observableOf(false)
}); });
} }

View File

@@ -12,14 +12,16 @@ import { take } from 'rxjs/operators';
import { PaginationComponent } from '../pagination/pagination.component'; import { PaginationComponent } from '../pagination/pagination.component';
import { createSuccessfulRemoteDataObject } from '../remote-data.utils'; import { createSuccessfulRemoteDataObject } from '../remote-data.utils';
import { createPaginatedList } from '../testing/utils.test'; import { createPaginatedList } from '../testing/utils.test';
import { ObjectValuesPipe } from '../utils/object-values-pipe';
class MockAbstractPaginatedDragAndDropListComponent extends AbstractPaginatedDragAndDropListComponent<DSpaceObject> { class MockAbstractPaginatedDragAndDropListComponent extends AbstractPaginatedDragAndDropListComponent<DSpaceObject> {
constructor(protected objectUpdatesService: ObjectUpdatesService, constructor(protected objectUpdatesService: ObjectUpdatesService,
protected elRef: ElementRef, protected elRef: ElementRef,
protected objectValuesPipe: ObjectValuesPipe,
protected mockUrl: string, protected mockUrl: string,
protected mockObjectsRD$: Observable<RemoteData<PaginatedList<DSpaceObject>>>) { protected mockObjectsRD$: Observable<RemoteData<PaginatedList<DSpaceObject>>>) {
super(objectUpdatesService, elRef); super(objectUpdatesService, elRef, objectValuesPipe);
} }
initializeObjectsRD(): void { initializeObjectsRD(): void {
@@ -35,6 +37,7 @@ describe('AbstractPaginatedDragAndDropListComponent', () => {
let component: MockAbstractPaginatedDragAndDropListComponent; let component: MockAbstractPaginatedDragAndDropListComponent;
let objectUpdatesService: ObjectUpdatesService; let objectUpdatesService: ObjectUpdatesService;
let elRef: ElementRef; let elRef: ElementRef;
let objectValuesPipe: ObjectValuesPipe;
const url = 'mock-abstract-paginated-drag-and-drop-list-component'; const url = 'mock-abstract-paginated-drag-and-drop-list-component';
@@ -52,32 +55,26 @@ describe('AbstractPaginatedDragAndDropListComponent', () => {
beforeEach(() => { beforeEach(() => {
objectUpdatesService = jasmine.createSpyObj('objectUpdatesService', { objectUpdatesService = jasmine.createSpyObj('objectUpdatesService', {
initializeWithCustomOrder: {}, initialize: {},
addPageToCustomOrder: {}, getFieldUpdatesExclusive: observableOf(updates)
getFieldUpdatesByCustomOrder: observableOf(updates),
saveMoveFieldUpdate: {}
}); });
elRef = { elRef = {
nativeElement: jasmine.createSpyObj('nativeElement', { nativeElement: jasmine.createSpyObj('nativeElement', {
querySelector: {} querySelector: {}
}) })
}; };
objectValuesPipe = new ObjectValuesPipe();
paginationComponent = jasmine.createSpyObj('paginationComponent', { paginationComponent = jasmine.createSpyObj('paginationComponent', {
doPageChange: {} doPageChange: {}
}); });
objectsRD$ = new BehaviorSubject(objectsRD); objectsRD$ = new BehaviorSubject(objectsRD);
component = new MockAbstractPaginatedDragAndDropListComponent(objectUpdatesService, elRef, url, objectsRD$); component = new MockAbstractPaginatedDragAndDropListComponent(objectUpdatesService, elRef, objectValuesPipe, url, objectsRD$);
component.paginationComponent = paginationComponent; component.paginationComponent = paginationComponent;
component.ngOnInit(); component.ngOnInit();
}); });
it('should call initializeWithCustomOrder to initialize the first page and add it to initializedPages', (done) => { it('should call initialize to initialize the objects in the store', () => {
expect(component.initializedPages.indexOf(0)).toBeLessThan(0); expect(objectUpdatesService.initialize).toHaveBeenCalled();
component.updates$.pipe(take(1)).subscribe(() => {
expect(objectUpdatesService.initializeWithCustomOrder).toHaveBeenCalled();
expect(component.initializedPages.indexOf(0)).toBeGreaterThanOrEqual(0);
done();
});
}); });
it('should initialize the updates correctly', (done) => { 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', () => { describe('switchPage', () => {
const page = 3; const page = 3;
@@ -149,30 +109,31 @@ describe('AbstractPaginatedDragAndDropListComponent', () => {
beforeEach(() => { beforeEach(() => {
elRef.nativeElement.querySelector.and.returnValue(hoverElement); elRef.nativeElement.querySelector.and.returnValue(hoverElement);
component.initializedPages.push(hoverPage - 1); spyOn(component.dropObject, 'emit');
component.drop(event); component.drop(event);
}); });
it('should detect the page and set currentPage$ to its value', () => { it('should send out a dropObject event with the expected processed paginated indexes', () => {
expect(component.currentPage$.value).toEqual(hoverPage); expect(component.dropObject.emit).toHaveBeenCalledWith(Object.assign({
}); fromIndex: ((component.currentPage$.value - 1) * component.pageSize) + event.previousIndex,
toIndex: ((hoverPage - 1) * component.pageSize),
it('should detect the page and update the pagination component with its value', () => { finish: jasmine.anything()
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);
}); });
}); });
describe('when the user is not hovering over a new page', () => { describe('when the user is not hovering over a new page', () => {
beforeEach(() => { beforeEach(() => {
spyOn(component.dropObject, 'emit');
component.drop(event); component.drop(event);
}); });
it('should send out a saveMoveFieldUpdate with the correct values', () => { it('should send out a dropObject event with the expected properties', () => {
expect(objectUpdatesService.saveMoveFieldUpdate).toHaveBeenCalledWith(url, event.previousIndex, event.currentIndex, 0, 0); expect(component.dropObject.emit).toHaveBeenCalledWith(Object.assign({
fromIndex: event.previousIndex,
toIndex: event.currentIndex,
finish: jasmine.anything()
}));
}); });
}); });
}); });

View File

@@ -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 { Observable } from 'rxjs/internal/Observable';
import { RemoteData } from '../../core/data/remote-data'; import { RemoteData } from '../../core/data/remote-data';
import { PaginatedList } from '../../core/data/paginated-list'; import { PaginatedList } from '../../core/data/paginated-list';
import { PaginationComponentOptions } from '../pagination/pagination-component-options.model'; import { PaginationComponentOptions } from '../pagination/pagination-component-options.model';
import { BehaviorSubject } from 'rxjs/internal/BehaviorSubject'; import { BehaviorSubject } from 'rxjs/internal/BehaviorSubject';
import { ObjectUpdatesService } from '../../core/data/object-updates/object-updates.service'; import { ObjectUpdatesService } from '../../core/data/object-updates/object-updates.service';
import { switchMap, take, tap } from 'rxjs/operators'; import { distinctUntilChanged, map, switchMap, take } from 'rxjs/operators';
import { hasValue, isEmpty, isNotEmpty } from '../empty.util'; import { hasValue, isNotEmpty } from '../empty.util';
import { paginatedListToArray } from '../../core/shared/operators'; import { paginatedListToArray } from '../../core/shared/operators';
import { DSpaceObject } from '../../core/shared/dspace-object.model'; import { DSpaceObject } from '../../core/shared/dspace-object.model';
import { CdkDragDrop } from '@angular/cdk/drag-drop'; import { CdkDragDrop, moveItemInArray } from '@angular/cdk/drag-drop';
import { ElementRef, ViewChild } from '@angular/core'; import { ElementRef, EventEmitter, OnDestroy, Output, ViewChild } from '@angular/core';
import { PaginationComponent } from '../pagination/pagination.component'; 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 * 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. * 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 * Dragging an object on top of a page number will automatically detect the page it's being dropped on and send a
* to the store and add the object on top of that page. * 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: * To extend this component, it is important to make sure to:
* - Initialize objectsRD$ within the initializeObjectsRD() method * - 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 * 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 * A view on the child pagination component
*/ */
@ViewChild(PaginationComponent, {static: false}) paginationComponent: PaginationComponent; @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 * 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>; 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 * The amount of objects to display per page
*/ */
@@ -70,23 +93,21 @@ export abstract class AbstractPaginatedDragAndDropListComponent<T extends DSpace
currentPage$ = new BehaviorSubject<number>(1); 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: { subs: Subscription[] = [];
fromIndex: number,
toIndex: number,
fromPage: number,
toPage: number,
field?: T
};
protected constructor(protected objectUpdatesService: ObjectUpdatesService, 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 * 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 { initializeUpdates(): void {
this.objectsRD$.pipe(
paginatedListToArray(),
take(1)
).subscribe((objects: T[]) => {
this.objectUpdatesService.initialize(this.url, objects, new Date());
});
this.updates$ = this.objectsRD$.pipe( this.updates$ = this.objectsRD$.pipe(
paginatedListToArray(), paginatedListToArray(),
tap((objects: T[]) => { switchMap((objects: T[]) => this.objectUpdatesService.getFieldUpdatesExclusive(this.url, objects))
// 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; this.subs.push(
if (isEmpty(this.initializedPages)) { this.updates$.pipe(
// No updates have been initialized yet for this list, initialize the first page map((fieldUpdates) => this.objectValuesPipe.transform(fieldUpdates)),
this.objectUpdatesService.initializeWithCustomOrder(this.url, objects, new Date(), this.pageSize, updatesPage); distinctUntilChanged(compareArraysUsingFieldUuids())
this.initializedPages.push(updatesPage); ).subscribe((updateValues) => {
} else if (this.initializedPages.indexOf(updatesPage) < 0) { this.customOrder = updateValues.map((fieldUpdate) => fieldUpdate.field.uuid);
// 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 // We received new values, stop displaying a loading indicator if it's present
this.objectUpdatesService.addPageToCustomOrder(this.url, objects, updatesPage); this.loading$.next(false);
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.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 * 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 * @param event
*/ */
drop(event: CdkDragDrop<any>) { 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 // 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'); const droppedOnElement = this.elRef.nativeElement.querySelector('.page-item:hover');
if (hasValue(droppedOnElement) && hasValue(droppedOnElement.textContent)) { if (hasValue(droppedOnElement) && hasValue(droppedOnElement.textContent)) {
// The user is hovering over a page, fetch the page's number from the element // The user is hovering over a page, fetch the page's number from the element
const page = Number(droppedOnElement.textContent); const droppedPage = Number(droppedOnElement.textContent);
if (hasValue(page) && !Number.isNaN(page)) { if (hasValue(droppedPage) && !Number.isNaN(droppedPage)) {
const id = event.item.element.nativeElement.id; dropPage = droppedPage - 1;
this.updates$.pipe(take(1)).subscribe((updates: FieldUpdates) => { dropIndex = 0;
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();
} }
});
} }
} 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 * unsub all subscriptions
* update present and clear the update afterwards.
*/ */
fireToUpdate() { ngOnDestroy(): void {
if (hasValue(this.toUpdate)) { this.subs.filter((sub) => hasValue(sub)).forEach((sub) => sub.unsubscribe());
this.objectUpdatesService.saveMoveFieldUpdate(this.url, this.toUpdate.fromIndex, this.toUpdate.toIndex, this.toUpdate.fromPage, this.toUpdate.toPage, this.toUpdate.field);
this.toUpdate = undefined;
}
} }
} }