mirror of
https://github.com/DSpace/dspace-angular.git
synced 2025-10-07 01:54:15 +00:00
Merge remote-tracking branch 'atmire-github/w2p-112198_add-relationship-effects-queue' into w2p-112198_add-relationship-effects-queue-main
Conflicts: src/app/shared/form/builder/ds-dynamic-form-ui/relation-lookup-modal/relationship.effects.spec.ts src/app/shared/form/builder/ds-dynamic-form-ui/relation-lookup-modal/relationship.effects.ts src/app/shared/form/builder/ds-dynamic-form-ui/relation-lookup-modal/search-tab/dynamic-lookup-relation-search-tab.component.html
This commit is contained in:
@@ -197,6 +197,8 @@ describe('RelationshipEffects', () => {
|
|||||||
let action;
|
let action;
|
||||||
describe('When the last value in the debounceMap is also an ADD_RELATIONSHIP action', () => {
|
describe('When the last value in the debounceMap is also an ADD_RELATIONSHIP action', () => {
|
||||||
beforeEach(() => {
|
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).initialActionMap[identifier] = RelationshipActionTypes.ADD_RELATIONSHIP;
|
||||||
((relationEffects as any).debounceTime as jasmine.Spy).and.returnValue((v) => v);
|
((relationEffects as any).debounceTime as jasmine.Spy).and.returnValue((v) => v);
|
||||||
});
|
});
|
||||||
@@ -262,6 +264,8 @@ describe('RelationshipEffects', () => {
|
|||||||
let action;
|
let action;
|
||||||
describe('When the last value in the debounceMap is also an REMOVE_RELATIONSHIP action', () => {
|
describe('When the last value in the debounceMap is also an REMOVE_RELATIONSHIP action', () => {
|
||||||
beforeEach(() => {
|
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).debounceTime as jasmine.Spy).and.returnValue((v) => v);
|
||||||
(relationEffects as any).initialActionMap[identifier] = RelationshipActionTypes.REMOVE_RELATIONSHIP;
|
(relationEffects as any).initialActionMap[identifier] = RelationshipActionTypes.REMOVE_RELATIONSHIP;
|
||||||
});
|
});
|
||||||
@@ -271,7 +275,7 @@ describe('RelationshipEffects', () => {
|
|||||||
actions = hot('--a-|', { a: action });
|
actions = hot('--a-|', { a: action });
|
||||||
const expected = cold('--b-|', { b: undefined });
|
const expected = cold('--b-|', { b: undefined });
|
||||||
expect(relationEffects.mapLastActions$).toBeObservable(expected);
|
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);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@@ -1,3 +1,4 @@
|
|||||||
|
|
||||||
import {
|
import {
|
||||||
Inject,
|
Inject,
|
||||||
Injectable,
|
Injectable,
|
||||||
@@ -12,13 +13,16 @@ import { TranslateService } from '@ngx-translate/core';
|
|||||||
import {
|
import {
|
||||||
BehaviorSubject,
|
BehaviorSubject,
|
||||||
Observable,
|
Observable,
|
||||||
|
Subject,
|
||||||
} from 'rxjs';
|
} from 'rxjs';
|
||||||
import {
|
import {
|
||||||
|
concatMap,
|
||||||
filter,
|
filter,
|
||||||
map,
|
map,
|
||||||
mergeMap,
|
mergeMap,
|
||||||
switchMap,
|
switchMap,
|
||||||
take,
|
take,
|
||||||
|
tap,
|
||||||
} from 'rxjs/operators';
|
} from 'rxjs/operators';
|
||||||
|
|
||||||
import { ObjectCacheService } from '../../../../../core/cache/object-cache.service';
|
import { ObjectCacheService } from '../../../../../core/cache/object-cache.service';
|
||||||
@@ -58,11 +62,31 @@ import {
|
|||||||
|
|
||||||
const DEBOUNCE_TIME = 500;
|
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
|
* NGRX effects for RelationshipEffects
|
||||||
*/
|
*/
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class RelationshipEffects {
|
export class RelationshipEffects {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Queue to hold all requests, so we can ensure they get sent one at a time
|
||||||
|
*/
|
||||||
|
private requestQueue: Subject<RelationOperation> = new Subject();
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Map that keeps track of the latest RelationshipEffects for each relationship's composed identifier
|
* 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];
|
nameVariant = this.nameVariantUpdates[identifier];
|
||||||
delete 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 {
|
} else {
|
||||||
this.removeRelationship(item1, item2, relationshipType, submissionId);
|
this.requestQueue.next({
|
||||||
|
type: RelationOperationType.Remove,
|
||||||
|
item1,
|
||||||
|
item2,
|
||||||
|
relationshipType,
|
||||||
|
submissionId,
|
||||||
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
delete this.debounceMap[identifier];
|
delete this.debounceMap[identifier];
|
||||||
@@ -183,8 +220,41 @@ export class RelationshipEffects {
|
|||||||
private selectableListService: SelectableListService,
|
private selectableListService: SelectableListService,
|
||||||
@Inject(DEBOUNCE_TIME_OPERATOR) private debounceTime: <T>(dueTime: number) => (source: Observable<T>) => Observable<T>,
|
@Inject(DEBOUNCE_TIME_OPERATOR) private debounceTime: <T>(dueTime: number) => (source: Observable<T>) => Observable<T>,
|
||||||
) {
|
) {
|
||||||
|
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 {
|
private createIdentifier(item1: Item, item2: Item, relationshipType: string): string {
|
||||||
return `${item1.uuid}-${item2.uuid}-${relationshipType}`;
|
return `${item1.uuid}-${item2.uuid}-${relationshipType}`;
|
||||||
}
|
}
|
||||||
@@ -207,7 +277,7 @@ export class RelationshipEffects {
|
|||||||
}
|
}
|
||||||
}),
|
}),
|
||||||
take(1),
|
take(1),
|
||||||
switchMap((rd: RemoteData<Relationship>) => {
|
tap((rd: RemoteData<Relationship>) => {
|
||||||
if (hasNoValue(rd) || rd.hasFailed) {
|
if (hasNoValue(rd) || rd.hasFailed) {
|
||||||
// An error occurred, deselect the object from the selectable list and display an error notification
|
// An error occurred, deselect the object from the selectable list and display an error notification
|
||||||
const listId = `list-${submissionId}-${relationshipType}`;
|
const listId = `list-${submissionId}-${relationshipType}`;
|
||||||
@@ -225,19 +295,15 @@ export class RelationshipEffects {
|
|||||||
}
|
}
|
||||||
this.notificationsService.error(this.translateService.instant('relationships.add.error.title'), errorContent);
|
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) {
|
private removeRelationship(item1: Item, item2: Item, relationshipType: string) {
|
||||||
this.relationshipService.getRelationshipByItemsAndLabel(item1, item2, relationshipType).pipe(
|
return this.relationshipService.getRelationshipByItemsAndLabel(item1, item2, relationshipType).pipe(
|
||||||
mergeMap((relationship: Relationship) => this.relationshipService.deleteRelationship(relationship.id, 'none')),
|
mergeMap((relationship: Relationship) => this.relationshipService.deleteRelationship(relationship.id, 'none')),
|
||||||
take(1),
|
take(1),
|
||||||
switchMap(() => this.refreshWorkspaceItemInCache(submissionId)),
|
);
|
||||||
).subscribe((submissionObject: SubmissionObject) => {
|
|
||||||
this.store.dispatch(new SaveSubmissionSectionFormSuccessAction(submissionId, [submissionObject], false));
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@@ -15,27 +15,7 @@
|
|||||||
(selectObject)="selectObject.emit($event)">
|
(selectObject)="selectObject.emit($event)">
|
||||||
<div additionalSearchOptions *ngIf="repeatable" class="position-absolute">
|
<div additionalSearchOptions *ngIf="repeatable" class="position-absolute">
|
||||||
<div class="input-group mb-3">
|
<div class="input-group mb-3">
|
||||||
<div class="input-group-prepend">
|
<div ngbDropdown class="input-group dropdown-button">
|
||||||
<div class="input-group-text">
|
|
||||||
<!-- In theory we don't need separate checkboxes for this,
|
|
||||||
but I wasn't able to get this to work correctly without them.
|
|
||||||
Checkboxes that are in the indeterminate state always switch to checked when clicked
|
|
||||||
This seemed like the cleanest and clearest solution to solve this issue for now. -->
|
|
||||||
|
|
||||||
<input *ngIf="!allSelected && (someSelected$ | async) !== true"
|
|
||||||
type="checkbox"
|
|
||||||
[indeterminate]="false"
|
|
||||||
(change)="selectAll()">
|
|
||||||
<input *ngIf="!allSelected && (someSelected$ | async)"
|
|
||||||
type="checkbox"
|
|
||||||
[indeterminate]="true"
|
|
||||||
(change)="deselectAll()">
|
|
||||||
<input *ngIf="allSelected" type="checkbox"
|
|
||||||
[checked]="true"
|
|
||||||
(change)="deselectAll()">
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div ngbDropdown class="input-group-append">
|
|
||||||
<button *ngIf="selectAllLoading" type="button"
|
<button *ngIf="selectAllLoading" type="button"
|
||||||
class="btn btn-outline-secondary rounded-right">
|
class="btn btn-outline-secondary rounded-right">
|
||||||
<span class="spinner-border spinner-border-sm" role="status"
|
<span class="spinner-border spinner-border-sm" role="status"
|
||||||
@@ -55,8 +35,6 @@
|
|||||||
(click)="selectPage(resultsRD?.page)">{{ ('submission.sections.describe.relationship-lookup.search-tab.select-page' | translate) }}</button>
|
(click)="selectPage(resultsRD?.page)">{{ ('submission.sections.describe.relationship-lookup.search-tab.select-page' | translate) }}</button>
|
||||||
<button class="dropdown-item"
|
<button class="dropdown-item"
|
||||||
(click)="deselectPage(resultsRD?.page)">{{ ('submission.sections.describe.relationship-lookup.search-tab.deselect-page' | translate) }}</button>
|
(click)="deselectPage(resultsRD?.page)">{{ ('submission.sections.describe.relationship-lookup.search-tab.deselect-page' | translate) }}</button>
|
||||||
<button class="dropdown-item" (click)="selectAll()">{{ ('submission.sections.describe.relationship-lookup.search-tab.select-all' | translate) }}</button>
|
|
||||||
<button class="dropdown-item" (click)="deselectAll()">{{ ('submission.sections.describe.relationship-lookup.search-tab.deselect-all' | translate) }}</button>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@@ -1,3 +1,7 @@
|
|||||||
.position-absolute {
|
.position-absolute {
|
||||||
right: var(--bs-spacer);
|
right: var(--bs-spacer);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.dropdown-button {
|
||||||
|
z-index: 3;
|
||||||
|
}
|
||||||
|
Reference in New Issue
Block a user