65717: Immediate patch request + delete in order; refresh cache and show notifications after submit

This commit is contained in:
Kristof De Langhe
2019-10-29 12:04:08 +01:00
parent 9cad39ff36
commit 0b2daf8cbf
7 changed files with 120 additions and 54 deletions

View File

@@ -296,11 +296,14 @@
"item.edit.bitstreams.headers.name": "Name", "item.edit.bitstreams.headers.name": "Name",
"item.edit.bitstreams.notifications.discarded.content": "Your changes were discarded. To reinstate your changes click the 'Undo' button", "item.edit.bitstreams.notifications.discarded.content": "Your changes were discarded. To reinstate your changes click the 'Undo' button",
"item.edit.bitstreams.notifications.discarded.title": "Changes discarded", "item.edit.bitstreams.notifications.discarded.title": "Changes discarded",
"item.edit.bitstreams.notifications.failed.title": "Error deleting bitstream", "item.edit.bitstreams.notifications.move.failed.title": "Error moving bitstreams",
"item.edit.bitstreams.notifications.move.saved.content": "Your move changes to this item's bitstreams and bundles have been saved.",
"item.edit.bitstreams.notifications.move.saved.title": "Move changes saved",
"item.edit.bitstreams.notifications.outdated.content": "The item you're currently working on has been changed by another user. Your current changes are discarded to prevent conflicts", "item.edit.bitstreams.notifications.outdated.content": "The item you're currently working on has been changed by another user. Your current changes are discarded to prevent conflicts",
"item.edit.bitstreams.notifications.outdated.title": "Changes outdated", "item.edit.bitstreams.notifications.outdated.title": "Changes outdated",
"item.edit.bitstreams.notifications.saved.content": "Your changes to this item's bitstreams were saved.", "item.edit.bitstreams.notifications.remove.failed.title": "Error deleting bitstream",
"item.edit.bitstreams.notifications.saved.title": "Changes saved", "item.edit.bitstreams.notifications.remove.saved.content": "Your removal changes to this item's bitstreams have been saved.",
"item.edit.bitstreams.notifications.remove.saved.title": "Removal changes saved",
"item.edit.bitstreams.reinstate-button": "Undo", "item.edit.bitstreams.reinstate-button": "Undo",
"item.edit.bitstreams.save-button": "Save", "item.edit.bitstreams.save-button": "Save",
"item.edit.bitstreams.upload-button": "Upload", "item.edit.bitstreams.upload-button": "Upload",

View File

@@ -6,7 +6,7 @@
<span class="d-none d-sm-inline">&nbsp;{{"item.edit.bitstreams.upload-button" | translate}}</span> <span class="d-none d-sm-inline">&nbsp;{{"item.edit.bitstreams.upload-button" | translate}}</span>
</button> </button>
<button class="btn btn-danger mr-1" *ngIf="!(isReinstatable() | async)" <button class="btn btn-danger mr-1" *ngIf="!(isReinstatable() | async)"
[disabled]="!(hasChanges() | async)" [disabled]="!(hasChanges() | async) || submitting"
(click)="discard()"><i (click)="discard()"><i
class="fas fa-times"></i> class="fas fa-times"></i>
<span class="d-none d-sm-inline">&nbsp;{{"item.edit.bitstreams.discard-button" | translate}}</span> <span class="d-none d-sm-inline">&nbsp;{{"item.edit.bitstreams.discard-button" | translate}}</span>
@@ -16,7 +16,7 @@
class="fas fa-undo-alt"></i> class="fas fa-undo-alt"></i>
<span class="d-none d-sm-inline">&nbsp;{{"item.edit.bitstreams.reinstate-button" | translate}}</span> <span class="d-none d-sm-inline">&nbsp;{{"item.edit.bitstreams.reinstate-button" | translate}}</span>
</button> </button>
<button class="btn btn-primary" [disabled]="!(hasChanges() | async)" <button class="btn btn-primary" [disabled]="!(hasChanges() | async) || submitting"
(click)="submit()"><i (click)="submit()"><i
class="fas fa-save"></i> class="fas fa-save"></i>
<span class="d-none d-sm-inline">&nbsp;{{"item.edit.bitstreams.save-button" | translate}}</span> <span class="d-none d-sm-inline">&nbsp;{{"item.edit.bitstreams.save-button" | translate}}</span>
@@ -31,7 +31,6 @@
<div class="col-6 col-sm-5 col-md-4 col-lg-3 text-center row-element">{{'item.edit.bitstreams.headers.actions' | translate}}</div> <div class="col-6 col-sm-5 col-md-4 col-lg-3 text-center row-element">{{'item.edit.bitstreams.headers.actions' | translate}}</div>
</div> </div>
<ds-item-edit-bitstream-bundle *ngFor="let bundle of bundles" <ds-item-edit-bitstream-bundle *ngFor="let bundle of bundles"
[url]="url"
[bundle]="bundle" [bundle]="bundle"
[item]="item"> [item]="item">
</ds-item-edit-bitstream-bundle> </ds-item-edit-bitstream-bundle>
@@ -45,7 +44,7 @@
<div class="button-row bottom"> <div class="button-row bottom">
<div class="mt-4 float-right"> <div class="mt-4 float-right">
<button class="btn btn-danger" *ngIf="!(isReinstatable() | async)" <button class="btn btn-danger" *ngIf="!(isReinstatable() | async)"
[disabled]="!(hasChanges() | async)" [disabled]="!(hasChanges() | async) || submitting"
(click)="discard()"><i (click)="discard()"><i
class="fas fa-times"></i> class="fas fa-times"></i>
<span class="d-none d-sm-inline">&nbsp;{{"item.edit.bitstreams.discard-button" | translate}}</span> <span class="d-none d-sm-inline">&nbsp;{{"item.edit.bitstreams.discard-button" | translate}}</span>
@@ -55,7 +54,7 @@
class="fas fa-undo-alt"></i> class="fas fa-undo-alt"></i>
<span class="d-none d-sm-inline">&nbsp;{{"item.edit.bitstreams.reinstate-button" | translate}}</span> <span class="d-none d-sm-inline">&nbsp;{{"item.edit.bitstreams.reinstate-button" | translate}}</span>
</button> </button>
<button class="btn btn-primary" [disabled]="!(hasChanges() | async)" <button class="btn btn-primary" [disabled]="!(hasChanges() | async) || submitting"
(click)="submit()"><i (click)="submit()"><i
class="fas fa-save"></i> class="fas fa-save"></i>
<span class="d-none d-sm-inline">&nbsp;{{"item.edit.bitstreams.save-button" | translate}}</span> <span class="d-none d-sm-inline">&nbsp;{{"item.edit.bitstreams.save-button" | translate}}</span>

View File

@@ -10,8 +10,8 @@ import { NotificationsService } from '../../../shared/notifications/notification
import { TranslateService } from '@ngx-translate/core'; import { TranslateService } from '@ngx-translate/core';
import { GLOBAL_CONFIG, GlobalConfig } from '../../../../config'; import { GLOBAL_CONFIG, GlobalConfig } from '../../../../config';
import { BitstreamDataService } from '../../../core/data/bitstream-data.service'; import { BitstreamDataService } from '../../../core/data/bitstream-data.service';
import { hasValue, isNotEmptyOperator } from '../../../shared/empty.util'; import { hasValue, isNotEmpty, isNotEmptyOperator } from '../../../shared/empty.util';
import { zip as observableZip } from 'rxjs'; import { zip as observableZip, combineLatest as observableCombineLatest, of as observableOf } from 'rxjs';
import { ErrorResponse, RestResponse } from '../../../core/cache/response.models'; import { ErrorResponse, RestResponse } from '../../../core/cache/response.models';
import { ObjectCacheService } from '../../../core/cache/object-cache.service'; import { ObjectCacheService } from '../../../core/cache/object-cache.service';
import { RequestService } from '../../../core/data/request.service'; import { RequestService } from '../../../core/data/request.service';
@@ -52,6 +52,12 @@ export class ItemBitstreamsComponent extends AbstractItemUpdateComponent impleme
pageSize: 9999 pageSize: 9999
} as any; } as any;
/**
* Are we currently submitting the changes?
* Used to disable any action buttons until the submit finishes
*/
submitting = false;
/** /**
* A subscription that checks when the item is deleted in cache and reloads the item by sending a new request * A subscription that checks when the item is deleted in cache and reloads the item by sending a new request
* This is used to update the item in cache after bitstreams are deleted * This is used to update the item in cache after bitstreams are deleted
@@ -113,6 +119,7 @@ export class ItemBitstreamsComponent extends AbstractItemUpdateComponent impleme
).subscribe((itemRD: RemoteData<Item>) => { ).subscribe((itemRD: RemoteData<Item>) => {
if (hasValue(itemRD)) { if (hasValue(itemRD)) {
this.item = itemRD.payload; this.item = itemRD.payload;
this.postItemInit();
this.initializeOriginalFields(); this.initializeOriginalFields();
this.initializeUpdates(); this.initializeUpdates();
this.cdRef.detectChanges(); this.cdRef.detectChanges();
@@ -122,41 +129,74 @@ export class ItemBitstreamsComponent extends AbstractItemUpdateComponent impleme
/** /**
* Submit the current changes * Submit the current changes
* Bitstreams that were dragged around send out a patch request with move operations to the rest API
* Bitstreams marked as deleted send out a delete request to the rest API * Bitstreams marked as deleted send out a delete request to the rest API
* Display notifications and reset the current item/updates * Display notifications and reset the current item/updates
*/ */
submit() { submit() {
const removedBitstreams$ = this.bundles$.pipe( this.submitting = true;
take(1), const bundlesOnce$ = this.bundles$.pipe(take(1));
// Fetch all move operations for each bundle
const moveOperations$ = bundlesOnce$.pipe(
switchMap((bundles: Bundle[]) => observableZip(...bundles.map((bundle: Bundle) =>
this.objectUpdatesService.getMoveOperations(bundle.self).pipe(
take(1),
map((operations: MoveOperation[]) => [...operations.map((operation: MoveOperation) => Object.assign(operation, {
from: `/_links/bitstreams${operation.from}/href`,
path: `/_links/bitstreams${operation.path}/href`
}))])
)
)))
);
// Send out an immediate patch request for each bundle
const patchResponses$ = observableCombineLatest(bundlesOnce$, moveOperations$).pipe(
switchMap(([bundles, moveOperationList]: [Bundle[], Operation[][]]) =>
observableZip(...bundles.map((bundle: Bundle, index: number) => {
if (isNotEmpty(moveOperationList[index])) {
return this.bundleService.immediatePatch(bundle, moveOperationList[index]);
} else {
return observableOf(undefined);
}
}))
)
);
// Fetch all removed bitstreams from the object update service
const removedBitstreams$ = bundlesOnce$.pipe(
switchMap((bundles: Bundle[]) => observableZip( switchMap((bundles: Bundle[]) => observableZip(
...bundles.map((bundle: Bundle) => this.objectUpdatesService.getFieldUpdates(bundle.self, [], true)) ...bundles.map((bundle: Bundle) => this.objectUpdatesService.getFieldUpdates(bundle.self, [], true))
)), )),
map((fieldUpdates: FieldUpdates[]) => ([] as FieldUpdate[]).concat( map((fieldUpdates: FieldUpdates[]) => ([] as FieldUpdate[]).concat(
...fieldUpdates.map((updates: FieldUpdates) => Object.values(updates).filter((fieldUpdate: FieldUpdate) => fieldUpdate.changeType === FieldChangeType.REMOVE)) ...fieldUpdates.map((updates: FieldUpdates) => Object.values(updates).filter((fieldUpdate: FieldUpdate) => fieldUpdate.changeType === FieldChangeType.REMOVE))
)), )),
map((fieldUpdates: FieldUpdate[]) => fieldUpdates.map((fieldUpdate: FieldUpdate) => fieldUpdate.field)), map((fieldUpdates: FieldUpdate[]) => fieldUpdates.map((fieldUpdate: FieldUpdate) => fieldUpdate.field))
isNotEmptyOperator()
); );
removedBitstreams$.pipe( // Send out delete requests for all deleted bitstreams
const removedResponses$ = removedBitstreams$.pipe(
take(1), take(1),
switchMap((removedBistreams: Bitstream[]) => observableZip(...removedBistreams.map((bitstream: Bitstream) => this.bitstreamService.deleteAndReturnResponse(bitstream)))) switchMap((removedBistreams: Bitstream[]) => {
).subscribe((responses: RestResponse[]) => { if (isNotEmpty(removedBistreams)) {
this.displayNotifications(responses); return observableZip(...removedBistreams.map((bitstream: Bitstream) => this.bitstreamService.deleteAndReturnResponse(bitstream)));
this.reset(); } else {
}); return observableOf(undefined);
}
})
);
this.bundles$.pipe(take(1)).subscribe((bundles: Bundle[]) => { // Perform the setup actions from above in order and display notifications
bundles.forEach((bundle: Bundle) => { patchResponses$.pipe(
this.objectUpdatesService.getMoveOperations(bundle.self).pipe( switchMap((responses: RestResponse[]) => {
take(1), this.displayNotifications('item.edit.bitstreams.notifications.move', responses);
isNotEmptyOperator(), return removedResponses$
map((operations: MoveOperation[]) => [...operations.map((operation: MoveOperation) => Object.assign(operation, { }),
from: `/_links/bitstreams${operation.from}/href`, take(1)
path: `/_links/bitstreams${operation.path}/href` ).subscribe((responses: RestResponse[]) => {
}))]) this.displayNotifications('item.edit.bitstreams.notifications.remove', responses);
).subscribe((operations: Operation[]) => this.bundleService.patch(bundle.self, operations)); this.reset();
}); this.submitting = false;
}); });
} }
@@ -164,17 +204,20 @@ export class ItemBitstreamsComponent extends AbstractItemUpdateComponent impleme
* Display notifications * Display notifications
* - Error notification for each failed response with their message * - Error notification for each failed response with their message
* - Success notification in case there's at least one successful response * - Success notification in case there's at least one successful response
* @param responses * @param key The i18n key for the notification messages
* @param responses The returned responses to display notifications for
*/ */
displayNotifications(responses: RestResponse[]) { displayNotifications(key: string, responses: RestResponse[]) {
const failedResponses = responses.filter((response: RestResponse) => !response.isSuccessful); if (isNotEmpty(responses)) {
const successfulResponses = responses.filter((response: RestResponse) => response.isSuccessful); const failedResponses = responses.filter((response: RestResponse) => hasValue(response) && !response.isSuccessful);
const successfulResponses = responses.filter((response: RestResponse) => hasValue(response) && response.isSuccessful);
failedResponses.forEach((response: ErrorResponse) => { failedResponses.forEach((response: ErrorResponse) => {
this.notificationsService.error(this.getNotificationTitle('failed'), response.errorMessage); this.notificationsService.error(this.translateService.instant(`${key}.failed.title`), response.errorMessage);
}); });
if (successfulResponses.length > 0) { if (successfulResponses.length > 0) {
this.notificationsService.success(this.getNotificationTitle('saved'), this.getNotificationContent('saved')); this.notificationsService.success(this.translateService.instant(`${key}.saved.title`), this.translateService.instant(`${key}.saved.content`));
}
} }
} }
@@ -230,8 +273,14 @@ export class ItemBitstreamsComponent extends AbstractItemUpdateComponent impleme
* Remove the current item's cache from object- and request-cache * Remove the current item's cache from object- and request-cache
*/ */
refreshItemCache() { refreshItemCache() {
this.objectCache.remove(this.item.self); this.bundles$.pipe(take(1)).subscribe((bundles: Bundle[]) => {
this.requestService.removeByHrefSubstring(this.item.self); bundles.forEach((bundle: Bundle) => {
this.objectCache.remove(bundle.self);
this.requestService.removeByHrefSubstring(bundle.self);
});
this.objectCache.remove(this.item.self);
this.requestService.removeByHrefSubstring(this.item.self);
});
} }
/** /**

View File

@@ -21,7 +21,6 @@
'bg-white': updateValue.changeType === undefined 'bg-white': updateValue.changeType === undefined
}"> }">
<ds-item-edit-bitstream [fieldUpdate]="updateValue" <ds-item-edit-bitstream [fieldUpdate]="updateValue"
[url]="url"
[bundleUrl]="bundle.self"> [bundleUrl]="bundle.self">
<button disabled slot="drag-handle" class="drag-handle btn btn-outline-secondary btn-sm" cdkDragHandle> <button disabled slot="drag-handle" class="drag-handle btn btn-outline-secondary btn-sm" cdkDragHandle>
<i class="fas fa-grip-vertical fa-fw" [title]="'item.edit.bitstreams.edit.buttons.drag' | translate"></i> <i class="fas fa-grip-vertical fa-fw" [title]="'item.edit.bitstreams.edit.buttons.drag' | translate"></i>

View File

@@ -40,11 +40,6 @@ export class ItemEditBitstreamBundleComponent implements OnInit {
*/ */
@Input() item: Item; @Input() item: Item;
/**
* The current url of this page
*/
@Input() url: string;
/** /**
* The bitstreams within this bundle retrieved from the REST API * The bitstreams within this bundle retrieved from the REST API
*/ */

View File

@@ -28,11 +28,6 @@ export class ItemEditBitstreamComponent implements OnChanges, OnInit {
*/ */
@Input() fieldUpdate: FieldUpdate; @Input() fieldUpdate: FieldUpdate;
/**
* The current url of this page
*/
@Input() url: string;
/** /**
* The url of the bundle * The url of the bundle
*/ */

View File

@@ -17,7 +17,7 @@ import {
FindAllOptions, FindAllOptions,
FindAllRequest, FindAllRequest,
FindByIDRequest, FindByIDRequest,
GetRequest GetRequest, PatchRequest
} from './request.models'; } from './request.models';
import { RequestService } from './request.service'; import { RequestService } from './request.service';
import { HttpOptions } from '../dspace-rest-v2/dspace-rest-v2.service'; import { HttpOptions } from '../dspace-rest-v2/dspace-rest-v2.service';
@@ -207,6 +207,32 @@ export abstract class DataService<T extends CacheableObject> {
this.objectCache.addPatch(href, operations); this.objectCache.addPatch(href, operations);
} }
/**
* Send out an immediate patch request, instead of adding to the object cache first
* This is useful in cases where you need the returned response and an object cache update is not needed
* @param dso The dso to send the patch to
* @param operations The patch operations
*/
immediatePatch(dso: T, operations: Operation[]): Observable<RestResponse> {
const requestId = this.requestService.generateRequestId();
const hrefObs = this.halService.getEndpoint(this.linkPath).pipe(
map((endpoint: string) => this.getIDHref(endpoint, dso.uuid)));
hrefObs.pipe(
find((href: string) => hasValue(href)),
map((href: string) => {
const request = new PatchRequest(requestId, href, operations);
this.requestService.configure(request);
})
).subscribe();
return this.requestService.getByUUID(requestId).pipe(
find((request: RequestEntry) => request.completed),
map((request: RequestEntry) => request.response)
);
}
/** /**
* Add a new patch to the object cache * Add a new patch to the object cache
* The patch is derived from the differences between the given object and its version in the object cache * The patch is derived from the differences between the given object and its version in the object cache