Skip to content
Snippets Groups Projects
Commit 0b2daf8c authored by Kristof De Langhe's avatar Kristof De Langhe
Browse files

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

parent 9cad39ff
Branches
Tags
No related merge requests found
......@@ -296,11 +296,14 @@
"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.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.title": "Changes outdated",
"item.edit.bitstreams.notifications.saved.content": "Your changes to this item's bitstreams were saved.",
"item.edit.bitstreams.notifications.saved.title": "Changes saved",
"item.edit.bitstreams.notifications.remove.failed.title": "Error deleting bitstream",
"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.save-button": "Save",
"item.edit.bitstreams.upload-button": "Upload",
......
......@@ -6,7 +6,7 @@
<span class="d-none d-sm-inline">&nbsp;{{"item.edit.bitstreams.upload-button" | translate}}</span>
</button>
<button class="btn btn-danger mr-1" *ngIf="!(isReinstatable() | async)"
[disabled]="!(hasChanges() | async)"
[disabled]="!(hasChanges() | async) || submitting"
(click)="discard()"><i
class="fas fa-times"></i>
<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>
<span class="d-none d-sm-inline">&nbsp;{{"item.edit.bitstreams.reinstate-button" | translate}}</span>
</button>
<button class="btn btn-primary" [disabled]="!(hasChanges() | async)"
<button class="btn btn-primary" [disabled]="!(hasChanges() | async) || submitting"
(click)="submit()"><i
class="fas fa-save"></i>
<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>
<ds-item-edit-bitstream-bundle *ngFor="let bundle of bundles"
[url]="url"
[bundle]="bundle"
[item]="item">
</ds-item-edit-bitstream-bundle>
......@@ -45,7 +44,7 @@
<div class="button-row bottom">
<div class="mt-4 float-right">
<button class="btn btn-danger" *ngIf="!(isReinstatable() | async)"
[disabled]="!(hasChanges() | async)"
[disabled]="!(hasChanges() | async) || submitting"
(click)="discard()"><i
class="fas fa-times"></i>
<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>
<span class="d-none d-sm-inline">&nbsp;{{"item.edit.bitstreams.reinstate-button" | translate}}</span>
</button>
<button class="btn btn-primary" [disabled]="!(hasChanges() | async)"
<button class="btn btn-primary" [disabled]="!(hasChanges() | async) || submitting"
(click)="submit()"><i
class="fas fa-save"></i>
<span class="d-none d-sm-inline">&nbsp;{{"item.edit.bitstreams.save-button" | translate}}</span>
......
......@@ -10,8 +10,8 @@ import { NotificationsService } from '../../../shared/notifications/notification
import { TranslateService } from '@ngx-translate/core';
import { GLOBAL_CONFIG, GlobalConfig } from '../../../../config';
import { BitstreamDataService } from '../../../core/data/bitstream-data.service';
import { hasValue, isNotEmptyOperator } from '../../../shared/empty.util';
import { zip as observableZip } from 'rxjs';
import { hasValue, isNotEmpty, isNotEmptyOperator } from '../../../shared/empty.util';
import { zip as observableZip, combineLatest as observableCombineLatest, of as observableOf } from 'rxjs';
import { ErrorResponse, RestResponse } from '../../../core/cache/response.models';
import { ObjectCacheService } from '../../../core/cache/object-cache.service';
import { RequestService } from '../../../core/data/request.service';
......@@ -52,6 +52,12 @@ export class ItemBitstreamsComponent extends AbstractItemUpdateComponent impleme
pageSize: 9999
} 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
* 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>) => {
if (hasValue(itemRD)) {
this.item = itemRD.payload;
this.postItemInit();
this.initializeOriginalFields();
this.initializeUpdates();
this.cdRef.detectChanges();
......@@ -122,41 +129,74 @@ export class ItemBitstreamsComponent extends AbstractItemUpdateComponent impleme
/**
* 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
* Display notifications and reset the current item/updates
*/
submit() {
const removedBitstreams$ = this.bundles$.pipe(
take(1),
this.submitting = true;
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(
...bundles.map((bundle: Bundle) => this.objectUpdatesService.getFieldUpdates(bundle.self, [], true))
)),
map((fieldUpdates: FieldUpdates[]) => ([] as FieldUpdate[]).concat(
...fieldUpdates.map((updates: FieldUpdates) => Object.values(updates).filter((fieldUpdate: FieldUpdate) => fieldUpdate.changeType === FieldChangeType.REMOVE))
)),
map((fieldUpdates: FieldUpdate[]) => fieldUpdates.map((fieldUpdate: FieldUpdate) => fieldUpdate.field)),
isNotEmptyOperator()
map((fieldUpdates: FieldUpdate[]) => fieldUpdates.map((fieldUpdate: FieldUpdate) => fieldUpdate.field))
);
removedBitstreams$.pipe(
// Send out delete requests for all deleted bitstreams
const removedResponses$ = removedBitstreams$.pipe(
take(1),
switchMap((removedBistreams: Bitstream[]) => observableZip(...removedBistreams.map((bitstream: Bitstream) => this.bitstreamService.deleteAndReturnResponse(bitstream))))
switchMap((removedBistreams: Bitstream[]) => {
if (isNotEmpty(removedBistreams)) {
return observableZip(...removedBistreams.map((bitstream: Bitstream) => this.bitstreamService.deleteAndReturnResponse(bitstream)));
} else {
return observableOf(undefined);
}
})
);
// Perform the setup actions from above in order and display notifications
patchResponses$.pipe(
switchMap((responses: RestResponse[]) => {
this.displayNotifications('item.edit.bitstreams.notifications.move', responses);
return removedResponses$
}),
take(1)
).subscribe((responses: RestResponse[]) => {
this.displayNotifications(responses);
this.displayNotifications('item.edit.bitstreams.notifications.remove', responses);
this.reset();
});
this.bundles$.pipe(take(1)).subscribe((bundles: Bundle[]) => {
bundles.forEach((bundle: Bundle) => {
this.objectUpdatesService.getMoveOperations(bundle.self).pipe(
take(1),
isNotEmptyOperator(),
map((operations: MoveOperation[]) => [...operations.map((operation: MoveOperation) => Object.assign(operation, {
from: `/_links/bitstreams${operation.from}/href`,
path: `/_links/bitstreams${operation.path}/href`
}))])
).subscribe((operations: Operation[]) => this.bundleService.patch(bundle.self, operations));
});
this.submitting = false;
});
}
......@@ -164,17 +204,20 @@ export class ItemBitstreamsComponent extends AbstractItemUpdateComponent impleme
* Display notifications
* - Error notification for each failed response with their message
* - 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[]) {
const failedResponses = responses.filter((response: RestResponse) => !response.isSuccessful);
const successfulResponses = responses.filter((response: RestResponse) => response.isSuccessful);
displayNotifications(key: string, responses: RestResponse[]) {
if (isNotEmpty(responses)) {
const failedResponses = responses.filter((response: RestResponse) => hasValue(response) && !response.isSuccessful);
const successfulResponses = responses.filter((response: RestResponse) => hasValue(response) && response.isSuccessful);
failedResponses.forEach((response: ErrorResponse) => {
this.notificationsService.error(this.getNotificationTitle('failed'), response.errorMessage);
});
if (successfulResponses.length > 0) {
this.notificationsService.success(this.getNotificationTitle('saved'), this.getNotificationContent('saved'));
failedResponses.forEach((response: ErrorResponse) => {
this.notificationsService.error(this.translateService.instant(`${key}.failed.title`), response.errorMessage);
});
if (successfulResponses.length > 0) {
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
*/
refreshItemCache() {
this.objectCache.remove(this.item.self);
this.requestService.removeByHrefSubstring(this.item.self);
this.bundles$.pipe(take(1)).subscribe((bundles: Bundle[]) => {
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);
});
}
/**
......
......@@ -21,7 +21,6 @@
'bg-white': updateValue.changeType === undefined
}">
<ds-item-edit-bitstream [fieldUpdate]="updateValue"
[url]="url"
[bundleUrl]="bundle.self">
<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>
......
......@@ -40,11 +40,6 @@ export class ItemEditBitstreamBundleComponent implements OnInit {
*/
@Input() item: Item;
/**
* The current url of this page
*/
@Input() url: string;
/**
* The bitstreams within this bundle retrieved from the REST API
*/
......
......@@ -28,11 +28,6 @@ export class ItemEditBitstreamComponent implements OnChanges, OnInit {
*/
@Input() fieldUpdate: FieldUpdate;
/**
* The current url of this page
*/
@Input() url: string;
/**
* The url of the bundle
*/
......
......@@ -17,7 +17,7 @@ import {
FindAllOptions,
FindAllRequest,
FindByIDRequest,
GetRequest
GetRequest, PatchRequest
} from './request.models';
import { RequestService } from './request.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);
}
/**
* 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
* The patch is derived from the differences between the given object and its version in the object cache
......
0% or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment