diff --git a/src/app/access-control/group-registry/group-form/group-form.component.ts b/src/app/access-control/group-registry/group-form/group-form.component.ts index b0178f1294..337f4470c1 100644 --- a/src/app/access-control/group-registry/group-form/group-form.component.ts +++ b/src/app/access-control/group-registry/group-form/group-form.component.ts @@ -377,7 +377,9 @@ export class GroupFormComponent implements OnInit, OnDestroy { */ setActiveGroup(groupId: string) { this.groupDataService.cancelEditGroup(); - this.groupDataService.findById(groupId) + const nextGroup$ = this.groupDataService.findById(groupId); + this.subs.push(nextGroup$.subscribe()); + nextGroup$ .pipe( getFirstSucceededRemoteData(), getRemoteDataPayload()) diff --git a/src/app/access-control/group-registry/group-form/members-list/members-list.component.ts b/src/app/access-control/group-registry/group-form/members-list/members-list.component.ts index 54d144da51..b092afa40e 100644 --- a/src/app/access-control/group-registry/group-form/members-list/members-list.component.ts +++ b/src/app/access-control/group-registry/group-form/members-list/members-list.component.ts @@ -174,7 +174,7 @@ export class MembersListComponent implements OnInit, OnDestroy { return this.ePersonDataService.findAllByHref(group._links.epersons.href, { currentPage: 1, elementsPerPage: 9999 - }, false) + }) .pipe( getFirstSucceededRemoteData(), getRemoteDataPayload(), diff --git a/src/app/core/eperson/group-data.service.spec.ts b/src/app/core/eperson/group-data.service.spec.ts index c30b74e966..01c8632afa 100644 --- a/src/app/core/eperson/group-data.service.spec.ts +++ b/src/app/core/eperson/group-data.service.spec.ts @@ -26,6 +26,12 @@ import { EPersonMock, EPersonMock2 } from '../../shared/testing/eperson.mock'; import { createPaginatedList, createRequestEntry$ } from '../../shared/testing/utils.test'; import { CoreState } from '../core-state.model'; import { FindListOptions } from '../data/find-list-options.model'; +import { DataService } from '../data/data.service'; +import { ObjectCacheService } from '../cache/object-cache.service'; +import { getMockLinkService } from '../../shared/mocks/link-service.mock'; +import { DataServiceStub } from '../../shared/testing/data-service.stub'; +import { of as observableOf } from 'rxjs'; +import { ObjectCacheEntry } from '../cache/object-cache.reducer'; describe('GroupDataService', () => { let service: GroupDataService; @@ -38,6 +44,8 @@ describe('GroupDataService', () => { let groups$; let halService; let rdbService; + let objectCache: ObjectCacheService; + let dataService: DataServiceStub; function init() { restEndpointURL = 'https://dspace.4science.it/dspace-spring-rest/api/eperson'; @@ -46,6 +54,8 @@ describe('GroupDataService', () => { groups$ = createSuccessfulRemoteDataObject$(createPaginatedList(groups)); rdbService = getMockRemoteDataBuildServiceHrefMap(undefined, { 'https://dspace.4science.it/dspace-spring-rest/api/eperson/groups': groups$ }); halService = new HALEndpointServiceStub(restEndpointURL); + objectCache = new ObjectCacheService(store, getMockLinkService()); + dataService = new DataServiceStub(); TestBed.configureTestingModule({ imports: [ CommonModule, @@ -58,7 +68,9 @@ describe('GroupDataService', () => { }), ], declarations: [], - providers: [], + providers: [ + { provide: DataService, useValue: dataService }, + ], schemas: [CUSTOM_ELEMENTS_SCHEMA] }); } @@ -71,7 +83,7 @@ describe('GroupDataService', () => { requestService, rdbService, store, - null, + objectCache, halService, null, ); @@ -109,6 +121,10 @@ describe('GroupDataService', () => { describe('addSubGroupToGroup', () => { beforeEach(() => { + spyOn(objectCache, 'getByHref').and.returnValue(observableOf({ + requestUUIDs: ['request1', 'request2'] + } as ObjectCacheEntry)); + spyOn(dataService, 'invalidateByHref'); service.addSubGroupToGroup(GroupMock, GroupMock2).subscribe(); }); it('should send PostRequest to eperson/groups/group-id/subgroups endpoint with new subgroup link in body', () => { @@ -119,20 +135,40 @@ describe('GroupDataService', () => { const expected = new PostRequest(requestService.generateRequestId(), GroupMock.self + '/' + service.subgroupsEndpoint, GroupMock2.self, options); expect(requestService.send).toHaveBeenCalledWith(expected); }); + it('should invalidate the previous requests of the parent group', () => { + expect(objectCache.getByHref).toHaveBeenCalledWith(GroupMock._links.self.href); + expect(requestService.setStaleByUUID).toHaveBeenCalledTimes(2); + expect(requestService.setStaleByUUID).toHaveBeenCalledWith('request1'); + expect(requestService.setStaleByUUID).toHaveBeenCalledWith('request2'); + }); }); describe('deleteSubGroupFromGroup', () => { beforeEach(() => { + spyOn(objectCache, 'getByHref').and.returnValue(observableOf({ + requestUUIDs: ['request1', 'request2'] + } as ObjectCacheEntry)); + spyOn(dataService, 'invalidateByHref'); service.deleteSubGroupFromGroup(GroupMock, GroupMock2).subscribe(); }); it('should send DeleteRequest to eperson/groups/group-id/subgroups/group-id endpoint', () => { const expected = new DeleteRequest(requestService.generateRequestId(), GroupMock.self + '/' + service.subgroupsEndpoint + '/' + GroupMock2.id); expect(requestService.send).toHaveBeenCalledWith(expected); }); + it('should invalidate the previous requests of the parent group\'', () => { + expect(objectCache.getByHref).toHaveBeenCalledWith(GroupMock._links.self.href); + expect(requestService.setStaleByUUID).toHaveBeenCalledTimes(2); + expect(requestService.setStaleByUUID).toHaveBeenCalledWith('request1'); + expect(requestService.setStaleByUUID).toHaveBeenCalledWith('request2'); + }); }); describe('addMemberToGroup', () => { beforeEach(() => { + spyOn(objectCache, 'getByHref').and.returnValue(observableOf({ + requestUUIDs: ['request1', 'request2'] + } as ObjectCacheEntry)); + spyOn(dataService, 'invalidateByHref'); service.addMemberToGroup(GroupMock, EPersonMock2).subscribe(); }); it('should send PostRequest to eperson/groups/group-id/epersons endpoint with new eperson member in body', () => { @@ -143,20 +179,38 @@ describe('GroupDataService', () => { const expected = new PostRequest(requestService.generateRequestId(), GroupMock.self + '/' + service.ePersonsEndpoint, EPersonMock2.self, options); expect(requestService.send).toHaveBeenCalledWith(expected); }); + it('should invalidate the previous requests of the EPerson and the group', () => { + expect(objectCache.getByHref).toHaveBeenCalledWith(EPersonMock2._links.self.href); + expect(objectCache.getByHref).toHaveBeenCalledWith(GroupMock._links.self.href); + expect(requestService.setStaleByUUID).toHaveBeenCalledTimes(4); + expect(requestService.setStaleByUUID).toHaveBeenCalledWith('request1'); + expect(requestService.setStaleByUUID).toHaveBeenCalledWith('request2'); + }); }); describe('deleteMemberFromGroup', () => { beforeEach(() => { + spyOn(objectCache, 'getByHref').and.returnValue(observableOf({ + requestUUIDs: ['request1', 'request2'] + } as ObjectCacheEntry)); + spyOn(dataService, 'invalidateByHref'); service.deleteMemberFromGroup(GroupMock, EPersonMock).subscribe(); }); it('should send DeleteRequest to eperson/groups/group-id/epersons/eperson-id endpoint', () => { const expected = new DeleteRequest(requestService.generateRequestId(), GroupMock.self + '/' + service.ePersonsEndpoint + '/' + EPersonMock.id); expect(requestService.send).toHaveBeenCalledWith(expected); }); + it('should invalidate the previous requests of the EPerson and the group', () => { + expect(objectCache.getByHref).toHaveBeenCalledWith(EPersonMock._links.self.href); + expect(objectCache.getByHref).toHaveBeenCalledWith(GroupMock._links.self.href); + expect(requestService.setStaleByUUID).toHaveBeenCalledTimes(4); + expect(requestService.setStaleByUUID).toHaveBeenCalledWith('request1'); + expect(requestService.setStaleByUUID).toHaveBeenCalledWith('request2'); + }); }); describe('editGroup', () => { - it('should dispatch a EDIT_GROUP action with the groupp to start editing', () => { + it('should dispatch a EDIT_GROUP action with the group to start editing', () => { service.editGroup(GroupMock); expect(store.dispatch).toHaveBeenCalledWith(new GroupRegistryEditGroupAction(GroupMock)); }); diff --git a/src/app/core/eperson/group-data.service.ts b/src/app/core/eperson/group-data.service.ts index 4467e26240..a994170281 100644 --- a/src/app/core/eperson/group-data.service.ts +++ b/src/app/core/eperson/group-data.service.ts @@ -2,8 +2,8 @@ import { HttpClient, HttpHeaders } from '@angular/common/http'; import { Injectable } from '@angular/core'; import { createSelector, select, Store } from '@ngrx/store'; -import { Observable } from 'rxjs'; -import { filter, map, take } from 'rxjs/operators'; +import { Observable, AsyncSubject, zip as observableZip } from 'rxjs'; +import { filter, map, take, switchMap } from 'rxjs/operators'; import { GroupRegistryCancelGroupAction, GroupRegistryEditGroupAction @@ -110,7 +110,8 @@ export class GroupDataService extends DataService { } /** - * Adds given subgroup as a subgroup to the given active group + * Adds given subgroup as a subgroup to the given active group and waits until the {@link activeGroup} and + * the {@link subgroup} are invalidated. * @param activeGroup Group we want to add subgroup to * @param subgroup Group we want to add as subgroup to activeGroup */ @@ -123,11 +124,46 @@ export class GroupDataService extends DataService { const postRequest = new PostRequest(requestId, activeGroup.self + '/' + this.subgroupsEndpoint, subgroup.self, options); this.requestService.send(postRequest); - return this.rdbService.buildFromRequestUUID(requestId); + const response$ = this.rdbService.buildFromRequestUUID(requestId); + + const invalidated$ = new AsyncSubject(); + response$.pipe( + getFirstCompletedRemoteData(), + switchMap((rd: RemoteData) => { + if (rd.hasSucceeded) { + return observableZip( + this.invalidateByHref(activeGroup._links.self.href), + this.requestService.setStaleByHrefSubstring(activeGroup._links.subgroups.href).pipe(take(1)), + ).pipe( + map((arr: boolean[]) => arr.every((b: boolean) => b === true)) + ); + } else { + return [true]; + } + }) + ).subscribe(() => { + invalidated$.next(true); + invalidated$.complete(); + }); + + return response$.pipe( + switchMap((rd: RemoteData) => { + if (rd.hasSucceeded) { + return invalidated$.pipe( + filter((invalidated: boolean) => invalidated), + map(() => rd) + ); + } else { + return [rd]; + } + }) + ); } /** - * Deletes a given subgroup from the subgroups of the given active group + * Deletes a given subgroup from the subgroups of the given active group and waits until the {@link activeGroup} and + * the {@link subgroup} are invalidated. + * are invalidated. * @param activeGroup Group we want to delete subgroup from * @param subgroup Subgroup we want to delete from activeGroup */ @@ -136,11 +172,45 @@ export class GroupDataService extends DataService { const deleteRequest = new DeleteRequest(requestId, activeGroup.self + '/' + this.subgroupsEndpoint + '/' + subgroup.id); this.requestService.send(deleteRequest); - return this.rdbService.buildFromRequestUUID(requestId); + const response$ = this.rdbService.buildFromRequestUUID(requestId); + + const invalidated$ = new AsyncSubject(); + response$.pipe( + getFirstCompletedRemoteData(), + switchMap((rd: RemoteData) => { + if (rd.hasSucceeded) { + return observableZip( + this.invalidateByHref(activeGroup._links.self.href), + this.requestService.setStaleByHrefSubstring(activeGroup._links.subgroups.href).pipe(take(1)), + ).pipe( + map((arr: boolean[]) => arr.every((b: boolean) => b === true)) + ); + } else { + return [true]; + } + }) + ).subscribe(() => { + invalidated$.next(true); + invalidated$.complete(); + }); + + return response$.pipe( + switchMap((rd: RemoteData) => { + if (rd.hasSucceeded) { + return invalidated$.pipe( + filter((invalidated: boolean) => invalidated), + map(() => rd) + ); + } else { + return [rd]; + } + }) + ); } /** - * Adds given ePerson as member to given group + * Adds given ePerson as member to a given group and invalidates the ePerson and waits until the {@link ePerson} and + * the {@link activeGroup} are invalidated. * @param activeGroup Group we want to add member to * @param ePerson EPerson we want to add as member to given activeGroup */ @@ -153,11 +223,47 @@ export class GroupDataService extends DataService { const postRequest = new PostRequest(requestId, activeGroup.self + '/' + this.ePersonsEndpoint, ePerson.self, options); this.requestService.send(postRequest); - return this.rdbService.buildFromRequestUUID(requestId); + const response$ = this.rdbService.buildFromRequestUUID(requestId); + + const invalidated$ = new AsyncSubject(); + response$.pipe( + getFirstCompletedRemoteData(), + switchMap((rd: RemoteData) => { + if (rd.hasSucceeded) { + return observableZip( + this.invalidateByHref(ePerson._links.self.href), + this.invalidateByHref(activeGroup._links.self.href), + this.requestService.setStaleByHrefSubstring(ePerson._links.groups.href).pipe(take(1)), + this.requestService.setStaleByHrefSubstring(activeGroup._links.epersons.href).pipe(take(1)), + ).pipe( + map((arr: boolean[]) => arr.every((b: boolean) => b === true)) + ); + } else { + return [true]; + } + }) + ).subscribe(() => { + invalidated$.next(true); + invalidated$.complete(); + }); + + return response$.pipe( + switchMap((rd: RemoteData) => { + if (rd.hasSucceeded) { + return invalidated$.pipe( + filter((invalidated: boolean) => invalidated), + map(() => rd) + ); + } else { + return [rd]; + } + }) + ); } /** - * Deletes a given ePerson from the members of the given active group + * Deletes a given ePerson from the members of the given active group and waits until the {@link ePerson} and the + * {@link activeGroup} are invalidated. * @param activeGroup Group we want to delete member from * @param ePerson EPerson we want to delete from members of given activeGroup */ @@ -166,7 +272,42 @@ export class GroupDataService extends DataService { const deleteRequest = new DeleteRequest(requestId, activeGroup.self + '/' + this.ePersonsEndpoint + '/' + ePerson.id); this.requestService.send(deleteRequest); - return this.rdbService.buildFromRequestUUID(requestId); + const response$ = this.rdbService.buildFromRequestUUID(requestId); + + const invalidated$ = new AsyncSubject(); + response$.pipe( + getFirstCompletedRemoteData(), + switchMap((rd: RemoteData) => { + if (rd.hasSucceeded) { + return observableZip( + this.invalidateByHref(ePerson._links.self.href), + this.invalidateByHref(activeGroup._links.self.href), + this.requestService.setStaleByHrefSubstring(ePerson._links.groups.href).pipe(take(1)), + this.requestService.setStaleByHrefSubstring(activeGroup._links.epersons.href).pipe(take(1)), + ).pipe( + map((arr: boolean[]) => arr.every((b: boolean) => b === true)) + ); + } else { + return [true]; + } + }) + ).subscribe(() => { + invalidated$.next(true); + invalidated$.complete(); + }); + + return response$.pipe( + switchMap((rd: RemoteData) => { + if (rd.hasSucceeded) { + return invalidated$.pipe( + filter((invalidated: boolean) => invalidated), + map(() => rd) + ); + } else { + return [rd]; + } + }) + ); } /** @@ -262,7 +403,7 @@ export class GroupDataService extends DataService { * @param role The name of the role for which to create a group * @param link The REST endpoint to create the group */ - createComcolGroup(dso: Community|Collection, role: string, link: string): Observable> { + createComcolGroup(dso: Community | Collection, role: string, link: string): Observable> { const requestId = this.requestService.generateRequestId(); const group = Object.assign(new Group(), { diff --git a/src/app/shared/testing/data-service.stub.ts b/src/app/shared/testing/data-service.stub.ts new file mode 100644 index 0000000000..62334a95e0 --- /dev/null +++ b/src/app/shared/testing/data-service.stub.ts @@ -0,0 +1,9 @@ +import { Observable, of as observableOf } from 'rxjs'; + +export class DataServiceStub { + + invalidateByHref(_href: string): Observable { + return observableOf(true); + } + +}