From 92f94c39b2374b83ef7f1243f66401c5162c6fb2 Mon Sep 17 00:00:00 2001 From: Kristof De Langhe Date: Tue, 29 Nov 2022 18:02:13 +0100 Subject: [PATCH 01/73] 97075: Edit metadata redesign --- .../dso-edit-metadata-form.ts | 133 ++++++++++++++++++ .../dso-edit-metadata.component.html | 126 +++++++++++++++++ .../dso-edit-metadata.component.scss | 73 ++++++++++ .../dso-edit-metadata.component.ts | 99 +++++++++++++ .../metadata-field-selector.component.html | 18 +++ .../metadata-field-selector.component.scss | 0 .../metadata-field-selector.component.ts | 68 +++++++++ src/app/dso-shared/dso-shared.module.ts | 20 +++ .../edit-item-page/edit-item-page.module.ts | 4 +- .../edit-item-page.routing.module.ts | 3 +- src/styles/_bootstrap_variables_mapping.scss | 6 + 11 files changed, 548 insertions(+), 2 deletions(-) create mode 100644 src/app/dso-shared/dso-edit-metadata/dso-edit-metadata-form.ts create mode 100644 src/app/dso-shared/dso-edit-metadata/dso-edit-metadata.component.html create mode 100644 src/app/dso-shared/dso-edit-metadata/dso-edit-metadata.component.scss create mode 100644 src/app/dso-shared/dso-edit-metadata/dso-edit-metadata.component.ts create mode 100644 src/app/dso-shared/dso-edit-metadata/metadata-field-selector/metadata-field-selector.component.html create mode 100644 src/app/dso-shared/dso-edit-metadata/metadata-field-selector/metadata-field-selector.component.scss create mode 100644 src/app/dso-shared/dso-edit-metadata/metadata-field-selector/metadata-field-selector.component.ts create mode 100644 src/app/dso-shared/dso-shared.module.ts 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 new file mode 100644 index 0000000000..c059f5064f --- /dev/null +++ b/src/app/dso-shared/dso-edit-metadata/dso-edit-metadata-form.ts @@ -0,0 +1,133 @@ +import { MetadataMap, MetadataValue } from '../../core/shared/metadata.models'; +import { hasNoValue, hasValue, isEmpty, isNotEmpty } from '../../shared/empty.util'; + +export enum DsoEditMetadataChangeType { + UPDATE = 1, + ADD = 2, + REMOVE = 3 +} + +export class DsoEditMetadataValue { + originalValue: MetadataValue; + newValue: MetadataValue; + reinstatableValue: MetadataValue; + + editing = false; + change: DsoEditMetadataChangeType; + reinstatableChange: DsoEditMetadataChangeType; + + constructor(value: MetadataValue, added = false) { + this.originalValue = value; + this.newValue = Object.assign(new MetadataValue(), value); + if (added) { + this.change = DsoEditMetadataChangeType.ADD; + this.editing = true; + } + } + + confirmChanges() { + if (hasNoValue(this.change) || this.change === DsoEditMetadataChangeType.UPDATE) { + if ((this.originalValue.value !== this.newValue.value || this.originalValue.language !== this.newValue.language)) { + this.change = DsoEditMetadataChangeType.UPDATE; + } else { + this.change = undefined; + } + } + this.editing = false; + } + + hasChanges(): boolean { + return hasValue(this.change); + } + + discardAndMarkReinstatable(): void { + if (this.change === DsoEditMetadataChangeType.UPDATE) { + this.reinstatableValue = this.newValue; + } + this.reinstatableChange = this.change; + this.discard(); + } + + discard(): void { + this.change = undefined; + this.newValue = Object.assign(new MetadataValue(), this.originalValue); + this.editing = false; + } + + reinstate(): void { + this.newValue = this.reinstatableValue; + this.reinstatableValue = undefined; + this.change = this.reinstatableChange; + this.reinstatableChange = undefined; + } + + isReinstatable(): boolean { + return hasValue(this.reinstatableValue) || hasValue(this.reinstatableChange); + } +} + +export class DsoEditMetadataForm { + fieldKeys: string[]; + fields: { + [mdField: string]: DsoEditMetadataValue[], + }; + newValue: DsoEditMetadataValue; + + constructor(metadata: MetadataMap) { + this.fieldKeys = []; + this.fields = {}; + Object.entries(metadata).forEach(([mdField, values]: [string, MetadataValue[]]) => { + this.fieldKeys.push(mdField); + this.fields[mdField] = values.map((value) => new DsoEditMetadataValue(value)); + }); + } + + add(): void { + if (hasNoValue(this.newValue)) { + this.newValue = new DsoEditMetadataValue(new MetadataValue(), true); + } + } + + setMetadataField(mdField: string) { + if (isEmpty(this.fields[mdField])) { + this.fieldKeys.push(mdField); + this.fields[mdField] = []; + } + this.fields[mdField].push(this.newValue); + this.newValue = undefined; + } + + remove(mdField: string, index: number) { + if (isNotEmpty(this.fields[mdField])) { + this.fields[mdField].splice(index, 1); + if (this.fields[mdField].length === 0) { + this.fieldKeys.splice(this.fieldKeys.indexOf(mdField), 1); + delete this.fields[mdField]; + } + } + } + + hasChanges(): boolean { + return Object.values(this.fields).some((values) => values.some((value) => value.hasChanges())); + } + + discard(): void { + Object.values(this.fields).forEach((values) => { + values.forEach((value) => { + value.discard(); + }); + }); + } + + reinstate(): void { + Object.values(this.fields).forEach((values) => { + values.forEach((value) => { + value.reinstate(); + }); + }); + } + + isReinstatable(): boolean { + return Object.values(this.fields).some((values) => values.some((value) => value.isReinstatable())); + } +} 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 new file mode 100644 index 0000000000..abe671e6f3 --- /dev/null +++ b/src/app/dso-shared/dso-edit-metadata/dso-edit-metadata.component.html @@ -0,0 +1,126 @@ +
+
+ + + + +
+ +
+
Field
+
+
+
Value
+
Lang
+
Edit
+
+
+
+
+
+ +
+
+
+
+ +
+
+ +
+
+
+ + + + +
+
+
+
+
+
+
+ {{ mdField }} +
+
+
+
+
{{ mdValue.newValue.value }}
+ +
+
+
{{ mdValue.newValue.language }}
+ +
+
+
+ + + + + + +
+
+
+
+
+ +
+ +
+
+
+ + + +
+
+
+ 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 new file mode 100644 index 0000000000..a13afaf07e --- /dev/null +++ b/src/app/dso-shared/dso-edit-metadata/dso-edit-metadata.component.scss @@ -0,0 +1,73 @@ +.lbl-cell { + min-width: 210px; + background-color: var(--bs-gray-100); + font-weight: bold; + + &.ds-success { + background-color: var(--bs-success-bg); + border: 1px solid var(--bs-success); + } +} + +.lbl-cell, +.ds-flex-cell { + padding: 1rem; + border: 1px solid var(--bs-gray-200); +} + +.ds-lang-cell { + min-width: 72px; +} + +.ds-edit-cell { + min-width: 173px; +} + +.ds-value-row { + background-color: white; + + &:active { + cursor: grabbing; + } + + &.ds-warning { + background-color: var(--bs-warning-bg); + + .ds-flex-cell { + border: 1px solid var(--bs-warning); + } + } + + &.ds-danger { + background-color: var(--bs-danger-bg); + + .ds-flex-cell { + border: 1px solid var(--bs-danger); + } + } + + &.ds-success { + background-color: var(--bs-success-bg); + + .ds-flex-cell { + border: 1px solid var(--bs-success); + } + } +} + +.ds-drop-list { + background-color: var(--bs-gray-500); +} + +.ds-field-row { + border: 1px solid var(--bs-gray-400); +} + + +.ds-header-row { + background-color: var(--bs-gray-100); +} + +.ds-drag-handle { + 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 new file mode 100644 index 0000000000..d961d9dce9 --- /dev/null +++ b/src/app/dso-shared/dso-edit-metadata/dso-edit-metadata.component.ts @@ -0,0 +1,99 @@ +import { Component, 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'; +import { map, switchMap } 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 { getFirstSucceededRemoteData, metadataFieldsToString } from '../../core/shared/operators'; + +@Component({ + selector: 'ds-dso-edit-metadata', + styleUrls: ['./dso-edit-metadata.component.scss'], + templateUrl: './dso-edit-metadata.component.html', +}) +export class DsoEditMetadataComponent implements OnInit, OnDestroy { + @Input() dso: DSpaceObject; + dsoType: string; + + form: DsoEditMetadataForm; + newMdField: string; + + isReinstatable: boolean; + hasChanges: boolean; + isEmpty: boolean; + + /** + * The AlertType enumeration for access in the component's template + * @type {AlertType} + */ + public AlertTypeEnum = AlertType; + + /** + * The DsoEditMetadataChangeType enumeration for access in the component's template + * @type {DsoEditMetadataChangeType} + */ + public DsoEditMetadataChangeTypeEnum = DsoEditMetadataChangeType; + + dsoUpdateSubscription: Subscription; + + constructor(protected route: ActivatedRoute) { + } + + ngOnInit(): void { + if (hasNoValue(this.dso)) { + this.dsoUpdateSubscription = observableCombineLatest([this.route.data, this.route.parent.data]).pipe( + map(([data, parentData]: [Data, Data]) => Object.assign({}, data, parentData)), + map((data: any) => data.dso) + ).subscribe((rd: RemoteData) => { + this.dso = rd.payload; + this.initForm(); + }); + } else { + 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(); + } + + onDebounce(): void { + this.hasChanges = this.form.hasChanges(); + this.isReinstatable = this.form.isReinstatable(); + this.isEmpty = Object.keys(this.form.fields).length === 0; + } + + submit(): void { + + } + + add(): void { + this.newMdField = undefined; + this.form.add(); + } + + discard(): void { + this.form.discard(); + } + + reinstate(): void { + this.form.reinstate(); + } + + 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 new file mode 100644 index 0000000000..d2761e52a6 --- /dev/null +++ b/src/app/dso-shared/dso-edit-metadata/metadata-field-selector/metadata-field-selector.component.html @@ -0,0 +1,18 @@ +
+ + +
diff --git a/src/app/dso-shared/dso-edit-metadata/metadata-field-selector/metadata-field-selector.component.scss b/src/app/dso-shared/dso-edit-metadata/metadata-field-selector/metadata-field-selector.component.scss new file mode 100644 index 0000000000..e69de29bb2 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 new file mode 100644 index 0000000000..63c27ef062 --- /dev/null +++ b/src/app/dso-shared/dso-edit-metadata/metadata-field-selector/metadata-field-selector.component.ts @@ -0,0 +1,68 @@ +import { Component, EventEmitter, Input, OnDestroy, OnInit, Output } from '@angular/core'; +import { startWith, 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'; +import { RegistryService } from '../../../core/registry/registry.service'; +import { FormControl } from '@angular/forms'; +import { BehaviorSubject } from 'rxjs/internal/BehaviorSubject'; +import { hasValue } from '../../../shared/empty.util'; +import { Subscription } from 'rxjs/internal/Subscription'; + +@Component({ + selector: 'ds-metadata-field-selector', + styleUrls: ['./metadata-field-selector.component.scss'], + templateUrl: './metadata-field-selector.component.html' +}) +export class MetadataFieldSelectorComponent implements OnInit, OnDestroy { + @Input() mdField: string; + @Output() mdFieldChange = new EventEmitter(); + mdFieldOptions$: Observable; + + public input: FormControl = new FormControl(); + + query$: BehaviorSubject = new BehaviorSubject(null); + debounceTime = 500; + + subs: Subscription[] = []; + + constructor(protected registryService: RegistryService) { + } + + ngOnInit(): void { + this.subs.push( + this.input.valueChanges.pipe( + debounceTime(this.debounceTime), + ).subscribe((valueChange) => { + this.query$.next(valueChange); + this.mdField = valueChange; + this.mdFieldChange.emit(this.mdField); + }), + ); + this.mdFieldOptions$ = this.query$.pipe( + distinctUntilChanged(), + switchMap((query) => { + if (query !== null) { + return this.registryService.queryMetadataFields(query, null, true, false, followLink('schema')).pipe( + getAllSucceededRemoteData(), + metadataFieldsToString(), + ); + } else { + return [[]]; + } + }), + ); + } + + select(mdFieldOption: string) { + this.mdField = mdFieldOption; + } + + ngOnDestroy(): void { + this.subs.filter((sub) => hasValue(sub)).forEach((sub) => sub.unsubscribe()); + } +} diff --git a/src/app/dso-shared/dso-shared.module.ts b/src/app/dso-shared/dso-shared.module.ts new file mode 100644 index 0000000000..263d394906 --- /dev/null +++ b/src/app/dso-shared/dso-shared.module.ts @@ -0,0 +1,20 @@ +import { NgModule } from '@angular/core'; +import { SharedModule } from '../shared/shared.module'; +import { DsoEditMetadataComponent } from './dso-edit-metadata/dso-edit-metadata.component'; +import { MetadataFieldSelectorComponent } from './dso-edit-metadata/metadata-field-selector/metadata-field-selector.component'; + +@NgModule({ + imports: [ + SharedModule, + ], + declarations: [ + DsoEditMetadataComponent, + MetadataFieldSelectorComponent, + ], + exports: [ + DsoEditMetadataComponent, + ], +}) +export class DsoSharedModule { + +} diff --git a/src/app/item-page/edit-item-page/edit-item-page.module.ts b/src/app/item-page/edit-item-page/edit-item-page.module.ts index 97901bd7c8..71ab64fa33 100644 --- a/src/app/item-page/edit-item-page/edit-item-page.module.ts +++ b/src/app/item-page/edit-item-page/edit-item-page.module.ts @@ -35,6 +35,7 @@ import { ItemVersionHistoryComponent } from './item-version-history/item-version import { ItemAuthorizationsComponent } from './item-authorizations/item-authorizations.component'; import { ObjectValuesPipe } from '../../shared/utils/object-values-pipe'; import { ResourcePoliciesModule } from '../../shared/resource-policies/resource-policies.module'; +import { DsoSharedModule } from '../../dso-shared/dso-shared.module'; /** @@ -48,7 +49,8 @@ import { ResourcePoliciesModule } from '../../shared/resource-policies/resource- EditItemPageRoutingModule, SearchPageModule, DragDropModule, - ResourcePoliciesModule + ResourcePoliciesModule, + DsoSharedModule, ], declarations: [ EditItemPageComponent, diff --git a/src/app/item-page/edit-item-page/edit-item-page.routing.module.ts b/src/app/item-page/edit-item-page/edit-item-page.routing.module.ts index 2535e42216..bd8af3742e 100644 --- a/src/app/item-page/edit-item-page/edit-item-page.routing.module.ts +++ b/src/app/item-page/edit-item-page/edit-item-page.routing.module.ts @@ -38,6 +38,7 @@ import { ItemPageBitstreamsGuard } from './item-page-bitstreams.guard'; import { ItemPageRelationshipsGuard } from './item-page-relationships.guard'; import { ItemPageVersionHistoryGuard } from './item-page-version-history.guard'; import { ItemPageCollectionMapperGuard } from './item-page-collection-mapper.guard'; +import { DsoEditMetadataComponent } from '../../dso-shared/dso-edit-metadata/dso-edit-metadata.component'; /** * Routing module that handles the routing for the Edit Item page administrator functionality @@ -75,7 +76,7 @@ import { ItemPageCollectionMapperGuard } from './item-page-collection-mapper.gua }, { path: 'metadata', - component: ItemMetadataComponent, + component: DsoEditMetadataComponent, data: { title: 'item.edit.tabs.metadata.title', showBreadcrumbs: true }, canActivate: [ItemPageMetadataGuard] }, diff --git a/src/styles/_bootstrap_variables_mapping.scss b/src/styles/_bootstrap_variables_mapping.scss index 5a64be7e2a..d352b24d38 100644 --- a/src/styles/_bootstrap_variables_mapping.scss +++ b/src/styles/_bootstrap_variables_mapping.scss @@ -29,11 +29,17 @@ --bs-teal: #{$teal}; --bs-cyan: #{$cyan}; --bs-primary: #{$primary}; + --bs-primary-bg: #{lighten($primary, 30%)}; --bs-secondary: #{$secondary}; + --bs-secondary-bg: #{lighten($secondary, 30%)}; --bs-success: #{$success}; + --bs-success-bg: #{lighten($success, 30%)}; --bs-info: #{$info}; + --bs-info-bg: #{lighten($info, 30%)}; --bs-warning: #{$warning}; + --bs-warning-bg: #{lighten($warning, 30%)}; --bs-danger: #{$danger}; + --bs-danger-bg: #{lighten($danger, 30%)}; --bs-light: #{$light}; --bs-dark: #{$dark}; From 627da93e23ab242425fe0dfaefe7e7159a687d2c Mon Sep 17 00:00:00 2001 From: Koen Pauwels Date: Wed, 30 Nov 2022 15:25:36 +0100 Subject: [PATCH 02/73] 97183 Workaround: reorder admin sidebar sections The issue described at https://github.com/DSpace/dspace-angular/issues/1643 was no longer reproducible: The menu component ultimately retrieves menu section information from the store, but in the `MenuComponent#ngOnInit` method, this information is piped through `distinctUntilChanged(compareArraysUsingIds())`, which discards an update that sets these menu elements to be visible. The behavior of this pipe is probably incorrect, but a proper fix is out of scope for the current task. For now, we work around the problem by adding top-level menu sections _after_ their children while initializing the menu section store, which side-steps this issue. --- src/app/menu.resolver.ts | 44 ++++++++++++++++++++-------------------- 1 file changed, 22 insertions(+), 22 deletions(-) diff --git a/src/app/menu.resolver.ts b/src/app/menu.resolver.ts index 8630150c58..60f2d1cca2 100644 --- a/src/app/menu.resolver.ts +++ b/src/app/menu.resolver.ts @@ -171,17 +171,6 @@ export class MenuResolver implements Resolve { ]).subscribe(([isCollectionAdmin, isCommunityAdmin, isSiteAdmin]) => { const menuList = [ /* News */ - { - id: 'new', - active: false, - visible: true, - model: { - type: MenuItemType.TEXT, - text: 'menu.section.new' - } as TextMenuItemModel, - icon: 'plus', - index: 0 - }, { id: 'new_community', parentID: 'new', @@ -232,6 +221,17 @@ export class MenuResolver implements Resolve { link: '/processes/new' } as LinkMenuItemModel, }, + { + id: 'new', + active: false, + visible: true, + model: { + type: MenuItemType.TEXT, + text: 'menu.section.new' + } as TextMenuItemModel, + icon: 'plus', + index: 0 + }, // TODO: enable this menu item once the feature has been implemented // { // id: 'new_item_version', @@ -246,17 +246,6 @@ export class MenuResolver implements Resolve { // }, /* Edit */ - { - id: 'edit', - active: false, - visible: true, - model: { - type: MenuItemType.TEXT, - text: 'menu.section.edit' - } as TextMenuItemModel, - icon: 'pencil-alt', - index: 1 - }, { id: 'edit_community', parentID: 'edit', @@ -296,6 +285,17 @@ export class MenuResolver implements Resolve { } } as OnClickMenuItemModel, }, + { + id: 'edit', + active: false, + visible: true, + model: { + type: MenuItemType.TEXT, + text: 'menu.section.edit' + } as TextMenuItemModel, + icon: 'pencil-alt', + index: 1 + }, /* Statistics */ // TODO: enable this menu item once the feature has been implemented From 105f6cb9542926259c67aed08763b0330782ba8e Mon Sep 17 00:00:00 2001 From: Kristof De Langhe Date: Wed, 30 Nov 2022 17:49:19 +0100 Subject: [PATCH 03/73] 97075: Edit metadata redesign pt2 --- src/app/core/shared/operators.ts | 2 +- .../dso-edit-metadata-form.ts | 100 ++++++++++++++++-- .../dso-edit-metadata.component.html | 43 ++++---- .../dso-edit-metadata.component.scss | 6 +- .../dso-edit-metadata.component.ts | 63 +++++++++-- .../metadata-field-selector.component.ts | 14 ++- 6 files changed, 180 insertions(+), 48 deletions(-) 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 @@