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 { BitstreamDataService } from './bitstream-data.service';
|
||||||
import { ObjectCacheService } from '../cache/object-cache.service';
|
import { ObjectCacheService } from '../cache/object-cache.service';
|
||||||
import { RequestService } from './request.service';
|
import { RequestService } from './request.service';
|
||||||
import { Bitstream } from '../shared/bitstream.model';
|
import { Bitstream } from '../shared/bitstream.model';
|
||||||
import { HALEndpointService } from '../shared/hal-endpoint.service';
|
import { HALEndpointService } from '../shared/hal-endpoint.service';
|
||||||
import { BitstreamFormatDataService } from './bitstream-format-data.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 { BitstreamFormat } from '../shared/bitstream-format.model';
|
||||||
import { BitstreamFormatSupportLevel } from '../shared/bitstream-format-support-level';
|
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 { getMockRequestService } from '../../shared/mocks/request.service.mock';
|
||||||
import { HALEndpointServiceStub } from '../../shared/testing/hal-endpoint-service.stub';
|
import { HALEndpointServiceStub } from '../../shared/testing/hal-endpoint-service.stub';
|
||||||
import { RemoteDataBuildService } from '../cache/builders/remote-data-build.service';
|
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 { testSearchDataImplementation } from './base/search-data.spec';
|
||||||
import { testPatchDataImplementation } from './base/patch-data.spec';
|
import { testPatchDataImplementation } from './base/patch-data.spec';
|
||||||
import { testDeleteDataImplementation } from './base/delete-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', () => {
|
describe('BitstreamDataService', () => {
|
||||||
let service: BitstreamDataService;
|
let service: BitstreamDataService;
|
||||||
@@ -25,10 +31,18 @@ describe('BitstreamDataService', () => {
|
|||||||
let rdbService: RemoteDataBuildService;
|
let rdbService: RemoteDataBuildService;
|
||||||
const bitstreamFormatHref = 'rest-api/bitstreamformats';
|
const bitstreamFormatHref = 'rest-api/bitstreamformats';
|
||||||
|
|
||||||
const bitstream = Object.assign(new Bitstream(), {
|
const bitstream1 = Object.assign(new Bitstream(), {
|
||||||
uuid: 'fake-bitstream',
|
id: 'fake-bitstream1',
|
||||||
|
uuid: 'fake-bitstream1',
|
||||||
_links: {
|
_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(), {
|
const format = Object.assign(new BitstreamFormat(), {
|
||||||
@@ -50,7 +64,18 @@ describe('BitstreamDataService', () => {
|
|||||||
});
|
});
|
||||||
rdbService = getMockRemoteDataBuildService();
|
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', () => {
|
describe('composition', () => {
|
||||||
@@ -62,11 +87,49 @@ describe('BitstreamDataService', () => {
|
|||||||
|
|
||||||
describe('when updating the bitstream\'s format', () => {
|
describe('when updating the bitstream\'s format', () => {
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
service.updateFormat(bitstream, format);
|
service.updateFormat(bitstream1, format);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should send a put request', () => {
|
it('should send a put request', () => {
|
||||||
expect(requestService.send).toHaveBeenCalledWith(jasmine.any(PutRequest));
|
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 { HttpHeaders } from '@angular/common/http';
|
||||||
import { Injectable } from '@angular/core';
|
import { Injectable } from '@angular/core';
|
||||||
import { combineLatest as observableCombineLatest, Observable } from 'rxjs';
|
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 { hasValue } from '../../shared/empty.util';
|
||||||
import { FollowLinkConfig } from '../../shared/utils/follow-link-config.model';
|
import { FollowLinkConfig } from '../../shared/utils/follow-link-config.model';
|
||||||
import { RemoteDataBuildService } from '../cache/builders/remote-data-build.service';
|
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 { BundleDataService } from './bundle-data.service';
|
||||||
import { buildPaginatedList, PaginatedList } from './paginated-list.model';
|
import { buildPaginatedList, PaginatedList } from './paginated-list.model';
|
||||||
import { RemoteData } from './remote-data';
|
import { RemoteData } from './remote-data';
|
||||||
import { PutRequest } from './request.models';
|
import { PatchRequest, PutRequest } from './request.models';
|
||||||
import { RequestService } from './request.service';
|
import { RequestService } from './request.service';
|
||||||
import { BitstreamFormatDataService } from './bitstream-format-data.service';
|
import { BitstreamFormatDataService } from './bitstream-format-data.service';
|
||||||
import { BitstreamFormat } from '../shared/bitstream-format.model';
|
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 { NoContent } from '../shared/NoContent.model';
|
||||||
import { IdentifiableDataService } from './base/identifiable-data.service';
|
import { IdentifiableDataService } from './base/identifiable-data.service';
|
||||||
import { dataService } from './base/data-service.decorator';
|
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
|
* 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>> {
|
deleteByHref(href: string, copyVirtualMetadata?: string[]): Observable<RemoteData<NoContent>> {
|
||||||
return this.deleteData.deleteByHref(href, copyVirtualMetadata);
|
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 { createSuccessfulRemoteDataObject, createSuccessfulRemoteDataObject$ } from '../../../shared/remote-data.utils';
|
||||||
import { createPaginatedList } from '../../../shared/testing/utils.test';
|
import { createPaginatedList } from '../../../shared/testing/utils.test';
|
||||||
import { FieldChangeType } from '../../../core/data/object-updates/field-change-type.model';
|
import { FieldChangeType } from '../../../core/data/object-updates/field-change-type.model';
|
||||||
|
import { BitstreamDataServiceStub } from '../../../shared/testing/bitstream-data-service.stub';
|
||||||
|
|
||||||
let comp: ItemBitstreamsComponent;
|
let comp: ItemBitstreamsComponent;
|
||||||
let fixture: ComponentFixture<ItemBitstreamsComponent>;
|
let fixture: ComponentFixture<ItemBitstreamsComponent>;
|
||||||
@@ -71,7 +72,7 @@ let objectUpdatesService: ObjectUpdatesService;
|
|||||||
let router: any;
|
let router: any;
|
||||||
let route: ActivatedRoute;
|
let route: ActivatedRoute;
|
||||||
let notificationsService: NotificationsService;
|
let notificationsService: NotificationsService;
|
||||||
let bitstreamService: BitstreamDataService;
|
let bitstreamService: BitstreamDataServiceStub;
|
||||||
let objectCache: ObjectCacheService;
|
let objectCache: ObjectCacheService;
|
||||||
let requestService: RequestService;
|
let requestService: RequestService;
|
||||||
let searchConfig: SearchConfigurationService;
|
let searchConfig: SearchConfigurationService;
|
||||||
@@ -112,9 +113,7 @@ describe('ItemBitstreamsComponent', () => {
|
|||||||
success: successNotification
|
success: successNotification
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
bitstreamService = jasmine.createSpyObj('bitstreamService', {
|
bitstreamService = new BitstreamDataServiceStub();
|
||||||
delete: jasmine.createSpy('delete')
|
|
||||||
});
|
|
||||||
objectCache = jasmine.createSpyObj('objectCache', {
|
objectCache = jasmine.createSpyObj('objectCache', {
|
||||||
remove: jasmine.createSpy('remove')
|
remove: jasmine.createSpy('remove')
|
||||||
});
|
});
|
||||||
@@ -179,15 +178,16 @@ describe('ItemBitstreamsComponent', () => {
|
|||||||
|
|
||||||
describe('when submit is called', () => {
|
describe('when submit is called', () => {
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
|
spyOn(bitstreamService, 'removeMultiple').and.callThrough();
|
||||||
comp.submit();
|
comp.submit();
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should call delete on the bitstreamService for the marked field', () => {
|
it('should call removeMultiple on the bitstreamService for the marked field', () => {
|
||||||
expect(bitstreamService.delete).toHaveBeenCalledWith(bitstream2.id);
|
expect(bitstreamService.removeMultiple).toHaveBeenCalledWith([bitstream2]);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should not call delete on the bitstreamService for the unmarked field', () => {
|
it('should not call removeMultiple on the bitstreamService for the unmarked field', () => {
|
||||||
expect(bitstreamService.delete).not.toHaveBeenCalledWith(bitstream1.id);
|
expect(bitstreamService.removeMultiple).not.toHaveBeenCalledWith([bitstream1]);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -210,7 +210,6 @@ describe('ItemBitstreamsComponent', () => {
|
|||||||
comp.dropBitstream(bundle, {
|
comp.dropBitstream(bundle, {
|
||||||
fromIndex: 0,
|
fromIndex: 0,
|
||||||
toIndex: 50,
|
toIndex: 50,
|
||||||
// eslint-disable-next-line no-empty, @typescript-eslint/no-empty-function
|
|
||||||
finish: () => {
|
finish: () => {
|
||||||
done();
|
done();
|
||||||
}
|
}
|
||||||
|
@@ -1,7 +1,7 @@
|
|||||||
import { ChangeDetectorRef, Component, NgZone, 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 } from 'rxjs/operators';
|
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 { ItemDataService } from '../../../core/data/item-data.service';
|
||||||
import { ObjectUpdatesService } from '../../../core/data/object-updates/object-updates.service';
|
import { ObjectUpdatesService } from '../../../core/data/object-updates/object-updates.service';
|
||||||
import { ActivatedRoute, Router } from '@angular/router';
|
import { ActivatedRoute, Router } from '@angular/router';
|
||||||
@@ -133,20 +133,16 @@ export class ItemBitstreamsComponent extends AbstractItemUpdateComponent impleme
|
|||||||
);
|
);
|
||||||
|
|
||||||
// Send out delete requests for all deleted bitstreams
|
// Send out delete requests for all deleted bitstreams
|
||||||
const removedResponses$ = removedBitstreams$.pipe(
|
const removedResponses$: Observable<RemoteData<NoContent>> = removedBitstreams$.pipe(
|
||||||
take(1),
|
take(1),
|
||||||
switchMap((removedBistreams: Bitstream[]) => {
|
switchMap((removedBitstreams: Bitstream[]) => {
|
||||||
if (isNotEmpty(removedBistreams)) {
|
return this.bitstreamService.removeMultiple(removedBitstreams);
|
||||||
return observableZip(...removedBistreams.map((bitstream: Bitstream) => this.bitstreamService.delete(bitstream.id)));
|
|
||||||
} else {
|
|
||||||
return observableOf(undefined);
|
|
||||||
}
|
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
|
|
||||||
// Perform the setup actions from above in order and display notifications
|
// Perform the setup actions from above in order and display notifications
|
||||||
removedResponses$.pipe(take(1)).subscribe((responses: RemoteData<NoContent>[]) => {
|
removedResponses$.subscribe((responses: RemoteData<NoContent>) => {
|
||||||
this.displayNotifications('item.edit.bitstreams.notifications.remove', responses);
|
this.displayNotifications('item.edit.bitstreams.notifications.remove', [responses]);
|
||||||
this.submitting = false;
|
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