diff --git a/src/app/+item-page/edit-item-page/item-metadata/edit-in-place-field/edit-in-place-field.component.spec.ts b/src/app/+item-page/edit-item-page/item-metadata/edit-in-place-field/edit-in-place-field.component.spec.ts index 565c720a75..02e4bf3413 100644 --- a/src/app/+item-page/edit-item-page/item-metadata/edit-in-place-field/edit-in-place-field.component.spec.ts +++ b/src/app/+item-page/edit-item-page/item-metadata/edit-in-place-field/edit-in-place-field.component.spec.ts @@ -8,7 +8,6 @@ import { RemoteData } from '../../../../core/data/remote-data'; import { PaginatedList } from '../../../../core/data/paginated-list'; import { MetadataField } from '../../../../core/metadata/metadatafield.model'; import { By } from '@angular/platform-browser'; -import { Metadatum } from '../../../../core/shared/metadatum.model'; import { FormsModule } from '@angular/forms'; import { SharedModule } from '../../../../shared/shared.module'; import { getTestScheduler } from 'jasmine-marbles'; @@ -17,6 +16,7 @@ import { TestScheduler } from 'rxjs/testing'; import { MetadataSchema } from '../../../../core/metadata/metadataschema.model'; import { FieldChangeType } from '../../../../core/data/object-updates/object-updates.actions'; import { TranslateModule } from '@ngx-translate/core'; +import { MetadatumViewModel } from '../../../../core/shared/metadata.models'; let comp: EditInPlaceFieldComponent; let fixture: ComponentFixture; @@ -38,7 +38,7 @@ const mdField3 = Object.assign(new MetadataField(), { qualifier: 'abstract' }); -const metadatum = Object.assign(new Metadatum(), { +const metadatum = Object.assign(new MetadatumViewModel(), { key: 'dc.description.abstract', value: 'Example abstract', language: 'en' 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 47bcc66537..0b9bc62c55 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 @@ -1,6 +1,5 @@ import { Component, Input, OnChanges, OnInit } from '@angular/core'; import { hasValue, isNotEmpty } from '../../../../shared/empty.util'; -import { Metadatum } from '../../../../core/shared/metadatum.model'; import { RegistryService } from '../../../../core/registry/registry.service'; import { cloneDeep } from 'lodash'; import { BehaviorSubject, Observable, of as observableOf } from 'rxjs'; @@ -10,8 +9,8 @@ import { InputSuggestion } from '../../../../shared/input-suggestions/input-sugg import { FieldChangeType } from '../../../../core/data/object-updates/object-updates.actions'; import { FieldUpdate } from '../../../../core/data/object-updates/object-updates.reducer'; import { ObjectUpdatesService } from '../../../../core/data/object-updates/object-updates.service'; -import { getSucceededRemoteData } from '../../../../core/shared/operators'; import { NgModel } from '@angular/forms'; +import { MetadatumViewModel } from '../../../../core/shared/metadata.models'; @Component({ // tslint:disable-next-line:component-selector @@ -41,7 +40,7 @@ export class EditInPlaceFieldComponent implements OnInit, OnChanges { /** * The metadatum of this field */ - metadata: Metadatum; + metadata: MetadatumViewModel; /** * Emits whether or not this field is currently editable @@ -118,7 +117,7 @@ export class EditInPlaceFieldComponent implements OnInit, OnChanges { * Sets the current metadatafield based on the fieldUpdate input field */ ngOnChanges(): void { - this.metadata = cloneDeep(this.fieldUpdate.field) as Metadatum; + this.metadata = cloneDeep(this.fieldUpdate.field) as MetadatumViewModel; } /** diff --git a/src/app/+item-page/edit-item-page/item-metadata/item-metadata.component.spec.ts b/src/app/+item-page/edit-item-page/item-metadata/item-metadata.component.spec.ts index 840a086293..c1d36c70fa 100644 --- a/src/app/+item-page/edit-item-page/item-metadata/item-metadata.component.spec.ts +++ b/src/app/+item-page/edit-item-page/item-metadata/item-metadata.component.spec.ts @@ -3,7 +3,6 @@ import { CUSTOM_ELEMENTS_SCHEMA, DebugElement, NO_ERRORS_SCHEMA } from '@angular import { of as observableOf } from 'rxjs'; import { getTestScheduler } from 'jasmine-marbles'; import { ItemMetadataComponent } from './item-metadata.component'; -import { Metadatum } from '../../../core/shared/metadatum.model'; import { TestScheduler } from 'rxjs/testing'; import { SharedModule } from '../../../shared/shared.module'; import { ObjectUpdatesService } from '../../../core/data/object-updates/object-updates.service'; @@ -22,6 +21,7 @@ import { GLOBAL_CONFIG } from '../../../../config'; import { Item } from '../../../core/shared/item.model'; import { FieldChangeType } from '../../../core/data/object-updates/object-updates.actions'; import { RemoteData } from '../../../core/data/remote-data'; +import { MetadatumViewModel } from '../../../core/shared/metadata.models'; let comp: ItemMetadataComponent; let fixture: ComponentFixture; @@ -43,19 +43,19 @@ const notificationsService = jasmine.createSpyObj('notificationsService', success: successNotification } ); -const metadatum1 = Object.assign(new Metadatum(), { +const metadatum1 = Object.assign(new MetadatumViewModel(), { key: 'dc.description.abstract', value: 'Example abstract', language: 'en' }); -const metadatum2 = Object.assign(new Metadatum(), { +const metadatum2 = Object.assign(new MetadatumViewModel(), { key: 'dc.title', value: 'Title test', language: 'de' }); -const metadatum3 = Object.assign(new Metadatum(), { +const metadatum3 = Object.assign(new MetadatumViewModel(), { key: 'dc.contributor.author', value: 'Shakespeare, William', }); @@ -140,7 +140,7 @@ describe('ItemMetadataComponent', () => { }); describe('add', () => { - const md = new Metadatum(); + const md = new MetadatumViewModel(); beforeEach(() => { comp.add(md); }); 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 7b02d6fd1e..fb6820d5fd 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 @@ -10,7 +10,6 @@ import { FieldUpdates, Identifiable } from '../../../core/data/object-updates/object-updates.reducer'; -import { Metadatum } from '../../../core/shared/metadatum.model'; import { first, map, switchMap, take, tap } from 'rxjs/operators'; import { getSucceededRemoteData } from '../../../core/shared/operators'; import { RemoteData } from '../../../core/data/remote-data'; @@ -19,6 +18,8 @@ import { GLOBAL_CONFIG, GlobalConfig } from '../../../../config'; import { TranslateService } from '@ngx-translate/core'; import { RegistryService } from '../../../core/registry/registry.service'; import { MetadataField } from '../../../core/metadata/metadatafield.model'; +import { MetadatumViewModel } from '../../../core/shared/metadata.models'; +import { Metadata } from '../../../core/shared/metadata.utils'; @Component({ selector: 'ds-item-metadata', @@ -92,14 +93,14 @@ export class ItemMetadataComponent implements OnInit { this.checkLastModified(); } }); - this.updates$ = this.objectUpdatesService.getFieldUpdates(this.url, this.item.metadata); + this.updates$ = this.objectUpdatesService.getFieldUpdates(this.url, this.item.metadataAsList); } /** * Sends a new add update for a field to the object updates service * @param metadata The metadata to add, if no parameter is supplied, create a new Metadatum */ - add(metadata: Metadatum = new Metadatum()) { + add(metadata: MetadatumViewModel = new MetadatumViewModel()) { this.objectUpdatesService.saveAddFieldUpdate(this.url, metadata); } @@ -124,7 +125,7 @@ export class ItemMetadataComponent implements OnInit { * Sends all initial values of this item to the object updates service */ private initializeOriginalFields() { - this.objectUpdatesService.initialize(this.url, this.item.metadata, this.item.lastModified); + this.objectUpdatesService.initialize(this.url, this.item.metadataAsList, this.item.lastModified); } /** @@ -141,11 +142,11 @@ export class ItemMetadataComponent implements OnInit { submit() { this.isValid().pipe(first()).subscribe((isValid) => { if (isValid) { - const metadata$: Observable = this.objectUpdatesService.getUpdatedFields(this.url, this.item.metadata) as Observable; + const metadata$: Observable = this.objectUpdatesService.getUpdatedFields(this.url, this.item.metadataAsList) as Observable; metadata$.pipe( first(), - switchMap((metadata: Metadatum[]) => { - const updatedItem: Item = Object.assign(cloneDeep(this.item), { metadata }); + switchMap((metadata: MetadatumViewModel[]) => { + const updatedItem: Item = Object.assign(cloneDeep(this.item), { metadata: Metadata.toMetadataMap(metadata) }); return this.itemService.update(updatedItem); }), tap(() => this.itemService.commitUpdates()), @@ -154,7 +155,7 @@ export class ItemMetadataComponent implements OnInit { (rd: RemoteData) => { this.item = rd.payload; this.initializeOriginalFields(); - this.updates$ = this.objectUpdatesService.getFieldUpdates(this.url, this.item.metadata); + this.updates$ = this.objectUpdatesService.getFieldUpdates(this.url, this.item.metadataAsList); this.notificationsService.success(this.getNotificationTitle('saved'), this.getNotificationContent('saved')); } ) diff --git a/src/app/+item-page/edit-item-page/modify-item-overview/modify-item-overview.component.ts b/src/app/+item-page/edit-item-page/modify-item-overview/modify-item-overview.component.ts index 282f8687e1..974bc8d37f 100644 --- a/src/app/+item-page/edit-item-page/modify-item-overview/modify-item-overview.component.ts +++ b/src/app/+item-page/edit-item-page/modify-item-overview/modify-item-overview.component.ts @@ -1,6 +1,6 @@ import {Component, Input, OnInit} from '@angular/core'; import {Item} from '../../../core/shared/item.model'; -import {MetadataMap} from '../../../core/shared/metadata.interfaces'; +import {MetadataMap} from '../../../core/shared/metadata.models'; @Component({ selector: 'ds-modify-item-overview', diff --git a/src/app/+item-page/field-components/metadata-uri-values/metadata-uri-values.component.ts b/src/app/+item-page/field-components/metadata-uri-values/metadata-uri-values.component.ts index 09d855e951..67684d44af 100644 --- a/src/app/+item-page/field-components/metadata-uri-values/metadata-uri-values.component.ts +++ b/src/app/+item-page/field-components/metadata-uri-values/metadata-uri-values.component.ts @@ -1,7 +1,7 @@ import { Component, Input } from '@angular/core'; import { MetadataValuesComponent } from '../metadata-values/metadata-values.component'; -import { MetadataValue } from '../../../core/shared/metadata.interfaces'; +import { MetadataValue } from '../../../core/shared/metadata.models'; /** * This component renders the configured 'values' into the ds-metadata-field-wrapper component as a link. diff --git a/src/app/+item-page/field-components/metadata-values/metadata-values.component.ts b/src/app/+item-page/field-components/metadata-values/metadata-values.component.ts index 708bdb49c7..abcd90848d 100644 --- a/src/app/+item-page/field-components/metadata-values/metadata-values.component.ts +++ b/src/app/+item-page/field-components/metadata-values/metadata-values.component.ts @@ -1,5 +1,5 @@ import { Component, Input } from '@angular/core'; -import { MetadataValue } from '../../../core/shared/metadata.interfaces'; +import { MetadataValue } from '../../../core/shared/metadata.models'; /** * This component renders the configured 'values' into the ds-metadata-field-wrapper component. diff --git a/src/app/+item-page/full/full-item-page.component.ts b/src/app/+item-page/full/full-item-page.component.ts index fcb724b564..6e19a50864 100644 --- a/src/app/+item-page/full/full-item-page.component.ts +++ b/src/app/+item-page/full/full-item-page.component.ts @@ -6,7 +6,7 @@ import { ActivatedRoute } from '@angular/router'; import { Observable } from 'rxjs'; import { ItemPageComponent } from '../simple/item-page.component'; -import { MetadataMap } from '../../core/shared/metadata.interfaces'; +import { MetadataMap } from '../../core/shared/metadata.models'; import { ItemDataService } from '../../core/data/item-data.service'; import { RemoteData } from '../../core/data/remote-data'; diff --git a/src/app/+search-page/normalized-search-result.model.ts b/src/app/+search-page/normalized-search-result.model.ts index 3c1a46872c..46f14c042d 100644 --- a/src/app/+search-page/normalized-search-result.model.ts +++ b/src/app/+search-page/normalized-search-result.model.ts @@ -1,5 +1,5 @@ import { autoserialize } from 'cerialize'; -import { MetadataMap } from '../core/shared/metadata.interfaces'; +import { MetadataMap } from '../core/shared/metadata.models'; import { ListableObject } from '../shared/object-collection/shared/listable-object.model'; /** diff --git a/src/app/+search-page/search-result.model.ts b/src/app/+search-page/search-result.model.ts index b2e5eafdec..ff865610c6 100644 --- a/src/app/+search-page/search-result.model.ts +++ b/src/app/+search-page/search-result.model.ts @@ -1,5 +1,5 @@ import { DSpaceObject } from '../core/shared/dspace-object.model'; -import { MetadataMap } from '../core/shared/metadata.interfaces'; +import { MetadataMap } from '../core/shared/metadata.models'; import { ListableObject } from '../shared/object-collection/shared/listable-object.model'; /** diff --git a/src/app/core/cache/models/normalized-dspace-object.model.ts b/src/app/core/cache/models/normalized-dspace-object.model.ts index c53607ce79..2248b62509 100644 --- a/src/app/core/cache/models/normalized-dspace-object.model.ts +++ b/src/app/core/cache/models/normalized-dspace-object.model.ts @@ -1,6 +1,6 @@ -import { autoserialize, autoserializeAs, deserialize, serialize } from 'cerialize'; +import { autoserializeAs, deserializeAs } from 'cerialize'; import { DSpaceObject } from '../../shared/dspace-object.model'; -import { MetadataMap } from '../../shared/metadata.interfaces'; +import { MetadataMap, MetadataMapSerializer } from '../../shared/metadata.models'; import { ResourceType } from '../../shared/resource-type'; import { mapsTo } from '../builders/build-decorators'; import { NormalizedObject } from './normalized-object.model'; @@ -17,7 +17,7 @@ export class NormalizedDSpaceObject extends NormalizedOb * Repeated here to make the serialization work, * inheritSerialization doesn't seem to work for more than one level */ - @deserialize + @deserializeAs(String) self: string; /** @@ -35,31 +35,31 @@ export class NormalizedDSpaceObject extends NormalizedOb * Repeated here to make the serialization work, * inheritSerialization doesn't seem to work for more than one level */ - @autoserialize + @autoserializeAs(String) uuid: string; /** * A string representing the kind of DSpaceObject, e.g. community, item, … */ - @autoserialize + @autoserializeAs(String) type: ResourceType; /** * All metadata of this DSpaceObject */ - @autoserialize + @autoserializeAs(MetadataMapSerializer) metadata: MetadataMap; /** * An array of DSpaceObjects that are direct parents of this DSpaceObject */ - @deserialize + @deserializeAs(String) parents: string[]; /** * The DSpaceObject that owns this DSpaceObject */ - @deserialize + @deserializeAs(String) owner: string; /** @@ -68,7 +68,7 @@ export class NormalizedDSpaceObject extends NormalizedOb * Repeated here to make the serialization work, * inheritSerialization doesn't seem to work for more than one level */ - @deserialize + @deserializeAs(Object) _links: { [name: string]: string } diff --git a/src/app/core/data/search-response-parsing.service.ts b/src/app/core/data/search-response-parsing.service.ts index 46b2572c4e..8e3171d05e 100644 --- a/src/app/core/data/search-response-parsing.service.ts +++ b/src/app/core/data/search-response-parsing.service.ts @@ -7,7 +7,7 @@ import { DSpaceRESTV2Response } from '../dspace-rest-v2/dspace-rest-v2-response. import { DSpaceRESTv2Serializer } from '../dspace-rest-v2/dspace-rest-v2.serializer'; import { hasValue } from '../../shared/empty.util'; import { SearchQueryResponse } from '../../+search-page/search-service/search-query-response.model'; -import { MetadataMap, MetadataValue } from '../shared/metadata.interfaces'; +import { MetadataMap, MetadataValue } from '../shared/metadata.models'; @Injectable() export class SearchResponseParsingService implements ResponseParsingService { @@ -22,7 +22,7 @@ export class SearchResponseParsingService implements ResponseParsingService { const mdMap: MetadataMap = {}; if (hhObject) { for (const key of Object.keys(hhObject)) { - const value: MetadataValue = { value: hhObject[key].join('...'), language: null }; + const value: MetadataValue = Object.assign(new MetadataValue(), { value: hhObject[key].join('...'), language: null }); mdMap[key] = [ value ]; } } diff --git a/src/app/core/eperson/models/eperson.model.ts b/src/app/core/eperson/models/eperson.model.ts index c349d22a9e..0218e52c7d 100644 --- a/src/app/core/eperson/models/eperson.model.ts +++ b/src/app/core/eperson/models/eperson.model.ts @@ -20,6 +20,6 @@ export class EPerson extends DSpaceObject { public selfRegistered: boolean; get name(): string { - return this.findMetadata('eperson.firstname') + ' ' + this.findMetadata('eperson.lastname'); + return this.firstMetadataValue('eperson.firstname') + ' ' + this.firstMetadataValue('eperson.lastname'); } } diff --git a/src/app/core/metadata/metadata.service.spec.ts b/src/app/core/metadata/metadata.service.spec.ts index 15b1bb52df..bd3532b840 100644 --- a/src/app/core/metadata/metadata.service.spec.ts +++ b/src/app/core/metadata/metadata.service.spec.ts @@ -37,7 +37,7 @@ import { HttpClient } from '@angular/common/http'; import { EmptyError } from 'rxjs/internal-compatibility'; import { NormalizedObjectBuildService } from '../cache/builders/normalized-object-build.service'; import { DSOChangeAnalyzer } from '../data/dso-change-analyzer.service'; -import { MetadataValue } from '../shared/metadata.interfaces'; +import { MetadataValue } from '../shared/metadata.models'; /* tslint:disable:max-classes-per-file */ @Component({ diff --git a/src/app/core/shared/dspace-object.model.ts b/src/app/core/shared/dspace-object.model.ts index edd82eca85..b23de0da90 100644 --- a/src/app/core/shared/dspace-object.model.ts +++ b/src/app/core/shared/dspace-object.model.ts @@ -1,11 +1,10 @@ -import { MetadataMap, MetadataValue, MetadataValueFilter } from './metadata.interfaces'; -import { Metadata } from './metadata.model'; +import { MetadataMap, MetadataValue, MetadataValueFilter } from './metadata.models'; +import { Metadata } from './metadata.utils'; import { CacheableObject } from '../cache/object-cache.reducer'; import { RemoteData } from '../data/remote-data'; import { ResourceType } from './resource-type'; import { ListableObject } from '../../shared/object-collection/shared/listable-object.model'; import { Observable } from 'rxjs'; -import { autoserialize } from 'cerialize'; /** * An abstract model class for a DSpaceObject. @@ -17,13 +16,11 @@ export class DSpaceObject implements CacheableObject, ListableObject { /** * The human-readable identifier of this DSpaceObject */ - @autoserialize id: string; /** * The universally unique identifier of this DSpaceObject */ - @autoserialize uuid: string; /** @@ -41,9 +38,12 @@ export class DSpaceObject implements CacheableObject, ListableObject { /** * All metadata of this DSpaceObject */ - @autoserialize metadata: MetadataMap; + get metadataAsList() { + return Metadata.toViewModelList(this.metadata); + } + /** * An array of DSpaceObjects that are direct parents of this DSpaceObject */ diff --git a/src/app/core/shared/metadata.interfaces.ts b/src/app/core/shared/metadata.interfaces.ts deleted file mode 100644 index 3590117ce8..0000000000 --- a/src/app/core/shared/metadata.interfaces.ts +++ /dev/null @@ -1,30 +0,0 @@ -/** A map of metadata keys to an ordered list of MetadataValue objects. */ -export interface MetadataMap { - [ key: string ]: MetadataValue[]; -} - -/** A single metadata value and its properties. */ -export interface MetadataValue { - - /** The language. */ - language: string; - - /** The string value. */ - value: string; -} - -/** Constraints for matching metadata values. */ -export interface MetadataValueFilter { - - /** The language constraint. */ - language?: string; - - /** The value constraint. */ - value?: string; - - /** Whether the value constraint should match without regard to case. */ - ignoreCase?: boolean; - - /** Whether the value constraint should match as a substring. */ - substring?: boolean; -} diff --git a/src/app/core/shared/metadata.model.spec.ts b/src/app/core/shared/metadata.model.spec.ts index dfeff8d600..7fbea14b13 100644 --- a/src/app/core/shared/metadata.model.spec.ts +++ b/src/app/core/shared/metadata.model.spec.ts @@ -1,10 +1,16 @@ import { isUndefined } from '../../shared/empty.util'; -import { MetadataValue, MetadataValueFilter } from './metadata.interfaces'; -import { Metadata } from './metadata.model'; +import * as uuidv4 from 'uuid/v4'; +import { + MetadataMap, + MetadataValue, + MetadataValueFilter, + MetadatumViewModel +} from './metadata.models'; +import { Metadata } from './metadata.utils'; const mdValue = (value: string, language?: string): MetadataValue => { - return { value: value, language: isUndefined(language) ? null : language }; -} + return { uuid: uuidv4(), value: value, language: isUndefined(language) ? null : language }; +}; const dcDescription = mdValue('Some description'); const dcAbstract = mdValue('Some abstract'); @@ -13,19 +19,27 @@ const dcTitle1 = mdValue('Title 1'); const dcTitle2 = mdValue('Title 2', 'en_US'); const bar = mdValue('Bar'); -const singleMap = { 'dc.title': [ dcTitle0 ] }; +const singleMap = { 'dc.title': [dcTitle0] }; const multiMap = { - 'dc.description': [ dcDescription ], - 'dc.description.abstract': [ dcAbstract ], - 'dc.title': [ dcTitle1, dcTitle2 ], - 'foo': [ bar ] + 'dc.description': [dcDescription], + 'dc.description.abstract': [dcAbstract], + 'dc.title': [dcTitle1, dcTitle2], + 'foo': [bar] }; +const multiViewModelList = [ + { key: 'dc.description', ...dcDescription, order: 0 }, + { key: 'dc.description.abstract', ...dcAbstract, order: 0 }, + { key: 'dc.title', ...dcTitle1, order: 0 }, + { key: 'dc.title', ...dcTitle2, order: 1 }, + { key: 'foo', ...bar, order: 0 } +]; + const testMethod = (fn, resultKind, mapOrMaps, keyOrKeys, expected, filter?) => { - const keys = keyOrKeys instanceof Array ? keyOrKeys : [ keyOrKeys ]; + const keys = keyOrKeys instanceof Array ? keyOrKeys : [keyOrKeys]; describe('and key' + (keys.length === 1 ? (' ' + keys[0]) : ('s ' + JSON.stringify(keys))) - + ' with ' + (isUndefined(filter) ? 'no filter' : 'filter ' + JSON.stringify(filter)), () => { + + ' with ' + (isUndefined(filter) ? 'no filter' : 'filter ' + JSON.stringify(filter)), () => { const result = fn(mapOrMaps, keys, filter); let shouldReturn; if (resultKind === 'boolean') { @@ -34,7 +48,7 @@ const testMethod = (fn, resultKind, mapOrMaps, keyOrKeys, expected, filter?) => shouldReturn = 'undefined'; } else if (expected instanceof Array) { shouldReturn = 'an array with ' + expected.length + ' ' + (expected.length > 1 ? 'ordered ' : '') - + resultKind + (expected.length !== 1 ? 's' : ''); + + resultKind + (expected.length !== 1 ? 's' : ''); } else { shouldReturn = 'a ' + resultKind; } @@ -57,30 +71,30 @@ describe('Metadata', () => { }); describe('with singleMap', () => { testAll(singleMap, 'foo', []); - testAll(singleMap, '*', [ dcTitle0 ]); + testAll(singleMap, '*', [dcTitle0]); testAll(singleMap, '*', [], { value: 'baz' }); - testAll(singleMap, 'dc.title', [ dcTitle0 ]); - testAll(singleMap, 'dc.*', [ dcTitle0 ]); + testAll(singleMap, 'dc.title', [dcTitle0]); + testAll(singleMap, 'dc.*', [dcTitle0]); }); describe('with multiMap', () => { - testAll(multiMap, 'foo', [ bar ]); - testAll(multiMap, '*', [ dcDescription, dcAbstract, dcTitle1, dcTitle2, bar ]); - testAll(multiMap, 'dc.title', [ dcTitle1, dcTitle2 ]); - testAll(multiMap, 'dc.*', [ dcDescription, dcAbstract, dcTitle1, dcTitle2 ]); - testAll(multiMap, [ 'dc.title', 'dc.*' ], [ dcTitle1, dcTitle2, dcDescription, dcAbstract ]); + testAll(multiMap, 'foo', [bar]); + testAll(multiMap, '*', [dcDescription, dcAbstract, dcTitle1, dcTitle2, bar]); + testAll(multiMap, 'dc.title', [dcTitle1, dcTitle2]); + testAll(multiMap, 'dc.*', [dcDescription, dcAbstract, dcTitle1, dcTitle2]); + testAll(multiMap, ['dc.title', 'dc.*'], [dcTitle1, dcTitle2, dcDescription, dcAbstract]); }); describe('with [ singleMap, multiMap ]', () => { - testAll([ singleMap, multiMap ], 'foo', [ bar ]); - testAll([ singleMap, multiMap ], '*', [ dcTitle0 ]); - testAll([ singleMap, multiMap ], 'dc.title', [ dcTitle0 ]); - testAll([ singleMap, multiMap ], 'dc.*', [ dcTitle0 ]); + testAll([singleMap, multiMap], 'foo', [bar]); + testAll([singleMap, multiMap], '*', [dcTitle0]); + testAll([singleMap, multiMap], 'dc.title', [dcTitle0]); + testAll([singleMap, multiMap], 'dc.*', [dcTitle0]); }); describe('with [ multiMap, singleMap ]', () => { - testAll([ multiMap, singleMap ], 'foo', [ bar ]); - testAll([ multiMap, singleMap ], '*', [ dcDescription, dcAbstract, dcTitle1, dcTitle2, bar ]); - testAll([ multiMap, singleMap ], 'dc.title', [ dcTitle1, dcTitle2 ]); - testAll([ multiMap, singleMap ], 'dc.*', [ dcDescription, dcAbstract, dcTitle1, dcTitle2 ]); - testAll([ multiMap, singleMap ], [ 'dc.title', 'dc.*' ], [ dcTitle1, dcTitle2, dcDescription, dcAbstract ]); + testAll([multiMap, singleMap], 'foo', [bar]); + testAll([multiMap, singleMap], '*', [dcDescription, dcAbstract, dcTitle1, dcTitle2, bar]); + testAll([multiMap, singleMap], 'dc.title', [dcTitle1, dcTitle2]); + testAll([multiMap, singleMap], 'dc.*', [dcDescription, dcAbstract, dcTitle1, dcTitle2]); + testAll([multiMap, singleMap], ['dc.title', 'dc.*'], [dcTitle1, dcTitle2, dcDescription, dcAbstract]); }); }); @@ -93,10 +107,10 @@ describe('Metadata', () => { testAllValues({}, '*', []); }); describe('with singleMap', () => { - testAllValues([ singleMap, multiMap ], '*', [ dcTitle0.value ]); + testAllValues([singleMap, multiMap], '*', [dcTitle0.value]); }); describe('with [ multiMap, singleMap ]', () => { - testAllValues([ multiMap, singleMap ], '*', [ dcDescription.value, dcAbstract.value, dcTitle1.value, dcTitle2.value, bar.value ]); + testAllValues([multiMap, singleMap], '*', [dcDescription.value, dcAbstract.value, dcTitle1.value, dcTitle2.value, bar.value]); }); }); @@ -112,7 +126,7 @@ describe('Metadata', () => { testFirst(singleMap, '*', dcTitle0); }); describe('with [ multiMap, singleMap ]', () => { - testFirst([ multiMap, singleMap ], '*', dcDescription); + testFirst([multiMap, singleMap], '*', dcDescription); }); }); @@ -128,7 +142,7 @@ describe('Metadata', () => { testFirstValue(singleMap, '*', dcTitle0.value); }); describe('with [ multiMap, singleMap ]', () => { - testFirstValue([ multiMap, singleMap ], '*', dcDescription.value); + testFirstValue([multiMap, singleMap], '*', dcDescription.value); }); }); @@ -145,7 +159,7 @@ describe('Metadata', () => { testHas(singleMap, '*', false, { value: 'baz' }); }); describe('with [ multiMap, singleMap ]', () => { - testHas([ multiMap, singleMap ], '*', true); + testHas([multiMap, singleMap], '*', true); }); }); @@ -153,7 +167,7 @@ describe('Metadata', () => { const testValueMatches = (value: MetadataValue, expected: boolean, filter?: MetadataValueFilter) => { describe('with value ' + JSON.stringify(value) + ' and filter ' - + (isUndefined(filter) ? 'undefined' : JSON.stringify(filter)), () => { + + (isUndefined(filter) ? 'undefined' : JSON.stringify(filter)), () => { const result = Metadata.valueMatches(value, filter); it('should return ' + expected, () => { expect(result).toEqual(expected); @@ -172,4 +186,32 @@ describe('Metadata', () => { testValueMatches(mdValue('a', 'en_US'), true, { language: 'en_US' }); }); + describe('toViewModelList method', () => { + + const testToViewModelList = (map: MetadataMap, expected: MetadatumViewModel[]) => { + describe('with map ' + JSON.stringify(map), () => { + const result = Metadata.toViewModelList(map); + it('should return ' + JSON.stringify(expected), () => { + expect(result).toEqual(expected); + }); + }); + }; + + testToViewModelList(multiMap, multiViewModelList); + }); + + describe('toMetadataMap method', () => { + + const testToMetadataMap = (metadatumList: MetadatumViewModel[], expected: MetadataMap) => { + describe('with metadatum list ' + JSON.stringify(metadatumList), () => { + const result = Metadata.toMetadataMap(metadatumList); + it('should return ' + JSON.stringify(expected), () => { + expect(result).toEqual(expected); + }); + }); + }; + + testToMetadataMap(multiViewModelList, multiMap); + }); + }); diff --git a/src/app/core/shared/metadata.models.ts b/src/app/core/shared/metadata.models.ts new file mode 100644 index 0000000000..9ff08d3c3d --- /dev/null +++ b/src/app/core/shared/metadata.models.ts @@ -0,0 +1,75 @@ +import * as uuidv4 from 'uuid/v4'; +import { autoserialize, Serialize, Deserialize } from 'cerialize'; + +/** A map of metadata keys to an ordered list of MetadataValue objects. */ +export class MetadataMap { + [key: string]: MetadataValue[]; +} + +/** A single metadata value and its properties. */ + +export class MetadataValue { + + /** The uuid. */ + uuid: string = uuidv4(); + + /** The language. */ + @autoserialize + language: string; + + /** The string value. */ + @autoserialize + value: string; + +} + +/** Constraints for matching metadata values. */ +export interface MetadataValueFilter { + /** The language constraint. */ + language?: string; + + /** The value constraint. */ + value?: string; + + /** Whether the value constraint should match without regard to case. */ + ignoreCase?: boolean; + + /** Whether the value constraint should match as a substring. */ + substring?: boolean; +} + +export class MetadatumViewModel { + /** The uuid. */ + uuid: string = uuidv4(); + + /** The metadatafield key. */ + key: string; + + /** The language. */ + language: string; + + /** The string value. */ + value: string; + + /** The order. */ + order: number; +} + + +export const MetadataMapSerializer = { + Serialize(map: MetadataMap): any { + const json = {}; + Object.keys(map).forEach((key: string) => { + json[key] = Serialize(map[key], MetadataValue); + }); + return json; + }, + + Deserialize(json: any): MetadataMap { + const metadataMap: MetadataMap = {}; + Object.keys(json).forEach((key: string) => { + metadataMap[key] = Deserialize(json[key], MetadataValue); + }); + return metadataMap; + } +}; diff --git a/src/app/core/shared/metadata.model.ts b/src/app/core/shared/metadata.utils.ts similarity index 85% rename from src/app/core/shared/metadata.model.ts rename to src/app/core/shared/metadata.utils.ts index 2b29659252..f6edc619dc 100644 --- a/src/app/core/shared/metadata.model.ts +++ b/src/app/core/shared/metadata.utils.ts @@ -1,5 +1,11 @@ import { isEmpty, isNotUndefined, isUndefined } from '../../shared/empty.util'; -import { MetadataMap, MetadataValue, MetadataValueFilter } from './metadata.interfaces'; +import { + MetadataMap, + MetadataValue, + MetadataValueFilter, + MetadatumViewModel +} from './metadata.models'; +import { groupBy, sortBy } from 'lodash'; /** * Utility class for working with DSpace object metadata. @@ -27,7 +33,7 @@ export class Metadata { */ public static all(mapOrMaps: MetadataMap | MetadataMap[], keyOrKeys: string | string[], filter?: MetadataValueFilter): MetadataValue[] { - const mdMaps: MetadataMap[] = mapOrMaps instanceof Array ? mapOrMaps : [ mapOrMaps ]; + const mdMaps: MetadataMap[] = mapOrMaps instanceof Array ? mapOrMaps : [mapOrMaps]; const matches: MetadataValue[] = []; for (const mdMap of mdMaps) { for (const mdKey of Metadata.resolveKeys(mdMap, keyOrKeys)) { @@ -71,7 +77,7 @@ export class Metadata { */ public static first(mdMapOrMaps: MetadataMap | MetadataMap[], keyOrKeys: string | string[], filter?: MetadataValueFilter): MetadataValue { - const mdMaps: MetadataMap[] = mdMapOrMaps instanceof Array ? mdMapOrMaps : [ mdMapOrMaps ]; + const mdMaps: MetadataMap[] = mdMapOrMaps instanceof Array ? mdMapOrMaps : [mdMapOrMaps]; for (const mdMap of mdMaps) { for (const key of Metadata.resolveKeys(mdMap, keyOrKeys)) { const values: MetadataValue[] = mdMap[key]; @@ -144,7 +150,7 @@ export class Metadata { * @param {string|string[]} keyOrKeys The metadata key(s) in scope. Wildcards are supported; see above. */ private static resolveKeys(mdMap: MetadataMap, keyOrKeys: string | string[]): string[] { - const inputKeys: string[] = keyOrKeys instanceof Array ? keyOrKeys : [ keyOrKeys ]; + const inputKeys: string[] = keyOrKeys instanceof Array ? keyOrKeys : [keyOrKeys]; const outputKeys: string[] = []; for (const inputKey of inputKeys) { if (inputKey.includes('*')) { @@ -160,4 +166,31 @@ export class Metadata { } return outputKeys; } + + public static toViewModelList(mdMap: MetadataMap) { + let metadatumList: MetadatumViewModel[] = []; + Object.keys(mdMap).forEach((key: string) => { + const fields = mdMap[key].map( + (metadataValue: MetadataValue, index: number) => + Object.assign( + {}, + metadataValue, + { + order: index, + key + })); + metadatumList = [...metadatumList, ...fields]; + }); + return metadatumList; + } + + public static toMetadataMap(viewModelList: MetadatumViewModel[]) { + const metadataMap: MetadataMap = {}; + const groupedList = groupBy(viewModelList, (viewModel) => viewModel.key); + Object.keys(groupedList).forEach((key: string) => { + const orderedValues = sortBy(groupedList[key], ['order']); + metadataMap[key] = orderedValues.map((value: MetadataValue, index: number) => Object.assign({}, value, { order: index })) + }); + return metadataMap; + } } diff --git a/src/app/shared/comcol-forms/comcol-form/comcol-form.component.ts b/src/app/shared/comcol-forms/comcol-form/comcol-form.component.ts index 6b28579491..f8263fab73 100644 --- a/src/app/shared/comcol-forms/comcol-form/comcol-form.component.ts +++ b/src/app/shared/comcol-forms/comcol-form/comcol-form.component.ts @@ -8,7 +8,7 @@ import { FormGroup } from '@angular/forms'; import { DynamicFormControlModel } from '@ng-dynamic-forms/core/src/model/dynamic-form-control.model'; import { TranslateService } from '@ngx-translate/core'; import { DSpaceObject } from '../../../core/shared/dspace-object.model'; -import { MetadataMap, MetadataValue } from '../../../core/shared/metadata.interfaces'; +import { MetadataMap, MetadataValue } from '../../../core/shared/metadata.models'; import { isNotEmpty } from '../../empty.util'; import { ResourceType } from '../../../core/shared/resource-type'; @@ -83,11 +83,14 @@ export class ComColFormComponent implements OnInit { onSubmit() { const formMetadata = new Object() as MetadataMap; this.formModel.forEach((fieldModel: DynamicInputModel) => { - const value: MetadataValue = { value: fieldModel.value as string, language: null }; + const value: MetadataValue = Object.assign(new MetadataValue(), { + value: fieldModel.value as string, + language: null + }); if (formMetadata.hasOwnProperty(fieldModel.name)) { formMetadata[fieldModel.name].push(value); } else { - formMetadata[fieldModel.name] = [ value ]; + formMetadata[fieldModel.name] = [value]; } }); diff --git a/src/app/shared/input-suggestions/input-suggestions.component.ts b/src/app/shared/input-suggestions/input-suggestions.component.ts index 620c984be3..727421c83e 100644 --- a/src/app/shared/input-suggestions/input-suggestions.component.ts +++ b/src/app/shared/input-suggestions/input-suggestions.component.ts @@ -12,7 +12,7 @@ import { ViewChildren } from '@angular/core'; import { BehaviorSubject } from 'rxjs'; -import { hasValue, isNotEmpty } from '../empty.util'; +import { hasValue, isNotEmpty, isNotUndefined } from '../empty.util'; import { InputSuggestion } from './input-suggestions.model'; import { ControlValueAccessor, NG_VALUE_ACCESSOR } from '@angular/forms'; @@ -128,7 +128,7 @@ export class InputSuggestionsComponent implements ControlValueAccessor, OnChange */ ngOnChanges(changes: SimpleChanges) { if (hasValue(changes.suggestions)) { - this.show.next(isNotEmpty(changes.suggestions.currentValue)); + this.show.next(isNotEmpty(changes.suggestions.currentValue) && !changes.suggestions.firstChange); } } diff --git a/src/app/shared/object-grid/search-result-grid-element/search-result-grid-element.component.ts b/src/app/shared/object-grid/search-result-grid-element/search-result-grid-element.component.ts index 0901b7b8cc..844d0bc165 100644 --- a/src/app/shared/object-grid/search-result-grid-element/search-result-grid-element.component.ts +++ b/src/app/shared/object-grid/search-result-grid-element/search-result-grid-element.component.ts @@ -6,7 +6,7 @@ import { AbstractListableElementComponent } from '../../object-collection/shared import { ListableObject } from '../../object-collection/shared/listable-object.model'; import { TruncatableService } from '../../truncatable/truncatable.service'; import { Observable } from 'rxjs'; -import { Metadata } from '../../../core/shared/metadata.model'; +import { Metadata } from '../../../core/shared/metadata.utils'; @Component({ selector: 'ds-search-result-grid-element', diff --git a/src/app/shared/object-list/search-result-list-element/search-result-list-element.component.ts b/src/app/shared/object-list/search-result-list-element/search-result-list-element.component.ts index 2a16b0b754..525d39e798 100644 --- a/src/app/shared/object-list/search-result-list-element/search-result-list-element.component.ts +++ b/src/app/shared/object-list/search-result-list-element/search-result-list-element.component.ts @@ -6,7 +6,7 @@ import { DSpaceObject } from '../../../core/shared/dspace-object.model'; import { ListableObject } from '../../object-collection/shared/listable-object.model'; import { AbstractListableElementComponent } from '../../object-collection/shared/object-collection-element/abstract-listable-element.component'; import { TruncatableService } from '../../truncatable/truncatable.service'; -import { Metadata } from '../../../core/shared/metadata.model'; +import { Metadata } from '../../../core/shared/metadata.utils'; @Component({ selector: 'ds-search-result-list-element',