diff --git a/src/app/core/shared/operators.ts b/src/app/core/shared/operators.ts index ea2a0283eb..2a68e44af8 100644 --- a/src/app/core/shared/operators.ts +++ b/src/app/core/shared/operators.ts @@ -349,7 +349,7 @@ export const metadataFieldsToString = () => map((schema: MetadataSchema) => ({ field, schema })) ); }); - return observableCombineLatest(fieldSchemaArray); + return isNotEmpty(fieldSchemaArray) ? observableCombineLatest(fieldSchemaArray) : [[]]; }), map((fieldSchemaArray: { field: MetadataField, schema: MetadataSchema }[]): string[] => { return fieldSchemaArray.map((fieldSchema: { field: MetadataField, schema: MetadataSchema }) => fieldSchema.schema.prefix + '.' + fieldSchema.field.toString()); 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 c059f5064f..220a4561f6 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,5 +1,10 @@ import { MetadataMap, MetadataValue } from '../../core/shared/metadata.models'; import { hasNoValue, hasValue, isEmpty, isNotEmpty } from '../../shared/empty.util'; +import { 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'; export enum DsoEditMetadataChangeType { UPDATE = 1, @@ -55,10 +60,14 @@ export class DsoEditMetadataValue { } reinstate(): void { - this.newValue = this.reinstatableValue; - this.reinstatableValue = undefined; - this.change = this.reinstatableChange; - this.reinstatableChange = undefined; + if (hasValue(this.reinstatableValue)) { + this.newValue = this.reinstatableValue; + this.reinstatableValue = undefined; + } + if (hasValue(this.reinstatableChange)) { + this.change = this.reinstatableChange; + this.reinstatableChange = undefined; + } } isReinstatable(): boolean { @@ -67,16 +76,23 @@ export class DsoEditMetadataValue { } export class DsoEditMetadataForm { + originalFieldKeys: string[]; fieldKeys: string[]; fields: { [mdField: string]: DsoEditMetadataValue[], }; + reinstatableNewValues: { + [mdField: string]: DsoEditMetadataValue[], + }; newValue: DsoEditMetadataValue; constructor(metadata: MetadataMap) { + this.originalFieldKeys = []; this.fieldKeys = []; this.fields = {}; + this.reinstatableNewValues = {}; Object.entries(metadata).forEach(([mdField, values]: [string, MetadataValue[]]) => { + this.originalFieldKeys.push(mdField); this.fieldKeys.push(mdField); this.fields[mdField] = values.map((value) => new DsoEditMetadataValue(value)); }); @@ -89,12 +105,17 @@ export class DsoEditMetadataForm { } setMetadataField(mdField: string) { + this.newValue.editing = false; + this.addValueToField(this.newValue, mdField); + this.newValue = undefined; + } + + private addValueToField(value: DsoEditMetadataValue, mdField: string) { if (isEmpty(this.fields[mdField])) { this.fieldKeys.push(mdField); this.fields[mdField] = []; } - this.fields[mdField].push(this.newValue); - this.newValue = undefined; + this.fields[mdField].push(value); } remove(mdField: string, index: number) { @@ -112,11 +133,31 @@ export class DsoEditMetadataForm { } discard(): void { - Object.values(this.fields).forEach((values) => { - values.forEach((value) => { - value.discard(); + Object.entries(this.fields).forEach(([field, values]) => { + let removeFromIndex = -1; + values.forEach((value, index) => { + if (value.change === DsoEditMetadataChangeType.ADD) { + if (isEmpty(this.reinstatableNewValues[field])) { + this.reinstatableNewValues[field] = []; + } + this.reinstatableNewValues[field].push(value); + if (removeFromIndex === -1) { + removeFromIndex = index; + } + } else { + value.discardAndMarkReinstatable(); + } }); + if (removeFromIndex > -1) { + this.fields[field].splice(removeFromIndex, this.fields[field].length - removeFromIndex); + } }); + this.fieldKeys.forEach((field) => { + if (this.originalFieldKeys.indexOf(field) < 0) { + delete this.fields[field]; + } + }); + this.fieldKeys = [...this.originalFieldKeys]; } reinstate(): void { @@ -125,9 +166,48 @@ export class DsoEditMetadataForm { value.reinstate(); }); }); + Object.entries(this.reinstatableNewValues).forEach(([field, values]) => { + values.forEach((value) => { + this.addValueToField(value, field); + }); + }); + this.reinstatableNewValues = {}; } isReinstatable(): boolean { - return Object.values(this.fields).some((values) => values.some((value) => value.isReinstatable())); + return isNotEmpty(this.reinstatableNewValues) || + Object.values(this.fields) + .some((values) => values + .some((value) => value.isReinstatable())); + } + + getOperations(): Operation[] { + const operations: Operation[] = []; + Object.entries(this.fields).forEach(([field, values]) => { + values.forEach((value, place) => { + 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); + } + if (hasValue(operation)) { + operations.push(operation.toOperation()); + } + } + }); + }); + return operations; } } 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 abe671e6f3..27cf706a1a 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 @@ -1,19 +1,19 @@
- - - - @@ -36,7 +36,7 @@
- +
@@ -44,16 +44,16 @@
- -
@@ -63,15 +63,15 @@
- {{ mdField }} + {{ mdField }}
{{ mdValue.newValue.value }}
- +
{{ mdValue.newValue.language }}
@@ -80,23 +80,24 @@
-
@@ -110,14 +111,14 @@
- - -
diff --git a/src/app/dso-shared/dso-edit-metadata/dso-edit-metadata.component.scss b/src/app/dso-shared/dso-edit-metadata/dso-edit-metadata.component.scss index a13afaf07e..8e038d7ef8 100644 --- a/src/app/dso-shared/dso-edit-metadata/dso-edit-metadata.component.scss +++ b/src/app/dso-shared/dso-edit-metadata/dso-edit-metadata.component.scss @@ -1,5 +1,6 @@ .lbl-cell { min-width: 210px; + max-width: 210px; background-color: var(--bs-gray-100); font-weight: bold; @@ -16,7 +17,8 @@ } .ds-lang-cell { - min-width: 72px; + min-width: 90px; + max-width: 90px; } .ds-edit-cell { @@ -68,6 +70,6 @@ background-color: var(--bs-gray-100); } -.ds-drag-handle { +.ds-drag-handle:not(.disabled) { cursor: grab; } 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 d961d9dce9..5f837e1b80 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,4 +1,4 @@ -import { Component, Input, OnDestroy, OnInit } from '@angular/core'; +import { Component, Injector, Input, OnDestroy, OnInit } from '@angular/core'; import { AlertType } from '../../shared/alert/aletr-type'; import { DSpaceObject } from '../../core/shared/dspace-object.model'; import { DsoEditMetadataChangeType, DsoEditMetadataForm } from './dso-edit-metadata-form'; @@ -12,7 +12,17 @@ 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 { getFirstSucceededRemoteData, metadataFieldsToString } from '../../core/shared/operators'; +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'; @Component({ selector: 'ds-dso-edit-metadata', @@ -21,6 +31,7 @@ import { getFirstSucceededRemoteData, metadataFieldsToString } from '../../core/ }) export class DsoEditMetadataComponent implements OnInit, OnDestroy { @Input() dso: DSpaceObject; + updateDataService: UpdateDataService; dsoType: string; form: DsoEditMetadataForm; @@ -29,6 +40,7 @@ export class DsoEditMetadataComponent implements OnInit, OnDestroy { isReinstatable: boolean; hasChanges: boolean; isEmpty: boolean; + saving$: BehaviorSubject = new BehaviorSubject(false); /** * The AlertType enumeration for access in the component's template @@ -44,7 +56,10 @@ export class DsoEditMetadataComponent implements OnInit, OnDestroy { dsoUpdateSubscription: Subscription; - constructor(protected route: ActivatedRoute) { + constructor(protected route: ActivatedRoute, + protected notificationsService: NotificationsService, + protected translateService: TranslateService, + protected parentInjector: Injector) { } ngOnInit(): void { @@ -54,27 +69,55 @@ export class DsoEditMetadataComponent implements OnInit, OnDestroy { map((data: any) => data.dso) ).subscribe((rd: RemoteData) => { this.dso = rd.payload; + this.initDataService(); this.initForm(); }); } else { + this.initDataService(); this.initForm(); } } - initForm(): void { - this.dsoType = typeof this.dso.type === 'string' ? this.dso.type as any : this.dso.type.value; - this.form = new DsoEditMetadataForm(this.dso.metadata); - this.onDebounce(); + initDataService(): void { + let type: ResourceType; + if (typeof this.dso.type === 'string') { + type = new ResourceType(this.dso.type); + } else { + type = this.dso.type; + } + const provider = getDataServiceFor(type); + this.updateDataService = Injector.create({ + providers: [], + parent: this.parentInjector + }).get(provider); + this.dsoType = type.value; } - onDebounce(): void { + initForm(): void { + this.form = new DsoEditMetadataForm(this.dso.metadata); + this.onValueSaved(); + } + + onValueSaved(): void { this.hasChanges = this.form.hasChanges(); this.isReinstatable = this.form.isReinstatable(); this.isEmpty = Object.keys(this.form.fields).length === 0; } submit(): void { - + this.saving$.next(true); + this.updateDataService.patch(this.dso, this.form.getOperations()).pipe( + getFirstCompletedRemoteData() + ).subscribe((rd: RemoteData) => { + this.saving$.next(false); + if (rd.hasFailed) { + this.notificationsService.error('error', rd.errorMessage); + } else { + this.notificationsService.success('saved', 'saved'); + this.dso = rd.payload; + this.initForm(); + } + }); } add(): void { @@ -84,10 +127,12 @@ export class DsoEditMetadataComponent implements OnInit, OnDestroy { discard(): void { this.form.discard(); + this.onValueSaved(); } reinstate(): void { this.form.reinstate(); + this.onValueSaved(); } ngOnDestroy() { diff --git a/src/app/dso-shared/dso-edit-metadata/metadata-field-selector/metadata-field-selector.component.ts b/src/app/dso-shared/dso-edit-metadata/metadata-field-selector/metadata-field-selector.component.ts index 63c27ef062..17050a76ba 100644 --- a/src/app/dso-shared/dso-edit-metadata/metadata-field-selector/metadata-field-selector.component.ts +++ b/src/app/dso-shared/dso-edit-metadata/metadata-field-selector/metadata-field-selector.component.ts @@ -1,9 +1,8 @@ import { Component, EventEmitter, Input, OnDestroy, OnInit, Output } from '@angular/core'; -import { startWith, switchMap, debounceTime, distinctUntilChanged } from 'rxjs/operators'; +import { switchMap, debounceTime, distinctUntilChanged } from 'rxjs/operators'; import { followLink } from '../../../shared/utils/follow-link-config.model'; import { getAllSucceededRemoteData, - getFirstSucceededRemoteData, metadataFieldsToString } from '../../../core/shared/operators'; import { Observable } from 'rxjs/internal/Observable'; @@ -26,7 +25,8 @@ export class MetadataFieldSelectorComponent implements OnInit, OnDestroy { public input: FormControl = new FormControl(); query$: BehaviorSubject = new BehaviorSubject(null); - debounceTime = 500; + debounceTime = 300; + selectedValueLoading = false; subs: Subscription[] = []; @@ -38,7 +38,10 @@ export class MetadataFieldSelectorComponent implements OnInit, OnDestroy { this.input.valueChanges.pipe( debounceTime(this.debounceTime), ).subscribe((valueChange) => { - this.query$.next(valueChange); + if (!this.selectedValueLoading) { + this.query$.next(valueChange); + } + this.selectedValueLoading = false; this.mdField = valueChange; this.mdFieldChange.emit(this.mdField); }), @@ -59,7 +62,8 @@ export class MetadataFieldSelectorComponent implements OnInit, OnDestroy { } select(mdFieldOption: string) { - this.mdField = mdFieldOption; + this.selectedValueLoading = true; + this.input.setValue(mdFieldOption); } ngOnDestroy(): void {