diff --git a/src/app/shared/form/builder/ds-dynamic-form-ui/relation-lookup-modal/relationship.effects.spec.ts b/src/app/shared/form/builder/ds-dynamic-form-ui/relation-lookup-modal/relationship.effects.spec.ts index 66684058cd..0588bab7d1 100644 --- a/src/app/shared/form/builder/ds-dynamic-form-ui/relation-lookup-modal/relationship.effects.spec.ts +++ b/src/app/shared/form/builder/ds-dynamic-form-ui/relation-lookup-modal/relationship.effects.spec.ts @@ -197,6 +197,8 @@ describe('RelationshipEffects', () => { let action; describe('When the last value in the debounceMap is also an ADD_RELATIONSHIP action', () => { beforeEach(() => { + jasmine.getEnv().allowRespy(true); + spyOn((relationEffects as any), 'addRelationship').and.returnValue(createSuccessfulRemoteDataObject$(relationship)); (relationEffects as any).initialActionMap[identifier] = RelationshipActionTypes.ADD_RELATIONSHIP; ((relationEffects as any).debounceTime as jasmine.Spy).and.returnValue((v) => v); }); @@ -262,6 +264,8 @@ describe('RelationshipEffects', () => { let action; describe('When the last value in the debounceMap is also an REMOVE_RELATIONSHIP action', () => { beforeEach(() => { + jasmine.getEnv().allowRespy(true); + spyOn((relationEffects as any), 'removeRelationship').and.returnValue(createSuccessfulRemoteDataObject$(undefined)); ((relationEffects as any).debounceTime as jasmine.Spy).and.returnValue((v) => v); (relationEffects as any).initialActionMap[identifier] = RelationshipActionTypes.REMOVE_RELATIONSHIP; }); @@ -271,7 +275,7 @@ describe('RelationshipEffects', () => { actions = hot('--a-|', { a: action }); const expected = cold('--b-|', { b: undefined }); expect(relationEffects.mapLastActions$).toBeObservable(expected); - expect((relationEffects as any).removeRelationship).toHaveBeenCalledWith(leftItem, rightItem, relationshipType.leftwardType, '1234'); + expect((relationEffects as any).removeRelationship).toHaveBeenCalledWith(leftItem, rightItem, relationshipType.leftwardType); }); }); diff --git a/src/app/shared/form/builder/ds-dynamic-form-ui/relation-lookup-modal/relationship.effects.ts b/src/app/shared/form/builder/ds-dynamic-form-ui/relation-lookup-modal/relationship.effects.ts index 51b3b70136..cb49c83115 100644 --- a/src/app/shared/form/builder/ds-dynamic-form-ui/relation-lookup-modal/relationship.effects.ts +++ b/src/app/shared/form/builder/ds-dynamic-form-ui/relation-lookup-modal/relationship.effects.ts @@ -1,3 +1,4 @@ + import { Inject, Injectable, @@ -12,13 +13,16 @@ import { TranslateService } from '@ngx-translate/core'; import { BehaviorSubject, Observable, + Subject, } from 'rxjs'; import { + concatMap, filter, map, mergeMap, switchMap, take, + tap, } from 'rxjs/operators'; import { ObjectCacheService } from '../../../../../core/cache/object-cache.service'; @@ -58,11 +62,31 @@ import { const DEBOUNCE_TIME = 500; +enum RelationOperationType { + Add, + Remove, +} + +interface RelationOperation { + type: RelationOperationType + item1: Item + item2: Item + relationshipType: string + submissionId: string + nameVariant?: string +} + /** * NGRX effects for RelationshipEffects */ @Injectable() export class RelationshipEffects { + + /** + * Queue to hold all requests, so we can ensure they get sent one at a time + */ + private requestQueue: Subject = new Subject(); + /** * Map that keeps track of the latest RelationshipEffects for each relationship's composed identifier */ @@ -104,9 +128,22 @@ export class RelationshipEffects { nameVariant = this.nameVariantUpdates[identifier]; delete this.nameVariantUpdates[identifier]; } - this.addRelationship(item1, item2, relationshipType, submissionId, nameVariant); + this.requestQueue.next({ + type: RelationOperationType.Add, + item1, + item2, + relationshipType, + submissionId, + nameVariant, + }); } else { - this.removeRelationship(item1, item2, relationshipType, submissionId); + this.requestQueue.next({ + type: RelationOperationType.Remove, + item1, + item2, + relationshipType, + submissionId, + }); } } delete this.debounceMap[identifier]; @@ -183,8 +220,41 @@ export class RelationshipEffects { private selectableListService: SelectableListService, @Inject(DEBOUNCE_TIME_OPERATOR) private debounceTime: (dueTime: number) => (source: Observable) => Observable, ) { + this.executeRequestsInQueue(); } + /** + * Subscribe to the request queue, execute the requests inside. Wait for each request to complete + * before sending the next one + * @private + */ + private executeRequestsInQueue() { + this.requestQueue.pipe( + // concatMap ensures the next request in the queue will only start after the previous one has emitted + concatMap((next: RelationOperation) => { + switch (next.type) { + case RelationOperationType.Add: + return this.addRelationship(next.item1, next.item2, next.relationshipType, next.submissionId, next.nameVariant).pipe( + map(() => next), + ); + case RelationOperationType.Remove: + return this.removeRelationship(next.item1, next.item2, next.relationshipType).pipe( + map(() => next), + ); + default: + return [next]; + } + }), + // refresh the workspaceitem after each request. It would be great if we could find a way to + // optimize this so it only happens when the queue is empty. + switchMap((next: RelationOperation) => this.refreshWorkspaceItemInCache(next.submissionId)), + // update the form after the workspaceitem is refreshed + ).subscribe((next: SubmissionObject) => { + this.store.dispatch(new SaveSubmissionSectionFormSuccessAction(next.id, [next], false)); + }); + } + + private createIdentifier(item1: Item, item2: Item, relationshipType: string): string { return `${item1.uuid}-${item2.uuid}-${relationshipType}`; } @@ -207,7 +277,7 @@ export class RelationshipEffects { } }), take(1), - switchMap((rd: RemoteData) => { + tap((rd: RemoteData) => { if (hasNoValue(rd) || rd.hasFailed) { // An error occurred, deselect the object from the selectable list and display an error notification const listId = `list-${submissionId}-${relationshipType}`; @@ -225,19 +295,15 @@ export class RelationshipEffects { } this.notificationsService.error(this.translateService.instant('relationships.add.error.title'), errorContent); } - return this.refreshWorkspaceItemInCache(submissionId); }), - ).subscribe((submissionObject: SubmissionObject) => this.store.dispatch(new SaveSubmissionSectionFormSuccessAction(submissionId, [submissionObject], false))); + ); } - private removeRelationship(item1: Item, item2: Item, relationshipType: string, submissionId: string) { - this.relationshipService.getRelationshipByItemsAndLabel(item1, item2, relationshipType).pipe( + private removeRelationship(item1: Item, item2: Item, relationshipType: string) { + return this.relationshipService.getRelationshipByItemsAndLabel(item1, item2, relationshipType).pipe( mergeMap((relationship: Relationship) => this.relationshipService.deleteRelationship(relationship.id, 'none')), take(1), - switchMap(() => this.refreshWorkspaceItemInCache(submissionId)), - ).subscribe((submissionObject: SubmissionObject) => { - this.store.dispatch(new SaveSubmissionSectionFormSuccessAction(submissionId, [submissionObject], false)); - }); + ); } /** diff --git a/src/app/shared/form/builder/ds-dynamic-form-ui/relation-lookup-modal/search-tab/dynamic-lookup-relation-search-tab.component.html b/src/app/shared/form/builder/ds-dynamic-form-ui/relation-lookup-modal/search-tab/dynamic-lookup-relation-search-tab.component.html index cfd2763b92..f1cd5418fe 100644 --- a/src/app/shared/form/builder/ds-dynamic-form-ui/relation-lookup-modal/search-tab/dynamic-lookup-relation-search-tab.component.html +++ b/src/app/shared/form/builder/ds-dynamic-form-ui/relation-lookup-modal/search-tab/dynamic-lookup-relation-search-tab.component.html @@ -15,27 +15,7 @@ (selectObject)="selectObject.emit($event)">
-
-
- - - - - -
-
-
+
diff --git a/src/app/shared/form/builder/ds-dynamic-form-ui/relation-lookup-modal/search-tab/dynamic-lookup-relation-search-tab.component.scss b/src/app/shared/form/builder/ds-dynamic-form-ui/relation-lookup-modal/search-tab/dynamic-lookup-relation-search-tab.component.scss index f73f8b8c34..b4bda36e16 100644 --- a/src/app/shared/form/builder/ds-dynamic-form-ui/relation-lookup-modal/search-tab/dynamic-lookup-relation-search-tab.component.scss +++ b/src/app/shared/form/builder/ds-dynamic-form-ui/relation-lookup-modal/search-tab/dynamic-lookup-relation-search-tab.component.scss @@ -1,3 +1,7 @@ .position-absolute { right: var(--bs-spacer); } + +.dropdown-button { + z-index: 3; +}