mirror of
https://github.com/DSpace/dspace-angular.git
synced 2025-10-07 10:04:11 +00:00
73014: MetadataPatchOperationService create patch in two steps to fix remove operation issue
This commit is contained in:
@@ -11,7 +11,6 @@ import { NgModel } from '@angular/forms';
|
||||
import { MetadatumViewModel } from '../../../../core/shared/metadata.models';
|
||||
import { MetadataField } from '../../../../core/metadata/metadata-field.model';
|
||||
import { InputSuggestion } from '../../../../shared/input-suggestions/input-suggestions.model';
|
||||
import { METADATA_PATCH_OPERATION_SERVICE_TOKEN } from '../../../../core/data/object-updates/patch-operation-service/metadata-patch-operation.service';
|
||||
|
||||
@Component({
|
||||
// tslint:disable-next-line:component-selector
|
||||
@@ -76,7 +75,7 @@ export class EditInPlaceFieldComponent implements OnInit, OnChanges {
|
||||
* Sends a new change update for this field to the object updates service
|
||||
*/
|
||||
update(ngModel?: NgModel) {
|
||||
this.objectUpdatesService.saveChangeFieldUpdate(this.url, cloneDeep(this.metadata), METADATA_PATCH_OPERATION_SERVICE_TOKEN);
|
||||
this.objectUpdatesService.saveChangeFieldUpdate(this.url, cloneDeep(this.metadata));
|
||||
if (hasValue(ngModel)) {
|
||||
this.checkValidity(ngModel);
|
||||
}
|
||||
@@ -104,7 +103,7 @@ export class EditInPlaceFieldComponent implements OnInit, OnChanges {
|
||||
* Sends a new remove update for this field to the object updates service
|
||||
*/
|
||||
remove() {
|
||||
this.objectUpdatesService.saveRemoveFieldUpdate(this.url, cloneDeep(this.metadata), METADATA_PATCH_OPERATION_SERVICE_TOKEN);
|
||||
this.objectUpdatesService.saveRemoveFieldUpdate(this.url, cloneDeep(this.metadata));
|
||||
}
|
||||
|
||||
/**
|
||||
|
@@ -94,14 +94,14 @@ export class ItemMetadataComponent extends AbstractItemUpdateComponent {
|
||||
* @param metadata The metadata to add, if no parameter is supplied, create a new Metadatum
|
||||
*/
|
||||
add(metadata: MetadatumViewModel = new MetadatumViewModel()) {
|
||||
this.objectUpdatesService.saveAddFieldUpdate(this.url, metadata, METADATA_PATCH_OPERATION_SERVICE_TOKEN);
|
||||
this.objectUpdatesService.saveAddFieldUpdate(this.url, metadata);
|
||||
}
|
||||
|
||||
/**
|
||||
* Sends all initial values of this item to the object updates service
|
||||
*/
|
||||
public initializeOriginalFields() {
|
||||
this.objectUpdatesService.initialize(this.url, this.item.metadataAsList, this.item.lastModified);
|
||||
this.objectUpdatesService.initialize(this.url, this.item.metadataAsList, this.item.lastModified, METADATA_PATCH_OPERATION_SERVICE_TOKEN);
|
||||
}
|
||||
|
||||
/**
|
||||
|
@@ -40,7 +40,8 @@ export class InitializeFieldsAction implements Action {
|
||||
payload: {
|
||||
url: string,
|
||||
fields: Identifiable[],
|
||||
lastModified: Date
|
||||
lastModified: Date,
|
||||
patchOperationServiceToken?: InjectionToken<PatchOperationService>
|
||||
};
|
||||
|
||||
/**
|
||||
@@ -50,16 +51,15 @@ export class InitializeFieldsAction implements Action {
|
||||
* the unique url of the page for which the fields are being initialized
|
||||
* @param fields The identifiable fields of which the updates are kept track of
|
||||
* @param lastModified The last modified date of the object that belongs to the page
|
||||
* @param order A custom order to keep track of objects moving around
|
||||
* @param pageSize The page size used to fill empty pages for the custom order
|
||||
* @param page The first page to populate in the custom order
|
||||
* @param patchOperationServiceToken An InjectionToken referring to the {@link PatchOperationService} used for creating a patch
|
||||
*/
|
||||
constructor(
|
||||
url: string,
|
||||
fields: Identifiable[],
|
||||
lastModified: Date
|
||||
lastModified: Date,
|
||||
patchOperationServiceToken?: InjectionToken<PatchOperationService>
|
||||
) {
|
||||
this.payload = { url, fields, lastModified };
|
||||
this.payload = { url, fields, lastModified, patchOperationServiceToken };
|
||||
}
|
||||
}
|
||||
|
||||
@@ -72,7 +72,6 @@ export class AddFieldUpdateAction implements Action {
|
||||
url: string,
|
||||
field: Identifiable,
|
||||
changeType: FieldChangeType,
|
||||
patchOperationServiceToken?: InjectionToken<PatchOperationService<Identifiable>>
|
||||
};
|
||||
|
||||
/**
|
||||
@@ -86,9 +85,8 @@ export class AddFieldUpdateAction implements Action {
|
||||
constructor(
|
||||
url: string,
|
||||
field: Identifiable,
|
||||
changeType: FieldChangeType,
|
||||
patchOperationServiceToken?: InjectionToken<PatchOperationService<Identifiable>>) {
|
||||
this.payload = { url, field, changeType, patchOperationServiceToken };
|
||||
changeType: FieldChangeType) {
|
||||
this.payload = { url, field, changeType };
|
||||
}
|
||||
}
|
||||
|
||||
|
@@ -51,7 +51,6 @@ export interface Identifiable {
|
||||
export interface FieldUpdate {
|
||||
field: Identifiable,
|
||||
changeType: FieldChangeType,
|
||||
patchOperationServiceToken?: InjectionToken<PatchOperationService<Identifiable>>
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -92,6 +91,7 @@ export interface ObjectUpdatesEntry {
|
||||
fieldUpdates: FieldUpdates;
|
||||
virtualMetadataSources: VirtualMetadataSources;
|
||||
lastModified: Date;
|
||||
patchOperationServiceToken?: InjectionToken<PatchOperationService>;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -166,6 +166,7 @@ function initializeFieldsUpdate(state: any, action: InitializeFieldsAction) {
|
||||
const url: string = action.payload.url;
|
||||
const fields: Identifiable[] = action.payload.fields;
|
||||
const lastModifiedServer: Date = action.payload.lastModified;
|
||||
const patchOperationServiceToken: InjectionToken<PatchOperationService> = action.payload.patchOperationServiceToken;
|
||||
const fieldStates = createInitialFieldStates(fields);
|
||||
const newPageState = Object.assign(
|
||||
{},
|
||||
@@ -173,7 +174,8 @@ function initializeFieldsUpdate(state: any, action: InitializeFieldsAction) {
|
||||
{ fieldStates: fieldStates },
|
||||
{ fieldUpdates: {} },
|
||||
{ virtualMetadataSources: {} },
|
||||
{ lastModified: lastModifiedServer }
|
||||
{ lastModified: lastModifiedServer },
|
||||
{ patchOperationServiceToken }
|
||||
);
|
||||
return Object.assign({}, state, { [url]: newPageState });
|
||||
}
|
||||
@@ -187,7 +189,6 @@ function addFieldUpdate(state: any, action: AddFieldUpdateAction) {
|
||||
const url: string = action.payload.url;
|
||||
const field: Identifiable = action.payload.field;
|
||||
const changeType: FieldChangeType = action.payload.changeType;
|
||||
const patchOperationServiceToken: InjectionToken<PatchOperationService<Identifiable>> = action.payload.patchOperationServiceToken;
|
||||
const pageState: ObjectUpdatesEntry = state[url] || {};
|
||||
|
||||
let states = pageState.fieldStates;
|
||||
@@ -198,7 +199,7 @@ function addFieldUpdate(state: any, action: AddFieldUpdateAction) {
|
||||
let fieldUpdate: any = pageState.fieldUpdates[field.uuid] || {};
|
||||
const newChangeType = determineChangeType(fieldUpdate.changeType, changeType);
|
||||
|
||||
fieldUpdate = Object.assign({}, { field, changeType: newChangeType, patchOperationServiceToken });
|
||||
fieldUpdate = Object.assign({}, { field, changeType: newChangeType });
|
||||
|
||||
const fieldUpdates = Object.assign({}, pageState.fieldUpdates, { [field.uuid]: fieldUpdate });
|
||||
|
||||
|
@@ -59,9 +59,10 @@ export class ObjectUpdatesService {
|
||||
* @param url The page's URL for which the changes are being mapped
|
||||
* @param fields The initial fields for the page's object
|
||||
* @param lastModified The date the object was last modified
|
||||
* @param patchOperationServiceToken An InjectionToken referring to the {@link PatchOperationService} used for creating a patch
|
||||
*/
|
||||
initialize(url, fields: Identifiable[], lastModified: Date): void {
|
||||
this.store.dispatch(new InitializeFieldsAction(url, fields, lastModified));
|
||||
initialize(url, fields: Identifiable[], lastModified: Date, patchOperationServiceToken?: InjectionToken<PatchOperationService>): void {
|
||||
this.store.dispatch(new InitializeFieldsAction(url, fields, lastModified, patchOperationServiceToken));
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -70,8 +71,8 @@ export class ObjectUpdatesService {
|
||||
* @param field An updated field for the page's object
|
||||
* @param changeType The last type of change applied to this field
|
||||
*/
|
||||
private saveFieldUpdate(url: string, field: Identifiable, changeType: FieldChangeType, patchOperationServiceToken?: InjectionToken<PatchOperationService<Identifiable>>) {
|
||||
this.store.dispatch(new AddFieldUpdateAction(url, field, changeType, patchOperationServiceToken))
|
||||
private saveFieldUpdate(url: string, field: Identifiable, changeType: FieldChangeType) {
|
||||
this.store.dispatch(new AddFieldUpdateAction(url, field, changeType))
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -188,8 +189,8 @@ export class ObjectUpdatesService {
|
||||
* @param url The page's URL for which the changes are saved
|
||||
* @param field An updated field for the page's object
|
||||
*/
|
||||
saveAddFieldUpdate(url: string, field: Identifiable, patchOperationServiceToken?: InjectionToken<PatchOperationService<Identifiable>>) {
|
||||
this.saveFieldUpdate(url, field, FieldChangeType.ADD, patchOperationServiceToken);
|
||||
saveAddFieldUpdate(url: string, field: Identifiable) {
|
||||
this.saveFieldUpdate(url, field, FieldChangeType.ADD);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -197,8 +198,8 @@ export class ObjectUpdatesService {
|
||||
* @param url The page's URL for which the changes are saved
|
||||
* @param field An updated field for the page's object
|
||||
*/
|
||||
saveRemoveFieldUpdate(url: string, field: Identifiable, patchOperationServiceToken?: InjectionToken<PatchOperationService<Identifiable>>) {
|
||||
this.saveFieldUpdate(url, field, FieldChangeType.REMOVE, patchOperationServiceToken);
|
||||
saveRemoveFieldUpdate(url: string, field: Identifiable) {
|
||||
this.saveFieldUpdate(url, field, FieldChangeType.REMOVE);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -206,8 +207,8 @@ export class ObjectUpdatesService {
|
||||
* @param url The page's URL for which the changes are saved
|
||||
* @param field An updated field for the page's object
|
||||
*/
|
||||
saveChangeFieldUpdate(url: string, field: Identifiable, patchOperationServiceToken?: InjectionToken<PatchOperationService<Identifiable>>) {
|
||||
this.saveFieldUpdate(url, field, FieldChangeType.UPDATE, patchOperationServiceToken);
|
||||
saveChangeFieldUpdate(url: string, field: Identifiable) {
|
||||
this.saveFieldUpdate(url, field, FieldChangeType.UPDATE);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -345,18 +346,17 @@ export class ObjectUpdatesService {
|
||||
|
||||
/**
|
||||
* Create a patch from the current object-updates state
|
||||
* The {@link ObjectUpdatesEntry} should contain a patchOperationServiceToken, in order to define how a patch should
|
||||
* be created. If it doesn't, an empty patch will be returned.
|
||||
* @param url The URL of the page for which the patch should be created
|
||||
*/
|
||||
createPatch(url: string): Observable<Operation[]> {
|
||||
return this.getObjectEntry(url).pipe(
|
||||
map((entry) => {
|
||||
const patch = [];
|
||||
Object.keys(entry.fieldUpdates).forEach((uuid) => {
|
||||
const update = entry.fieldUpdates[uuid];
|
||||
if (hasValue(update.patchOperationServiceToken)) {
|
||||
patch.push(this.injector.get(update.patchOperationServiceToken).fieldUpdateToPatchOperation(update));
|
||||
let patch = [];
|
||||
if (hasValue(entry.patchOperationServiceToken)) {
|
||||
patch = this.injector.get(entry.patchOperationServiceToken).fieldUpdatesToPatchOperations(entry.fieldUpdates);
|
||||
}
|
||||
});
|
||||
return patch;
|
||||
})
|
||||
);
|
||||
|
@@ -0,0 +1,33 @@
|
||||
import { Operation } from 'fast-json-patch';
|
||||
import { hasValue } from '../../../../shared/empty.util';
|
||||
|
||||
/**
|
||||
* Wrapper object for metadata patch Operations
|
||||
* It contains the operation type, metadata field, metadata place and patch value, as well as a method to transform it
|
||||
* into a fast-json-patch Operation.
|
||||
*/
|
||||
export class MetadataPatchOperation {
|
||||
op: string;
|
||||
field: string;
|
||||
place: number;
|
||||
value: any;
|
||||
|
||||
constructor(op: string, field: string, place?: number, value?: any) {
|
||||
this.op = op;
|
||||
this.field = field;
|
||||
this.place = place;
|
||||
this.value = value;
|
||||
}
|
||||
|
||||
/**
|
||||
* Transform the MetadataPatchOperation into a fast-json-patch Operation by constructing its path and other properties
|
||||
* using the information provided.
|
||||
*/
|
||||
toOperation(): Operation {
|
||||
let path = `/metadata/${this.field}`;
|
||||
if (hasValue(this.place)) {
|
||||
path += `/${this.place}`;
|
||||
}
|
||||
return { op: this.op as any, path, value: this.value };
|
||||
}
|
||||
}
|
@@ -1,29 +1,103 @@
|
||||
import { PatchOperationService } from './patch-operation.service';
|
||||
import { MetadataValue, MetadatumViewModel } from '../../../shared/metadata.models';
|
||||
import { FieldUpdate } from '../object-updates.reducer';
|
||||
import { MetadatumViewModel } from '../../../shared/metadata.models';
|
||||
import { FieldUpdates } from '../object-updates.reducer';
|
||||
import { Operation } from 'fast-json-patch';
|
||||
import { FieldChangeType } from '../object-updates.actions';
|
||||
import { InjectionToken } from '@angular/core';
|
||||
import { MetadataPatchOperation } from './metadata-patch-operation.model';
|
||||
import { hasValue } from '../../../../shared/empty.util';
|
||||
|
||||
/**
|
||||
* Token to use for injecting this service anywhere you want
|
||||
* This token can used to store in the object-updates store
|
||||
*/
|
||||
export const METADATA_PATCH_OPERATION_SERVICE_TOKEN = new InjectionToken<MetadataPatchOperationService>('MetadataPatchOperationService', {
|
||||
providedIn: 'root',
|
||||
factory: () => new MetadataPatchOperationService(),
|
||||
});
|
||||
|
||||
export class MetadataPatchOperationService implements PatchOperationService<MetadatumViewModel> {
|
||||
fieldUpdateToPatchOperation(fieldUpdate: FieldUpdate): Operation {
|
||||
const metadatum = fieldUpdate.field as MetadatumViewModel;
|
||||
const path = `/metadata/${metadatum.key}`;
|
||||
/**
|
||||
* Service transforming {@link FieldUpdates} into {@link Operation}s for metadata values
|
||||
* This expects the fields within every {@link FieldUpdate} to be {@link MetadatumViewModel}s
|
||||
*/
|
||||
export class MetadataPatchOperationService implements PatchOperationService {
|
||||
|
||||
/**
|
||||
* Transform a {@link FieldUpdates} object into an array of fast-json-patch Operations for metadata values
|
||||
* This method first creates an array of {@link MetadataPatchOperation} wrapper operations, which are then
|
||||
* iterated over to create the actual patch operations. While iterating, it has the ability to check for previous
|
||||
* operations that would modify the operation's position and act accordingly.
|
||||
* @param fieldUpdates
|
||||
*/
|
||||
fieldUpdatesToPatchOperations(fieldUpdates: FieldUpdates): Operation[] {
|
||||
const metadataPatch = this.fieldUpdatesToMetadataPatchOperations(fieldUpdates);
|
||||
|
||||
// This map stores what metadata fields had a value deleted at which places
|
||||
// This is used to modify the place of operations to match previous operations
|
||||
const metadataRemoveMap = new Map<string, number[]>();
|
||||
const patch = [];
|
||||
metadataPatch.forEach((operation) => {
|
||||
// If this operation is removing or editing an existing value, first check the map for previous operations
|
||||
// If the map contains remove operations before this operation's place, lower the place by 1 for each
|
||||
if ((operation.op === 'remove' || operation.op === 'replace') && hasValue(operation.place)) {
|
||||
if (metadataRemoveMap.has(operation.field)) {
|
||||
metadataRemoveMap.get(operation.field).forEach((index) => {
|
||||
if (index < operation.place) {
|
||||
operation.place--;
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// If this is a remove operation, add its (updated) place to the map, so we can adjust following operations accordingly
|
||||
if (operation.op === 'remove' && hasValue(operation.place)) {
|
||||
if (!metadataRemoveMap.has(operation.field)) {
|
||||
metadataRemoveMap.set(operation.field, []);
|
||||
}
|
||||
metadataRemoveMap.get(operation.field).push(operation.place);
|
||||
}
|
||||
|
||||
// Transform the updated operation into a fast-json-patch Operation and add it to the patch
|
||||
patch.push(operation.toOperation());
|
||||
});
|
||||
|
||||
return patch;
|
||||
}
|
||||
|
||||
/**
|
||||
* Transform a {@link FieldUpdates} object into an array of {@link MetadataPatchOperation} wrapper objects
|
||||
* These wrapper objects contain detailed information about the patch operation that needs to be creates for each update
|
||||
* This information can then be modified before creating the actual patch
|
||||
* @param fieldUpdates
|
||||
*/
|
||||
fieldUpdatesToMetadataPatchOperations(fieldUpdates: FieldUpdates): MetadataPatchOperation[] {
|
||||
const metadataPatch = [];
|
||||
|
||||
Object.keys(fieldUpdates).forEach((uuid) => {
|
||||
const update = fieldUpdates[uuid];
|
||||
const metadatum = update.field as MetadatumViewModel;
|
||||
const val = {
|
||||
value: metadatum.value,
|
||||
language: metadatum.language
|
||||
}
|
||||
|
||||
switch (fieldUpdate.changeType) {
|
||||
case FieldChangeType.ADD: return { op: 'add', path, value: [ val ] };
|
||||
case FieldChangeType.REMOVE: return { op: 'remove', path: `${path}/${metadatum.place}` };
|
||||
case FieldChangeType.UPDATE: return { op: 'replace', path: `${path}/${metadatum.place}`, value: val };
|
||||
default: return undefined;
|
||||
let operation: MetadataPatchOperation;
|
||||
switch (update.changeType) {
|
||||
case FieldChangeType.ADD:
|
||||
operation = new MetadataPatchOperation('add', metadatum.key, undefined, [ val ]);
|
||||
break;
|
||||
case FieldChangeType.REMOVE:
|
||||
operation = new MetadataPatchOperation('remove', metadatum.key, metadatum.place);
|
||||
break;
|
||||
case FieldChangeType.UPDATE:
|
||||
operation = new MetadataPatchOperation('replace', metadatum.key, metadatum.place, val);
|
||||
break;
|
||||
}
|
||||
|
||||
metadataPatch.push(operation);
|
||||
});
|
||||
|
||||
return metadataPatch;
|
||||
}
|
||||
|
||||
}
|
||||
|
@@ -1,6 +1,15 @@
|
||||
import { FieldUpdate, Identifiable } from '../object-updates.reducer';
|
||||
import { FieldUpdates } from '../object-updates.reducer';
|
||||
import { Operation } from 'fast-json-patch';
|
||||
|
||||
export interface PatchOperationService<T extends Identifiable> {
|
||||
fieldUpdateToPatchOperation(fieldUpdate: FieldUpdate): Operation;
|
||||
/**
|
||||
* Interface for a service dealing with the transformations of patch operations from the object-updates store
|
||||
* The implementations of this service know how to deal with the fields of a FieldUpdate and how to transform them
|
||||
* into patch Operations.
|
||||
*/
|
||||
export interface PatchOperationService {
|
||||
/**
|
||||
* Transform a {@link FieldUpdates} object into an array of fast-json-patch Operations
|
||||
* @param fieldUpdates
|
||||
*/
|
||||
fieldUpdatesToPatchOperations(fieldUpdates: FieldUpdates): Operation[];
|
||||
}
|
||||
|
Reference in New Issue
Block a user