diff --git a/src/app/core/data/bitstream-data.service.spec.ts b/src/app/core/data/bitstream-data.service.spec.ts index c67eaa2d68..89178f8dd2 100644 --- a/src/app/core/data/bitstream-data.service.spec.ts +++ b/src/app/core/data/bitstream-data.service.spec.ts @@ -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, callback: (rd?: RemoteData) => Observable, ..._linksToFollow: FollowLinkConfig[]): Observable> { + callback(); + return; + } + + beforeEach(() => { + spyOn(service, 'invalidateByHref'); + spyOn(rdbService, 'buildFromRequestUUIDAndAwait').and.callFake((requestUUID$: string | Observable, callback: (rd?: RemoteData) => Observable, ...linksToFollow: FollowLinkConfig[]) => 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'); + }); + }); }); diff --git a/src/app/core/data/bitstream-data.service.ts b/src/app/core/data/bitstream-data.service.ts index 6bdcefe187..bb4ec28166 100644 --- a/src/app/core/data/bitstream-data.service.ts +++ b/src/app/core/data/bitstream-data.service.ts @@ -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 imp deleteByHref(href: string, copyVirtualMetadata?: string[]): Observable> { 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> { + const operations: RemoveOperation[] = bitstreams.map((bitstream: Bitstream) => { + return { + op: 'remove', + path: `/bitstreams/${bitstream.id}`, + }; + }); + const requestId: string = this.requestService.generateRequestId(); + + const hrefObs: Observable = 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)))); + } + } diff --git a/src/app/item-page/edit-item-page/item-bitstreams/item-bitstreams.component.spec.ts b/src/app/item-page/edit-item-page/item-bitstreams/item-bitstreams.component.spec.ts index 67d047d776..10e1812131 100644 --- a/src/app/item-page/edit-item-page/item-bitstreams/item-bitstreams.component.spec.ts +++ b/src/app/item-page/edit-item-page/item-bitstreams/item-bitstreams.component.spec.ts @@ -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; @@ -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(); } diff --git a/src/app/item-page/edit-item-page/item-bitstreams/item-bitstreams.component.ts b/src/app/item-page/edit-item-page/item-bitstreams/item-bitstreams.component.ts index 0c7dfb1e34..ee53bd919c 100644 --- a/src/app/item-page/edit-item-page/item-bitstreams/item-bitstreams.component.ts +++ b/src/app/item-page/edit-item-page/item-bitstreams/item-bitstreams.component.ts @@ -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> = 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[]) => { - this.displayNotifications('item.edit.bitstreams.notifications.remove', responses); + removedResponses$.subscribe((responses: RemoteData) => { + this.displayNotifications('item.edit.bitstreams.notifications.remove', [responses]); this.submitting = false; }); } diff --git a/src/app/shared/testing/bitstream-data-service.stub.ts b/src/app/shared/testing/bitstream-data-service.stub.ts new file mode 100644 index 0000000000..5b05109b98 --- /dev/null +++ b/src/app/shared/testing/bitstream-data-service.stub.ts @@ -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> { + return observableOf(new RemoteData(0, 0, 0, RequestEntryState.Success)); + } + +}