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 220a4561f6..984bec575e 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 @@ -6,19 +6,50 @@ import { MetadataPatchRemoveOperation } from '../../core/data/object-updates/pat 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'; +/* tslint:disable:max-classes-per-file */ + +/** + * Enumeration for the type of change occurring on a metadata value + */ export enum DsoEditMetadataChangeType { UPDATE = 1, ADD = 2, REMOVE = 3 } +/** + * Class holding information about a metadata value and its changes within an edit form + */ export class DsoEditMetadataValue { + /** + * The original metadata value (should stay the same!) used to compare changes with + */ originalValue: MetadataValue; + + /** + * The new value, dynamically changing + */ newValue: MetadataValue; + + /** + * A value that can be used to undo any discarding that took place + */ reinstatableValue: MetadataValue; + /** + * Whether or not this value is currently being edited or not + */ editing = false; + + /** + * The type of change that's taking place on this metadata value + * Empty if no changes are made + */ change: DsoEditMetadataChangeType; + + /** + * A type or change that can be used to undo any discarding that took place + */ reinstatableChange: DsoEditMetadataChangeType; constructor(value: MetadataValue, added = false) { @@ -30,6 +61,12 @@ export class DsoEditMetadataValue { } } + /** + * Save the current changes made to the metadata value + * This will set the type of change to UPDATE if the new metadata value's value and/or language are different from + * the original value + * It will also set the editing flag to false + */ confirmChanges() { if (hasNoValue(this.change) || this.change === DsoEditMetadataChangeType.UPDATE) { if ((this.originalValue.value !== this.newValue.value || this.originalValue.language !== this.newValue.language)) { @@ -41,10 +78,19 @@ export class DsoEditMetadataValue { this.editing = false; } + /** + * Returns if the current value contains changes or not + * If the metadata value contains changes, but they haven't been confirmed yet through confirmChanges(), this might + * return false (which is desired) + */ hasChanges(): boolean { return hasValue(this.change); } + /** + * Discard the current changes and mark the value and change type re-instatable by storing them in their relevant + * properties + */ discardAndMarkReinstatable(): void { if (this.change === DsoEditMetadataChangeType.UPDATE) { this.reinstatableValue = this.newValue; @@ -53,12 +99,19 @@ export class DsoEditMetadataValue { this.discard(); } + /** + * Discard the current changes + * Call discardAndMarkReinstatable() instead, if the discard should be re-instatable + */ discard(): void { this.change = undefined; this.newValue = Object.assign(new MetadataValue(), this.originalValue); this.editing = false; } + /** + * Re-instate (undo) the last discard by replacing the value and change type with their reinstate properties (if present) + */ reinstate(): void { if (hasValue(this.reinstatableValue)) { this.newValue = this.reinstatableValue; @@ -70,20 +123,50 @@ export class DsoEditMetadataValue { } } + /** + * Returns if either the value or change type have a re-instatable property + * This will be the case if a discard has taken place that undid changes to the value or type + */ isReinstatable(): boolean { return hasValue(this.reinstatableValue) || hasValue(this.reinstatableChange); } } +/** + * Class holding information about the metadata of a DSpaceObject and its changes within an edit form + */ export class DsoEditMetadataForm { + /** + * List of original metadata field keys (before any changes took place) + */ originalFieldKeys: string[]; + + /** + * List of current metadata field keys (includes new fields for values added by the user) + */ fieldKeys: string[]; + + /** + * Current state of the form + * Key: Metadata field + * Value: List of {@link DsoEditMetadataValue}s for the metadata field + */ fields: { [mdField: string]: DsoEditMetadataValue[], }; + + /** + * A map of previously added metadata values before a discard of the form took place + * This can be used to re-instate the entire form to before the discard taking place + */ reinstatableNewValues: { [mdField: string]: DsoEditMetadataValue[], }; + + /** + * A (temporary) new metadata value added by the user, not belonging to a metadata field yet + * This value will be finalised and added to a field using setMetadataField() + */ newValue: DsoEditMetadataValue; constructor(metadata: MetadataMap) { @@ -98,18 +181,32 @@ export class DsoEditMetadataForm { }); } + /** + * Add a new temporary value for the user to edit + */ add(): void { if (hasNoValue(this.newValue)) { this.newValue = new DsoEditMetadataValue(new MetadataValue(), true); } } + /** + * Add the temporary value to a metadata field + * Clear the temporary value afterwards + * @param mdField + */ setMetadataField(mdField: string) { this.newValue.editing = false; this.addValueToField(this.newValue, mdField); this.newValue = undefined; } + /** + * Add a value to a metadata field within the map + * @param value + * @param mdField + * @private + */ private addValueToField(value: DsoEditMetadataValue, mdField: string) { if (isEmpty(this.fields[mdField])) { this.fieldKeys.push(mdField); @@ -118,6 +215,11 @@ export class DsoEditMetadataForm { this.fields[mdField].push(value); } + /** + * Remove a value from a metadata field on a given index (this actually removes the value, not just marking it deleted) + * @param mdField + * @param index + */ remove(mdField: string, index: number) { if (isNotEmpty(this.fields[mdField])) { this.fields[mdField].splice(index, 1); @@ -128,10 +230,17 @@ export class DsoEditMetadataForm { } } + /** + * Returns if at least one value within the form contains a change + */ hasChanges(): boolean { return Object.values(this.fields).some((values) => values.some((value) => value.hasChanges())); } + /** + * Discard all changes within the form and store their current values within re-instatable properties so they can be + * undone afterwards + */ discard(): void { Object.entries(this.fields).forEach(([field, values]) => { let removeFromIndex = -1; @@ -174,6 +283,9 @@ export class DsoEditMetadataForm { this.reinstatableNewValues = {}; } + /** + * Returns if at least one value contains a re-instatable property, meaning a discard can be reversed + */ isReinstatable(): boolean { return isNotEmpty(this.reinstatableNewValues) || Object.values(this.fields) @@ -181,6 +293,9 @@ export class DsoEditMetadataForm { .some((value) => value.isReinstatable())); } + /** + * Get the json PATCH operations for the current changes within this form + */ getOperations(): Operation[] { const operations: Operation[] = []; Object.entries(this.fields).forEach(([field, values]) => { @@ -211,3 +326,4 @@ export class DsoEditMetadataForm { return operations; } } +/* tslint:enable:max-classes-per-file */ 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 4154a9ce56..7578f2ec99 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 @@ -20,12 +20,12 @@
-
Field
+
{{ dsoType + '.edit.metadata.headers.field' | translate }}
-
Value
-
Lang
-
Edit
+
{{ dsoType + '.edit.metadata.headers.value' | translate }}
+
{{ dsoType + '.edit.metadata.headers.language' | translate }}
+
{{ dsoType + '.edit.metadata.headers.edit' | translate }}
@@ -43,19 +43,19 @@
- - - - @@ -82,25 +82,25 @@
- - - -
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 97f34ba2a3..eee2bc963c 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 @@ -1,28 +1,22 @@ import { Component, Injector, Input, OnDestroy, OnInit, ViewChild } from '@angular/core'; import { AlertType } from '../../shared/alert/aletr-type'; import { DSpaceObject } from '../../core/shared/dspace-object.model'; -import { DsoEditMetadataChangeType, DsoEditMetadataForm, DsoEditMetadataValue } from './dso-edit-metadata-form'; -import { map, switchMap } from 'rxjs/operators'; +import { DsoEditMetadataChangeType, DsoEditMetadataForm } from './dso-edit-metadata-form'; +import { map } from 'rxjs/operators'; import { ActivatedRoute, Data } from '@angular/router'; import { combineLatest as observableCombineLatest } from 'rxjs/internal/observable/combineLatest'; import { Subscription } from 'rxjs/internal/Subscription'; import { RemoteData } from '../../core/data/remote-data'; import { hasNoValue, hasValue } from '../../shared/empty.util'; -import { RegistryService } from '../../core/registry/registry.service'; import { BehaviorSubject } from 'rxjs/internal/BehaviorSubject'; -import { Observable } from 'rxjs/internal/Observable'; -import { followLink } from '../../shared/utils/follow-link-config.model'; import { getFirstCompletedRemoteData, - getFirstSucceededRemoteData, - metadataFieldsToString } from '../../core/shared/operators'; import { UpdateDataService } from '../../core/data/update-data.service'; import { getDataServiceFor } from '../../core/cache/builders/build-decorators'; import { ResourceType } from '../../core/shared/resource-type'; import { NotificationsService } from '../../shared/notifications/notifications.service'; import { TranslateService } from '@ngx-translate/core'; -import { DataService } from '../../core/data/data.service'; import { MetadataFieldSelectorComponent } from './metadata-field-selector/metadata-field-selector.component'; @Component({ @@ -30,19 +24,56 @@ import { MetadataFieldSelectorComponent } from './metadata-field-selector/metada styleUrls: ['./dso-edit-metadata.component.scss'], templateUrl: './dso-edit-metadata.component.html', }) +/** + * Component showing a table of all metadata on a DSpaceObject and options to modify them + */ export class DsoEditMetadataComponent implements OnInit, OnDestroy { + /** + * DSpaceObject to edit metadata for + */ @Input() dso: DSpaceObject; + + /** + * Reference to the component responsible for showing a metadata-field selector + * Used to validate its contents (existing metadata field) before adding a new metadata value + */ @ViewChild(MetadataFieldSelectorComponent) metadataFieldSelectorComponent: MetadataFieldSelectorComponent; + + /** + * Resolved update data-service for the given DSpaceObject (depending on its type, e.g. ItemDataService for an Item) + * Used to send the PATCH request + */ updateDataService: UpdateDataService; + + /** + * Type of the DSpaceObject in String + * Used to resolve i18n messages + */ dsoType: string; + /** + * A dynamic form object containing all information about the metadata and the changes made to them, see {@link DsoEditMetadataForm} + */ form: DsoEditMetadataForm; + + /** + * The metadata field entered by the user for a new metadata value + */ newMdField: string; + // Properties determined by the state of the dynamic form, updated by onValueSaved() isReinstatable: boolean; hasChanges: boolean; isEmpty: boolean; + + /** + * Whether or not the form is currently being submitted + */ saving$: BehaviorSubject = new BehaviorSubject(false); + + /** + * Whether or not the metadata field is currently being validated + */ loadingFieldValidation$: BehaviorSubject = new BehaviorSubject(false); /** @@ -57,6 +88,10 @@ export class DsoEditMetadataComponent implements OnInit, OnDestroy { */ public DsoEditMetadataChangeTypeEnum = DsoEditMetadataChangeType; + /** + * Subscription for updating the current DSpaceObject + * Unsubscribed from in ngOnDestroy() + */ dsoUpdateSubscription: Subscription; constructor(protected route: ActivatedRoute, @@ -65,6 +100,10 @@ export class DsoEditMetadataComponent implements OnInit, OnDestroy { protected parentInjector: Injector) { } + /** + * Read the route (or parent route)'s data to retrieve the current DSpaceObject + * After it's retrieved, initialise the data-service and form + */ ngOnInit(): void { if (hasNoValue(this.dso)) { this.dsoUpdateSubscription = observableCombineLatest([this.route.data, this.route.parent.data]).pipe( @@ -81,6 +120,9 @@ export class DsoEditMetadataComponent implements OnInit, OnDestroy { } } + /** + * Initialise (resolve) the data-service for the current DSpaceObject + */ initDataService(): void { let type: ResourceType; if (typeof this.dso.type === 'string') { @@ -96,17 +138,29 @@ export class DsoEditMetadataComponent implements OnInit, OnDestroy { this.dsoType = type.value; } + /** + * Initialise the dynamic form object by passing the DSpaceObject's metadata + * Call onValueSaved() to update the form's state properties + */ initForm(): void { this.form = new DsoEditMetadataForm(this.dso.metadata); this.onValueSaved(); } + /** + * Update the form's state properties + */ onValueSaved(): void { this.hasChanges = this.form.hasChanges(); this.isReinstatable = this.form.isReinstatable(); this.isEmpty = Object.keys(this.form.fields).length === 0; } + /** + * Submit the current changes to the form by retrieving json PATCH operations from the form and sending it to the + * DSpaceObject's data-service + * Display notificiations and reset the form afterwards if successful + */ submit(): void { this.saving$.next(true); this.updateDataService.patch(this.dso, this.form.getOperations()).pipe( @@ -114,15 +168,23 @@ export class DsoEditMetadataComponent implements OnInit, OnDestroy { ).subscribe((rd: RemoteData) => { this.saving$.next(false); if (rd.hasFailed) { - this.notificationsService.error('error', rd.errorMessage); + this.notificationsService.error(this.translateService.instant(`${this.dsoType}.edit.metadata.notifications.error.title`), rd.errorMessage); } else { - this.notificationsService.success('saved', 'saved'); + this.notificationsService.success( + this.translateService.instant(`${this.dsoType}.edit.metadata.notifications.saved.title`), + this.translateService.instant(`${this.dsoType}.edit.metadata.notifications.saved.content`) + ); this.dso = rd.payload; this.initForm(); } }); } + /** + * Set the metadata field of the temporary added new metadata value + * This will move the new value to its respective parent metadata field + * Validate the metadata field first + */ setMetadataField() { this.loadingFieldValidation$.next(true); this.metadataFieldSelectorComponent.validate().subscribe((valid) => { @@ -134,21 +196,33 @@ export class DsoEditMetadataComponent implements OnInit, OnDestroy { }); } + /** + * Add a new temporary metadata value + */ add(): void { this.newMdField = undefined; this.form.add(); } + /** + * Discard all changes within the current form + */ discard(): void { this.form.discard(); this.onValueSaved(); } + /** + * Restore any changes previously discarded from the form + */ reinstate(): void { this.form.reinstate(); this.onValueSaved(); } + /** + * Unsubscribe from any open subscriptions + */ ngOnDestroy() { if (hasValue(this.dsoUpdateSubscription)) { this.dsoUpdateSubscription.unsubscribe(); diff --git a/src/app/dso-shared/dso-edit-metadata/metadata-field-selector/metadata-field-selector.component.html b/src/app/dso-shared/dso-edit-metadata/metadata-field-selector/metadata-field-selector.component.html index 2a69c21188..4c310bd81b 100644 --- a/src/app/dso-shared/dso-edit-metadata/metadata-field-selector/metadata-field-selector.component.html +++ b/src/app/dso-shared/dso-edit-metadata/metadata-field-selector/metadata-field-selector.component.html @@ -6,7 +6,7 @@ (focusin)="query$.next(mdField)" (dsClickOutside)="query$.next(null)" (click)="$event.stopPropagation();" /> -
Invalid metadata field, please pick an existing one from the suggestions when searching
+
{{ dsoType + '.edit.metadata.metadatafield.invalid' | translate }}