diff --git a/src/app/+item-page/edit-item-page/item-metadata/edit-in-place-field/edit-in-place-field.component.ts b/src/app/+item-page/edit-item-page/item-metadata/edit-in-place-field/edit-in-place-field.component.ts index 74594d2339..5a905fc7ea 100644 --- a/src/app/+item-page/edit-item-page/item-metadata/edit-in-place-field/edit-in-place-field.component.ts +++ b/src/app/+item-page/edit-item-page/item-metadata/edit-in-place-field/edit-in-place-field.component.ts @@ -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)); } /** diff --git a/src/app/+item-page/edit-item-page/item-metadata/item-metadata.component.ts b/src/app/+item-page/edit-item-page/item-metadata/item-metadata.component.ts index edeb692c43..eccfc42c57 100644 --- a/src/app/+item-page/edit-item-page/item-metadata/item-metadata.component.ts +++ b/src/app/+item-page/edit-item-page/item-metadata/item-metadata.component.ts @@ -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); } /** diff --git a/src/app/core/data/object-updates/object-updates.actions.ts b/src/app/core/data/object-updates/object-updates.actions.ts index d98b8495d4..ff0babdd14 100644 --- a/src/app/core/data/object-updates/object-updates.actions.ts +++ b/src/app/core/data/object-updates/object-updates.actions.ts @@ -40,7 +40,8 @@ export class InitializeFieldsAction implements Action { payload: { url: string, fields: Identifiable[], - lastModified: Date + lastModified: Date, + patchOperationServiceToken?: InjectionToken }; /** @@ -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 ) { - 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> }; /** @@ -86,9 +85,8 @@ export class AddFieldUpdateAction implements Action { constructor( url: string, field: Identifiable, - changeType: FieldChangeType, - patchOperationServiceToken?: InjectionToken>) { - this.payload = { url, field, changeType, patchOperationServiceToken }; + changeType: FieldChangeType) { + this.payload = { url, field, changeType }; } } diff --git a/src/app/core/data/object-updates/object-updates.reducer.ts b/src/app/core/data/object-updates/object-updates.reducer.ts index 3a2723a10e..94bb845aa8 100644 --- a/src/app/core/data/object-updates/object-updates.reducer.ts +++ b/src/app/core/data/object-updates/object-updates.reducer.ts @@ -51,7 +51,6 @@ export interface Identifiable { export interface FieldUpdate { field: Identifiable, changeType: FieldChangeType, - patchOperationServiceToken?: InjectionToken> } /** @@ -92,6 +91,7 @@ export interface ObjectUpdatesEntry { fieldUpdates: FieldUpdates; virtualMetadataSources: VirtualMetadataSources; lastModified: Date; + patchOperationServiceToken?: InjectionToken; } /** @@ -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 = 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> = 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 }); diff --git a/src/app/core/data/object-updates/object-updates.service.ts b/src/app/core/data/object-updates/object-updates.service.ts index 5889c6e499..8bd32e54e2 100644 --- a/src/app/core/data/object-updates/object-updates.service.ts +++ b/src/app/core/data/object-updates/object-updates.service.ts @@ -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): 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>) { - 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>) { - 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>) { - 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>) { - 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 { 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; }) ); diff --git a/src/app/core/data/object-updates/patch-operation-service/metadata-patch-operation.model.ts b/src/app/core/data/object-updates/patch-operation-service/metadata-patch-operation.model.ts new file mode 100644 index 0000000000..52e76cb75a --- /dev/null +++ b/src/app/core/data/object-updates/patch-operation-service/metadata-patch-operation.model.ts @@ -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 }; + } +} diff --git a/src/app/core/data/object-updates/patch-operation-service/metadata-patch-operation.service.ts b/src/app/core/data/object-updates/patch-operation-service/metadata-patch-operation.service.ts index cbc441c3da..2644bbc1df 100644 --- a/src/app/core/data/object-updates/patch-operation-service/metadata-patch-operation.service.ts +++ b/src/app/core/data/object-updates/patch-operation-service/metadata-patch-operation.service.ts @@ -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', { providedIn: 'root', factory: () => new MetadataPatchOperationService(), }); -export class MetadataPatchOperationService implements PatchOperationService { - fieldUpdateToPatchOperation(fieldUpdate: FieldUpdate): Operation { - const metadatum = fieldUpdate.field as MetadatumViewModel; - const path = `/metadata/${metadatum.key}`; - const val = { - value: metadatum.value, - language: metadatum.language - } +/** + * 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 { - 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; - } + /** + * 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(); + 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 + } + + 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; + } + } diff --git a/src/app/core/data/object-updates/patch-operation-service/patch-operation.service.ts b/src/app/core/data/object-updates/patch-operation-service/patch-operation.service.ts index 7a88e41b41..7c67f9a2e5 100644 --- a/src/app/core/data/object-updates/patch-operation-service/patch-operation.service.ts +++ b/src/app/core/data/object-updates/patch-operation-service/patch-operation.service.ts @@ -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 { - 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[]; }