diff --git a/src/app/core/data/object-updates/patch-operation-service/operations/metadata/metadata-patch-move-operation.model.ts b/src/app/core/data/object-updates/patch-operation-service/operations/metadata/metadata-patch-move-operation.model.ts
new file mode 100644
index 0000000000..962d53dfee
--- /dev/null
+++ b/src/app/core/data/object-updates/patch-operation-service/operations/metadata/metadata-patch-move-operation.model.ts
@@ -0,0 +1,33 @@
+import { MetadataPatchOperation } from './metadata-patch-operation.model';
+import { Operation } from 'fast-json-patch';
+
+/**
+ * Wrapper object for a metadata patch move Operation
+ */
+export class MetadataPatchMoveOperation extends MetadataPatchOperation {
+ static operationType = 'move';
+
+ /**
+ * The original place of the metadata value to move
+ */
+ from: number;
+
+ /**
+ * The new place to move the metadata value to
+ */
+ to: number;
+
+ constructor(field: string, from: number, to: number) {
+ super(MetadataPatchMoveOperation.operationType, field);
+ this.from = from;
+ this.to = to;
+ }
+
+ /**
+ * Transform the MetadataPatchOperation into a fast-json-patch Operation by constructing its path and other properties
+ * using the information provided.
+ */
+ toOperation(): Operation {
+ return { op: this.op as any, from: `/metadata/${this.field}/${this.from}`, path: `/metadata/${this.field}/${this.to}` };
+ }
+}
diff --git a/src/app/dso-shared/dso-edit-metadata/dso-edit-metadata-field-values/dso-edit-metadata-field-values.component.html b/src/app/dso-shared/dso-edit-metadata/dso-edit-metadata-field-values/dso-edit-metadata-field-values.component.html
index 6a0386da8e..190d5a7e31 100644
--- a/src/app/dso-shared/dso-edit-metadata/dso-edit-metadata-field-values/dso-edit-metadata-field-values.component.html
+++ b/src/app/dso-shared/dso-edit-metadata/dso-edit-metadata-field-values/dso-edit-metadata-field-values.component.html
@@ -1,4 +1,4 @@
-
+
+ (undo)="mdValue.change === DsoEditMetadataChangeTypeEnum.ADD ? form.remove(mdField, idx) : mdValue.discard(); valueSaved.emit()"
+ (dragging)="$event ? draggingMdField$.next(mdField) : draggingMdField$.next(null)">
diff --git a/src/app/dso-shared/dso-edit-metadata/dso-edit-metadata-field-values/dso-edit-metadata-field-values.component.scss b/src/app/dso-shared/dso-edit-metadata/dso-edit-metadata-field-values/dso-edit-metadata-field-values.component.scss
index afe5084bc2..6b52c4f4b3 100644
--- a/src/app/dso-shared/dso-edit-metadata/dso-edit-metadata-field-values/dso-edit-metadata-field-values.component.scss
+++ b/src/app/dso-shared/dso-edit-metadata/dso-edit-metadata-field-values/dso-edit-metadata-field-values.component.scss
@@ -1,3 +1,7 @@
.ds-drop-list {
background-color: var(--bs-gray-500);
+
+ &.disabled {
+ opacity: 0.3;
+ }
}
diff --git a/src/app/dso-shared/dso-edit-metadata/dso-edit-metadata-field-values/dso-edit-metadata-field-values.component.ts b/src/app/dso-shared/dso-edit-metadata/dso-edit-metadata-field-values/dso-edit-metadata-field-values.component.ts
index 0db427c218..2702e0ba1f 100644
--- a/src/app/dso-shared/dso-edit-metadata/dso-edit-metadata-field-values/dso-edit-metadata-field-values.component.ts
+++ b/src/app/dso-shared/dso-edit-metadata/dso-edit-metadata-field-values/dso-edit-metadata-field-values.component.ts
@@ -1,7 +1,9 @@
import { Component, EventEmitter, Input, Output } from '@angular/core';
-import { DsoEditMetadataChangeType, DsoEditMetadataForm } from '../dso-edit-metadata-form';
+import { DsoEditMetadataChangeType, DsoEditMetadataForm, DsoEditMetadataValue } from '../dso-edit-metadata-form';
import { Observable } from 'rxjs/internal/Observable';
import { DSpaceObject } from '../../../core/shared/dspace-object.model';
+import { BehaviorSubject } from 'rxjs/internal/BehaviorSubject';
+import { CdkDragDrop, moveItemInArray } from '@angular/cdk/drag-drop';
@Component({
selector: 'ds-dso-edit-metadata-field-values',
@@ -38,6 +40,12 @@ export class DsoEditMetadataFieldValuesComponent {
*/
@Input() saving$: Observable
;
+ /**
+ * Tracks for which metadata-field a drag operation is taking place
+ * Null when no drag is currently happening for any field
+ */
+ @Input() draggingMdField$: BehaviorSubject;
+
/**
* Emit when the value has been saved within the form
*/
@@ -48,4 +56,26 @@ export class DsoEditMetadataFieldValuesComponent {
* @type {DsoEditMetadataChangeType}
*/
public DsoEditMetadataChangeTypeEnum = DsoEditMetadataChangeType;
+
+ /**
+ * Drop a value into a new position
+ * Update the form's value array for the current field to match the dropped position
+ * Update the values their place property to match the new order
+ * Send an update to the parent
+ * @param event
+ */
+ drop(event: CdkDragDrop) {
+ const dragIndex = event.previousIndex;
+ const dropIndex = event.currentIndex;
+ // Move the value within its field
+ moveItemInArray(this.form.fields[this.mdField], dragIndex, dropIndex);
+ // Update all the values in this field their place property
+ this.form.fields[this.mdField].forEach((value: DsoEditMetadataValue, index: number) => {
+ value.newValue.place = index;
+ value.confirmChanges();
+ });
+ // Update the form statuses
+ this.form.resetReinstatable();
+ this.valueSaved.emit();
+ }
}
diff --git a/src/app/dso-shared/dso-edit-metadata/dso-edit-metadata-form.ts b/src/app/dso-shared/dso-edit-metadata/dso-edit-metadata-form.ts
index fde0ecf9d4..cc1f55838b 100644
--- a/src/app/dso-shared/dso-edit-metadata/dso-edit-metadata-form.ts
+++ b/src/app/dso-shared/dso-edit-metadata/dso-edit-metadata-form.ts
@@ -1,10 +1,11 @@
import { MetadataMap, MetadataValue } from '../../core/shared/metadata.models';
import { hasNoValue, hasValue, isEmpty, isNotEmpty } from '../../shared/empty.util';
-import { Operation } from 'fast-json-patch';
+import { MoveOperation, Operation } from 'fast-json-patch';
import { MetadataPatchReplaceOperation } from '../../core/data/object-updates/patch-operation-service/operations/metadata/metadata-patch-replace-operation.model';
import { MetadataPatchRemoveOperation } from '../../core/data/object-updates/patch-operation-service/operations/metadata/metadata-patch-remove-operation.model';
import { MetadataPatchAddOperation } from '../../core/data/object-updates/patch-operation-service/operations/metadata/metadata-patch-add-operation.model';
-import { MetadataPatchOperation } from '../../core/data/object-updates/patch-operation-service/operations/metadata/metadata-patch-operation.model';
+import { ArrayMoveChangeAnalyzer } from '../../core/data/array-move-change-analyzer.service';
+import { MetadataPatchMoveOperation } from '../../core/data/object-updates/patch-operation-service/operations/metadata/metadata-patch-move-operation.model';
/* tslint:disable:max-classes-per-file */
@@ -69,7 +70,7 @@ export class DsoEditMetadataValue {
*/
confirmChanges(finishEditing = false) {
if (hasNoValue(this.change) || this.change === DsoEditMetadataChangeType.UPDATE) {
- if ((this.originalValue.value !== this.newValue.value || this.originalValue.language !== this.newValue.language)) {
+ if ((this.originalValue.value !== this.newValue.value || this.originalValue.language !== this.newValue.language || this.originalValue.place !== this.newValue.place)) {
this.change = DsoEditMetadataChangeType.UPDATE;
} else {
this.change = undefined;
@@ -187,7 +188,7 @@ export class DsoEditMetadataForm {
Object.entries(metadata).forEach(([mdField, values]: [string, MetadataValue[]]) => {
this.originalFieldKeys.push(mdField);
this.fieldKeys.push(mdField);
- this.fields[mdField] = values.map((value: MetadataValue) => new DsoEditMetadataValue(value));
+ this.setValuesForFieldSorted(mdField, values.map((value: MetadataValue) => new DsoEditMetadataValue(value)));
});
}
@@ -208,6 +209,10 @@ export class DsoEditMetadataForm {
setMetadataField(mdField: string): void {
this.newValue.editing = false;
this.addValueToField(this.newValue, mdField);
+ // Set the place property to match the new value's position within its field
+ const place = this.fields[mdField].length - 1;
+ this.fields[mdField][place].originalValue.place = place;
+ this.fields[mdField][place].newValue.place = place;
this.newValue = undefined;
}
@@ -253,6 +258,7 @@ export class DsoEditMetadataForm {
*/
discard(): void {
this.resetReinstatable();
+ // Discard changes from each value from each field
Object.entries(this.fields).forEach(([field, values]: [string, DsoEditMetadataValue[]]) => {
let removeFromIndex = -1;
values.forEach((value: DsoEditMetadataValue, index: number) => {
@@ -272,25 +278,39 @@ export class DsoEditMetadataForm {
this.fields[field].splice(removeFromIndex, this.fields[field].length - removeFromIndex);
}
});
+ // Delete new metadata fields
this.fieldKeys.forEach((field: string) => {
if (this.originalFieldKeys.indexOf(field) < 0) {
delete this.fields[field];
}
});
this.fieldKeys = [...this.originalFieldKeys];
+ // Reset the order of values within their fields to match their place property
+ this.fieldKeys.forEach((field: string) => {
+ this.setValuesForFieldSorted(field, this.fields[field]);
+ });
}
+ /**
+ * Undo any previously discarded changes
+ */
reinstate(): void {
+ // Reinstate each value
Object.values(this.fields).forEach((values: DsoEditMetadataValue[]) => {
values.forEach((value: DsoEditMetadataValue) => {
value.reinstate();
});
});
+ // Re-add new values
Object.entries(this.reinstatableNewValues).forEach(([field, values]: [string, DsoEditMetadataValue[]]) => {
values.forEach((value: DsoEditMetadataValue) => {
this.addValueToField(value, field);
});
});
+ // Reset the order of values within their fields to match their place property
+ this.fieldKeys.forEach((field: string) => {
+ this.setValuesForFieldSorted(field, this.fields[field]);
+ });
this.reinstatableNewValues = {};
}
@@ -317,34 +337,73 @@ export class DsoEditMetadataForm {
}
/**
- * Get the json PATCH operations for the current changes within this form
+ * Set the values of a metadata field and sort them by their newValue's place property
+ * @param mdField
+ * @param values
*/
- getOperations(): Operation[] {
+ private setValuesForFieldSorted(mdField: string, values: DsoEditMetadataValue[]) {
+ this.fields[mdField] = values.sort((a: DsoEditMetadataValue, b: DsoEditMetadataValue) => a.newValue.place - b.newValue.place);
+ }
+
+ /**
+ * Get the json PATCH operations for the current changes within this form
+ * For each metadata field, it'll return operations in the following order: replace, remove (from last to first place), add and move
+ * This order is important, as each operation is executed in succession of the previous one
+ */
+ getOperations(moveAnalyser: ArrayMoveChangeAnalyzer): Operation[] {
const operations: Operation[] = [];
Object.entries(this.fields).forEach(([field, values]: [string, DsoEditMetadataValue[]]) => {
- values.forEach((value: DsoEditMetadataValue, place: number) => {
- if (value.hasChanges()) {
- let operation: MetadataPatchOperation;
- if (value.change === DsoEditMetadataChangeType.UPDATE) {
- operation = new MetadataPatchReplaceOperation(field, place, {
- value: value.newValue.value,
- language: value.newValue.language,
- });
- } else if (value.change === DsoEditMetadataChangeType.REMOVE) {
- operation = new MetadataPatchRemoveOperation(field, place);
- } else if (value.change === DsoEditMetadataChangeType.ADD) {
- operation = new MetadataPatchAddOperation(field, {
- value: value.newValue.value,
- language: value.newValue.language,
- });
- } else {
- console.warn('Illegal metadata change state detected for', value);
+ const replaceOperations: MetadataPatchReplaceOperation[] = [];
+ const removeOperations: MetadataPatchRemoveOperation[] = [];
+ const addOperations: MetadataPatchAddOperation[] = [];
+ values
+ .sort((a: DsoEditMetadataValue, b: DsoEditMetadataValue) => a.originalValue.place - b.originalValue.place)
+ .forEach((value: DsoEditMetadataValue) => {
+ if (value.hasChanges()) {
+ if (value.change === DsoEditMetadataChangeType.UPDATE) {
+ // Only changes to value or language are considered "replace" operations. Changes to place are considered "move", which is processed below.
+ if (value.originalValue.value !== value.newValue.value || value.originalValue.language !== value.newValue.language) {
+ replaceOperations.push(new MetadataPatchReplaceOperation(field, value.originalValue.place, {
+ value: value.newValue.value,
+ language: value.newValue.language,
+ }));
+ }
+ } else if (value.change === DsoEditMetadataChangeType.REMOVE) {
+ removeOperations.push(new MetadataPatchRemoveOperation(field, value.originalValue.place));
+ } else if (value.change === DsoEditMetadataChangeType.ADD) {
+ addOperations.push(new MetadataPatchAddOperation(field, {
+ value: value.newValue.value,
+ language: value.newValue.language,
+ }));
+ } else {
+ console.warn('Illegal metadata change state detected for', value);
+ }
}
- if (hasValue(operation)) {
- operations.push(operation.toOperation());
- }
- }
- });
+ });
+
+ operations.push(...replaceOperations
+ .map((operation: MetadataPatchReplaceOperation) => operation.toOperation()));
+ operations.push(...removeOperations
+ // Sort remove operations backwards first, because they get executed in order. This avoids one removal affecting the next.
+ .sort((a: MetadataPatchRemoveOperation, b: MetadataPatchRemoveOperation) => b.place - a.place)
+ .map((operation: MetadataPatchRemoveOperation) => operation.toOperation()));
+ operations.push(...addOperations
+ .map((operation: MetadataPatchAddOperation) => operation.toOperation()));
+ });
+ // Calculate and add the move operations that need to happen in order to move value from their old place to their new within the field
+ // This uses an ArrayMoveChangeAnalyzer
+ Object.entries(this.fields).forEach(([field, values]: [string, DsoEditMetadataValue[]]) => {
+ // Exclude values marked for removal, because operations are executed in order (remove first, then move)
+ const valuesWithoutRemoved = values.filter((value: DsoEditMetadataValue) => value.change !== DsoEditMetadataChangeType.REMOVE);
+ const moveOperations = moveAnalyser
+ .diff(
+ valuesWithoutRemoved
+ .map((value: DsoEditMetadataValue) => value.originalValue.place),
+ valuesWithoutRemoved
+ .sort((a: DsoEditMetadataValue, b: DsoEditMetadataValue) => a.newValue.place - b.newValue.place)
+ .map((value: DsoEditMetadataValue) => value.originalValue.place))
+ .map((operation: MoveOperation) => new MetadataPatchMoveOperation(field, +operation.from.substr(1), +operation.path.substr(1)).toOperation());
+ operations.push(...moveOperations);
});
return operations;
}
diff --git a/src/app/dso-shared/dso-edit-metadata/dso-edit-metadata-value/dso-edit-metadata-value.component.html b/src/app/dso-shared/dso-edit-metadata/dso-edit-metadata-value/dso-edit-metadata-value.component.html
index e72a9e8444..9941d03308 100644
--- a/src/app/dso-shared/dso-edit-metadata/dso-edit-metadata-value/dso-edit-metadata-value.component.html
+++ b/src/app/dso-shared/dso-edit-metadata/dso-edit-metadata-value/dso-edit-metadata-value.component.html
@@ -1,4 +1,5 @@
{{ mdValue.newValue.value }}
@@ -34,8 +35,7 @@
-
-
diff --git a/src/app/dso-shared/dso-edit-metadata/dso-edit-metadata-value/dso-edit-metadata-value.component.scss b/src/app/dso-shared/dso-edit-metadata/dso-edit-metadata-value/dso-edit-metadata-value.component.scss
index 1196aa1940..bcf491b9ce 100644
--- a/src/app/dso-shared/dso-edit-metadata/dso-edit-metadata-value/dso-edit-metadata-value.component.scss
+++ b/src/app/dso-shared/dso-edit-metadata/dso-edit-metadata-value/dso-edit-metadata-value.component.scss
@@ -10,3 +10,7 @@
::ng-deep .tooltip-inner {
min-width: var(--ds-dso-edit-virtual-tooltip-min-width);
}
+
+.cdk-drag-placeholder {
+ opacity: 0;
+}
diff --git a/src/app/dso-shared/dso-edit-metadata/dso-edit-metadata-value/dso-edit-metadata-value.component.ts b/src/app/dso-shared/dso-edit-metadata/dso-edit-metadata-value/dso-edit-metadata-value.component.ts
index b61255e4fe..dce0ec68b0 100644
--- a/src/app/dso-shared/dso-edit-metadata/dso-edit-metadata-value/dso-edit-metadata-value.component.ts
+++ b/src/app/dso-shared/dso-edit-metadata/dso-edit-metadata-value/dso-edit-metadata-value.component.ts
@@ -71,6 +71,11 @@ export class DsoEditMetadataValueComponent implements OnInit {
*/
@Output() undo: EventEmitter
= new EventEmitter();
+ /**
+ * Emits true when the user starts dragging a value, false when the user stops dragging
+ */
+ @Output() dragging: EventEmitter = new EventEmitter();
+
/**
* The DsoEditMetadataChangeType enumeration for access in the component's template
* @type {DsoEditMetadataChangeType}
diff --git a/src/app/dso-shared/dso-edit-metadata/dso-edit-metadata.component.html b/src/app/dso-shared/dso-edit-metadata/dso-edit-metadata.component.html
index 0ca62ff1b6..e498dc373e 100644
--- a/src/app/dso-shared/dso-edit-metadata/dso-edit-metadata.component.html
+++ b/src/app/dso-shared/dso-edit-metadata/dso-edit-metadata.component.html
@@ -48,6 +48,7 @@
[form]="form"
[dsoType]="dsoType"
[saving$]="saving$"
+ [draggingMdField$]="draggingMdField$"
[mdField]="mdField"
(valueSaved)="onValueSaved()">
diff --git a/src/app/dso-shared/dso-edit-metadata/dso-edit-metadata.component.ts b/src/app/dso-shared/dso-edit-metadata/dso-edit-metadata.component.ts
index e98cc3663d..7f98720b2c 100644
--- a/src/app/dso-shared/dso-edit-metadata/dso-edit-metadata.component.ts
+++ b/src/app/dso-shared/dso-edit-metadata/dso-edit-metadata.component.ts
@@ -19,6 +19,7 @@ import { NotificationsService } from '../../shared/notifications/notifications.s
import { TranslateService } from '@ngx-translate/core';
import { MetadataFieldSelectorComponent } from './metadata-field-selector/metadata-field-selector.component';
import { Observable } from 'rxjs/internal/Observable';
+import { ArrayMoveChangeAnalyzer } from '../../core/data/array-move-change-analyzer.service';
@Component({
selector: 'ds-dso-edit-metadata',
@@ -72,6 +73,13 @@ export class DsoEditMetadataComponent implements OnInit, OnDestroy {
*/
saving$: BehaviorSubject = new BehaviorSubject(false);
+ /**
+ * Tracks for which metadata-field a drag operation is taking place
+ * Null when no drag is currently happening for any field
+ * This is a BehaviorSubject that is passed down to child components, to give them the power to alter the state
+ */
+ draggingMdField$: BehaviorSubject = new BehaviorSubject(null);
+
/**
* Whether or not the metadata field is currently being validated
*/
@@ -98,7 +106,8 @@ export class DsoEditMetadataComponent implements OnInit, OnDestroy {
constructor(protected route: ActivatedRoute,
protected notificationsService: NotificationsService,
protected translateService: TranslateService,
- protected parentInjector: Injector) {
+ protected parentInjector: Injector,
+ protected arrayMoveChangeAnalyser: ArrayMoveChangeAnalyzer) {
}
/**
@@ -167,7 +176,7 @@ export class DsoEditMetadataComponent implements OnInit, OnDestroy {
*/
submit(): void {
this.saving$.next(true);
- this.updateDataService.patch(this.dso, this.form.getOperations()).pipe(
+ this.updateDataService.patch(this.dso, this.form.getOperations(this.arrayMoveChangeAnalyser)).pipe(
getFirstCompletedRemoteData()
).subscribe((rd: RemoteData) => {
this.saving$.next(false);