mirror of
https://github.com/DSpace/dspace-angular.git
synced 2025-10-18 15:33:04 +00:00
102039: Use patch to delete multiple bitstreams at once
This commit is contained in:
@@ -1,13 +1,14 @@
|
||||
import { TestBed } from '@angular/core/testing';
|
||||
import { BitstreamDataService } from './bitstream-data.service';
|
||||
import { ObjectCacheService } from '../cache/object-cache.service';
|
||||
import { RequestService } from './request.service';
|
||||
import { Bitstream } from '../shared/bitstream.model';
|
||||
import { HALEndpointService } from '../shared/hal-endpoint.service';
|
||||
import { BitstreamFormatDataService } from './bitstream-format-data.service';
|
||||
import { of as observableOf } from 'rxjs';
|
||||
import { Observable, of as observableOf } from 'rxjs';
|
||||
import { BitstreamFormat } from '../shared/bitstream-format.model';
|
||||
import { BitstreamFormatSupportLevel } from '../shared/bitstream-format-support-level';
|
||||
import { PutRequest } from './request.models';
|
||||
import { PatchRequest, PutRequest } from './request.models';
|
||||
import { getMockRequestService } from '../../shared/mocks/request.service.mock';
|
||||
import { HALEndpointServiceStub } from '../../shared/testing/hal-endpoint-service.stub';
|
||||
import { RemoteDataBuildService } from '../cache/builders/remote-data-build.service';
|
||||
@@ -15,6 +16,11 @@ import { getMockRemoteDataBuildService } from '../../shared/mocks/remote-data-bu
|
||||
import { testSearchDataImplementation } from './base/search-data.spec';
|
||||
import { testPatchDataImplementation } from './base/patch-data.spec';
|
||||
import { testDeleteDataImplementation } from './base/delete-data.spec';
|
||||
import { DSOChangeAnalyzer } from './dso-change-analyzer.service';
|
||||
import { NotificationsService } from '../../shared/notifications/notifications.service';
|
||||
import objectContaining = jasmine.objectContaining;
|
||||
import { RemoteData } from './remote-data';
|
||||
import { FollowLinkConfig } from '../../shared/utils/follow-link-config.model';
|
||||
|
||||
describe('BitstreamDataService', () => {
|
||||
let service: BitstreamDataService;
|
||||
@@ -25,10 +31,18 @@ describe('BitstreamDataService', () => {
|
||||
let rdbService: RemoteDataBuildService;
|
||||
const bitstreamFormatHref = 'rest-api/bitstreamformats';
|
||||
|
||||
const bitstream = Object.assign(new Bitstream(), {
|
||||
uuid: 'fake-bitstream',
|
||||
const bitstream1 = Object.assign(new Bitstream(), {
|
||||
id: 'fake-bitstream1',
|
||||
uuid: 'fake-bitstream1',
|
||||
_links: {
|
||||
self: { href: 'fake-bitstream-self' }
|
||||
self: { href: 'fake-bitstream1-self' }
|
||||
}
|
||||
});
|
||||
const bitstream2 = Object.assign(new Bitstream(), {
|
||||
id: 'fake-bitstream2',
|
||||
uuid: 'fake-bitstream2',
|
||||
_links: {
|
||||
self: { href: 'fake-bitstream2-self' }
|
||||
}
|
||||
});
|
||||
const format = Object.assign(new BitstreamFormat(), {
|
||||
@@ -50,7 +64,18 @@ describe('BitstreamDataService', () => {
|
||||
});
|
||||
rdbService = getMockRemoteDataBuildService();
|
||||
|
||||
service = new BitstreamDataService(requestService, rdbService, objectCache, halService, null, bitstreamFormatService, null, null);
|
||||
TestBed.configureTestingModule({
|
||||
providers: [
|
||||
{ provide: ObjectCacheService, useValue: objectCache },
|
||||
{ provide: RequestService, useValue: requestService },
|
||||
{ provide: HALEndpointService, useValue: halService },
|
||||
{ provide: BitstreamFormatDataService, useValue: bitstreamFormatService },
|
||||
{ provide: RemoteDataBuildService, useValue: rdbService },
|
||||
{ provide: DSOChangeAnalyzer, useValue: {} },
|
||||
{ provide: NotificationsService, useValue: {} },
|
||||
],
|
||||
});
|
||||
service = TestBed.inject(BitstreamDataService);
|
||||
});
|
||||
|
||||
describe('composition', () => {
|
||||
@@ -62,11 +87,49 @@ describe('BitstreamDataService', () => {
|
||||
|
||||
describe('when updating the bitstream\'s format', () => {
|
||||
beforeEach(() => {
|
||||
service.updateFormat(bitstream, format);
|
||||
service.updateFormat(bitstream1, format);
|
||||
});
|
||||
|
||||
it('should send a put request', () => {
|
||||
expect(requestService.send).toHaveBeenCalledWith(jasmine.any(PutRequest));
|
||||
});
|
||||
});
|
||||
|
||||
describe('removeMultiple', () => {
|
||||
function mockBuildFromRequestUUIDAndAwait(requestUUID$: string | Observable<string>, callback: (rd?: RemoteData<any>) => Observable<unknown>, ..._linksToFollow: FollowLinkConfig<any>[]): Observable<RemoteData<any>> {
|
||||
callback();
|
||||
return;
|
||||
}
|
||||
|
||||
beforeEach(() => {
|
||||
spyOn(service, 'invalidateByHref');
|
||||
spyOn(rdbService, 'buildFromRequestUUIDAndAwait').and.callFake((requestUUID$: string | Observable<string>, callback: (rd?: RemoteData<any>) => Observable<unknown>, ...linksToFollow: FollowLinkConfig<any>[]) => mockBuildFromRequestUUIDAndAwait(requestUUID$, callback, ...linksToFollow));
|
||||
});
|
||||
|
||||
it('should be able to 1 bitstream', () => {
|
||||
service.removeMultiple([bitstream1]);
|
||||
|
||||
expect(requestService.send).toHaveBeenCalledWith(objectContaining({
|
||||
href: `${url}/bitstreams`,
|
||||
body: [
|
||||
{ op: 'remove', path: '/bitstreams/fake-bitstream1' },
|
||||
],
|
||||
} as PatchRequest));
|
||||
expect(service.invalidateByHref).toHaveBeenCalledWith('fake-bitstream1-self');
|
||||
});
|
||||
|
||||
it('should be able to delete multiple bitstreams', () => {
|
||||
service.removeMultiple([bitstream1, bitstream2]);
|
||||
|
||||
expect(requestService.send).toHaveBeenCalledWith(objectContaining({
|
||||
href: `${url}/bitstreams`,
|
||||
body: [
|
||||
{ op: 'remove', path: '/bitstreams/fake-bitstream1' },
|
||||
{ op: 'remove', path: '/bitstreams/fake-bitstream2' },
|
||||
],
|
||||
} as PatchRequest));
|
||||
expect(service.invalidateByHref).toHaveBeenCalledWith('fake-bitstream1-self');
|
||||
expect(service.invalidateByHref).toHaveBeenCalledWith('fake-bitstream2-self');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
@@ -1,7 +1,7 @@
|
||||
import { HttpHeaders } from '@angular/common/http';
|
||||
import { Injectable } from '@angular/core';
|
||||
import { combineLatest as observableCombineLatest, Observable } from 'rxjs';
|
||||
import { map, switchMap, take } from 'rxjs/operators';
|
||||
import { find, map, switchMap, take } from 'rxjs/operators';
|
||||
import { hasValue } from '../../shared/empty.util';
|
||||
import { FollowLinkConfig } from '../../shared/utils/follow-link-config.model';
|
||||
import { RemoteDataBuildService } from '../cache/builders/remote-data-build.service';
|
||||
@@ -14,7 +14,7 @@ import { Item } from '../shared/item.model';
|
||||
import { BundleDataService } from './bundle-data.service';
|
||||
import { buildPaginatedList, PaginatedList } from './paginated-list.model';
|
||||
import { RemoteData } from './remote-data';
|
||||
import { PutRequest } from './request.models';
|
||||
import { PatchRequest, PutRequest } from './request.models';
|
||||
import { RequestService } from './request.service';
|
||||
import { BitstreamFormatDataService } from './bitstream-format-data.service';
|
||||
import { BitstreamFormat } from '../shared/bitstream-format.model';
|
||||
@@ -33,7 +33,7 @@ import { NotificationsService } from '../../shared/notifications/notifications.s
|
||||
import { NoContent } from '../shared/NoContent.model';
|
||||
import { IdentifiableDataService } from './base/identifiable-data.service';
|
||||
import { dataService } from './base/data-service.decorator';
|
||||
import { Operation } from 'fast-json-patch';
|
||||
import { Operation, RemoveOperation } from 'fast-json-patch';
|
||||
|
||||
/**
|
||||
* A service to retrieve {@link Bitstream}s from the REST API
|
||||
@@ -277,4 +277,34 @@ export class BitstreamDataService extends IdentifiableDataService<Bitstream> imp
|
||||
deleteByHref(href: string, copyVirtualMetadata?: string[]): Observable<RemoteData<NoContent>> {
|
||||
return this.deleteData.deleteByHref(href, copyVirtualMetadata);
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete multiple {@link Bitstream}s at once by sending a PATCH request to the backend
|
||||
*
|
||||
* @param bitstreams The bitstreams that should be removed
|
||||
*/
|
||||
removeMultiple(bitstreams: Bitstream[]): Observable<RemoteData<NoContent>> {
|
||||
const operations: RemoveOperation[] = bitstreams.map((bitstream: Bitstream) => {
|
||||
return {
|
||||
op: 'remove',
|
||||
path: `/bitstreams/${bitstream.id}`,
|
||||
};
|
||||
});
|
||||
const requestId: string = this.requestService.generateRequestId();
|
||||
|
||||
const hrefObs: Observable<string> = this.halService.getEndpoint(this.linkPath);
|
||||
|
||||
hrefObs.pipe(
|
||||
find((href: string) => hasValue(href)),
|
||||
).subscribe((href: string) => {
|
||||
const request = new PatchRequest(requestId, href, operations);
|
||||
if (hasValue(this.responseMsToLive)) {
|
||||
request.responseMsToLive = this.responseMsToLive;
|
||||
}
|
||||
this.requestService.send(request);
|
||||
});
|
||||
|
||||
return this.rdbService.buildFromRequestUUIDAndAwait(requestId, () => observableCombineLatest(bitstreams.map((bitstream: Bitstream) => this.invalidateByHref(bitstream._links.self.href))));
|
||||
}
|
||||
|
||||
}
|
||||
|
@@ -25,6 +25,7 @@ import { getMockRequestService } from '../../../shared/mocks/request.service.moc
|
||||
import { createSuccessfulRemoteDataObject, createSuccessfulRemoteDataObject$ } from '../../../shared/remote-data.utils';
|
||||
import { createPaginatedList } from '../../../shared/testing/utils.test';
|
||||
import { FieldChangeType } from '../../../core/data/object-updates/field-change-type.model';
|
||||
import { BitstreamDataServiceStub } from '../../../shared/testing/bitstream-data-service.stub';
|
||||
|
||||
let comp: ItemBitstreamsComponent;
|
||||
let fixture: ComponentFixture<ItemBitstreamsComponent>;
|
||||
@@ -71,7 +72,7 @@ let objectUpdatesService: ObjectUpdatesService;
|
||||
let router: any;
|
||||
let route: ActivatedRoute;
|
||||
let notificationsService: NotificationsService;
|
||||
let bitstreamService: BitstreamDataService;
|
||||
let bitstreamService: BitstreamDataServiceStub;
|
||||
let objectCache: ObjectCacheService;
|
||||
let requestService: RequestService;
|
||||
let searchConfig: SearchConfigurationService;
|
||||
@@ -112,9 +113,7 @@ describe('ItemBitstreamsComponent', () => {
|
||||
success: successNotification
|
||||
}
|
||||
);
|
||||
bitstreamService = jasmine.createSpyObj('bitstreamService', {
|
||||
delete: jasmine.createSpy('delete')
|
||||
});
|
||||
bitstreamService = new BitstreamDataServiceStub();
|
||||
objectCache = jasmine.createSpyObj('objectCache', {
|
||||
remove: jasmine.createSpy('remove')
|
||||
});
|
||||
@@ -179,15 +178,16 @@ describe('ItemBitstreamsComponent', () => {
|
||||
|
||||
describe('when submit is called', () => {
|
||||
beforeEach(() => {
|
||||
spyOn(bitstreamService, 'removeMultiple').and.callThrough();
|
||||
comp.submit();
|
||||
});
|
||||
|
||||
it('should call delete on the bitstreamService for the marked field', () => {
|
||||
expect(bitstreamService.delete).toHaveBeenCalledWith(bitstream2.id);
|
||||
it('should call removeMultiple on the bitstreamService for the marked field', () => {
|
||||
expect(bitstreamService.removeMultiple).toHaveBeenCalledWith([bitstream2]);
|
||||
});
|
||||
|
||||
it('should not call delete on the bitstreamService for the unmarked field', () => {
|
||||
expect(bitstreamService.delete).not.toHaveBeenCalledWith(bitstream1.id);
|
||||
it('should not call removeMultiple on the bitstreamService for the unmarked field', () => {
|
||||
expect(bitstreamService.removeMultiple).not.toHaveBeenCalledWith([bitstream1]);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -210,7 +210,6 @@ describe('ItemBitstreamsComponent', () => {
|
||||
comp.dropBitstream(bundle, {
|
||||
fromIndex: 0,
|
||||
toIndex: 50,
|
||||
// eslint-disable-next-line no-empty, @typescript-eslint/no-empty-function
|
||||
finish: () => {
|
||||
done();
|
||||
}
|
||||
|
@@ -1,7 +1,7 @@
|
||||
import { ChangeDetectorRef, Component, NgZone, OnDestroy } from '@angular/core';
|
||||
import { AbstractItemUpdateComponent } from '../abstract-item-update/abstract-item-update.component';
|
||||
import { filter, map, switchMap, take } from 'rxjs/operators';
|
||||
import { Observable, of as observableOf, Subscription, zip as observableZip } from 'rxjs';
|
||||
import { Observable, Subscription, zip as observableZip } from 'rxjs';
|
||||
import { ItemDataService } from '../../../core/data/item-data.service';
|
||||
import { ObjectUpdatesService } from '../../../core/data/object-updates/object-updates.service';
|
||||
import { ActivatedRoute, Router } from '@angular/router';
|
||||
@@ -133,20 +133,16 @@ export class ItemBitstreamsComponent extends AbstractItemUpdateComponent impleme
|
||||
);
|
||||
|
||||
// Send out delete requests for all deleted bitstreams
|
||||
const removedResponses$ = removedBitstreams$.pipe(
|
||||
const removedResponses$: Observable<RemoteData<NoContent>> = removedBitstreams$.pipe(
|
||||
take(1),
|
||||
switchMap((removedBistreams: Bitstream[]) => {
|
||||
if (isNotEmpty(removedBistreams)) {
|
||||
return observableZip(...removedBistreams.map((bitstream: Bitstream) => this.bitstreamService.delete(bitstream.id)));
|
||||
} else {
|
||||
return observableOf(undefined);
|
||||
}
|
||||
switchMap((removedBitstreams: Bitstream[]) => {
|
||||
return this.bitstreamService.removeMultiple(removedBitstreams);
|
||||
})
|
||||
);
|
||||
|
||||
// Perform the setup actions from above in order and display notifications
|
||||
removedResponses$.pipe(take(1)).subscribe((responses: RemoteData<NoContent>[]) => {
|
||||
this.displayNotifications('item.edit.bitstreams.notifications.remove', responses);
|
||||
removedResponses$.subscribe((responses: RemoteData<NoContent>) => {
|
||||
this.displayNotifications('item.edit.bitstreams.notifications.remove', [responses]);
|
||||
this.submitting = false;
|
||||
});
|
||||
}
|
||||
|
13
src/app/shared/testing/bitstream-data-service.stub.ts
Normal file
13
src/app/shared/testing/bitstream-data-service.stub.ts
Normal file
@@ -0,0 +1,13 @@
|
||||
import { Bitstream } from '../../core/shared/bitstream.model';
|
||||
import { Observable, of as observableOf } from 'rxjs';
|
||||
import { RemoteData } from '../../core/data/remote-data';
|
||||
import { NoContent } from '../../core/shared/NoContent.model';
|
||||
import { RequestEntryState } from '../../core/data/request-entry-state.model';
|
||||
|
||||
export class BitstreamDataServiceStub {
|
||||
|
||||
removeMultiple(_bitstreams: Bitstream[]): Observable<RemoteData<NoContent>> {
|
||||
return observableOf(new RemoteData(0, 0, 0, RequestEntryState.Success));
|
||||
}
|
||||
|
||||
}
|
Reference in New Issue
Block a user