DS-4107 Retrieve and model metadata as a map

This commit is contained in:
Chris Wilper
2018-12-22 23:03:45 -05:00
parent 34e78bd495
commit 2368df9513
35 changed files with 310 additions and 281 deletions

View File

@@ -7,10 +7,12 @@
</tr> </tr>
</thead> </thead>
<tbody> <tbody>
<tr *ngFor="let metadatum of metadata" class="metadata-row"> <ng-container *ngFor="let entry of metadata | keyvalue">
<td>{{metadatum.key}}</td> <tr *ngFor="let value of entry.value" class="metadata-row">
<td>{{metadatum.value}}</td> <td>{{entry.key}}</td>
<td>{{metadatum.language}}</td> <td>{{value.value}}</td>
</tr> <td>{{value.language}}</td>
</tr>
</ng-container>
</tbody> </tbody>
</table> </table>

View File

@@ -1,6 +1,6 @@
import {Component, Input, OnInit} from '@angular/core'; import {Component, Input, OnInit} from '@angular/core';
import {Item} from '../../../core/shared/item.model'; import {Item} from '../../../core/shared/item.model';
import {Metadatum} from '../../../core/shared/metadatum.model'; import {MetadataMap} from '../../../core/shared/metadata.interfaces';
@Component({ @Component({
selector: 'ds-modify-item-overview', selector: 'ds-modify-item-overview',
@@ -12,7 +12,7 @@ import {Metadatum} from '../../../core/shared/metadatum.model';
export class ModifyItemOverviewComponent implements OnInit { export class ModifyItemOverviewComponent implements OnInit {
@Input() item: Item; @Input() item: Item;
metadata: Metadatum[]; metadata: MetadataMap;
ngOnInit(): void { ngOnInit(): void {
this.metadata = this.item.metadata; this.metadata = this.item.metadata;

View File

@@ -1,5 +1,5 @@
<ds-metadata-field-wrapper [label]="label | translate"> <ds-metadata-field-wrapper [label]="label | translate">
<a *ngFor="let metadatum of values; let last=last;" [href]="metadatum.value"> <a *ngFor="let value of values; let last=last;" [href]="value.value">
{{ linktext || metadatum.value }}<span *ngIf="!last" [innerHTML]="separator"></span> {{ linktext || value.value }}<span *ngIf="!last" [innerHTML]="separator"></span>
</a> </a>
</ds-metadata-field-wrapper> </ds-metadata-field-wrapper>

View File

@@ -1,6 +1,7 @@
import { Component, Input } from '@angular/core'; import { Component, Input } from '@angular/core';
import { MetadataValuesComponent } from '../metadata-values/metadata-values.component'; import { MetadataValuesComponent } from '../metadata-values/metadata-values.component';
import { MetadataValue } from '../../../core/shared/metadata.interfaces';
/** /**
* This component renders the configured 'values' into the ds-metadata-field-wrapper component as a link. * This component renders the configured 'values' into the ds-metadata-field-wrapper component as a link.
@@ -18,7 +19,7 @@ export class MetadataUriValuesComponent extends MetadataValuesComponent {
@Input() linktext: any; @Input() linktext: any;
@Input() values: any; @Input() values: MetadataValue[];
@Input() separator: string; @Input() separator: string;

View File

@@ -1,5 +1,5 @@
<ds-metadata-field-wrapper [label]="label | translate"> <ds-metadata-field-wrapper [label]="label | translate">
<span *ngFor="let metadatum of values; let last=last;"> <span *ngFor="let value of values; let last=last;">
{{metadatum.value}}<span *ngIf="!last" [innerHTML]="separator"></span> {{value.value}}<span *ngIf="!last" [innerHTML]="separator"></span>
</span> </span>
</ds-metadata-field-wrapper> </ds-metadata-field-wrapper>

View File

@@ -1,5 +1,5 @@
import { Component, Input } from '@angular/core'; import { Component, Input } from '@angular/core';
import { Metadatum } from '../../../core/shared/metadatum.model'; import { MetadataValue } from '../../../core/shared/metadata.interfaces';
/** /**
* This component renders the configured 'values' into the ds-metadata-field-wrapper component. * This component renders the configured 'values' into the ds-metadata-field-wrapper component.
@@ -12,7 +12,7 @@ import { Metadatum } from '../../../core/shared/metadatum.model';
}) })
export class MetadataValuesComponent { export class MetadataValuesComponent {
@Input() values: Metadatum[]; @Input() values: MetadataValue[];
@Input() separator: string; @Input() separator: string;

View File

@@ -17,7 +17,7 @@
<dt class="col-md-4">{{"item.page.filesection.description" | translate}}</dt> <dt class="col-md-4">{{"item.page.filesection.description" | translate}}</dt>
<dd class="col-md-8">{{file.findMetadata("dc.description")}}</dd> <dd class="col-md-8">{{file.firstMetadataValue("dc.description")}}</dd>
</dl> </dl>
</div> </div>
<div class="col-2"> <div class="col-2">

View File

@@ -9,11 +9,13 @@
</div> </div>
<table class="table table-responsive table-striped"> <table class="table table-responsive table-striped">
<tbody> <tbody>
<tr *ngFor="let metadatum of (metadata$ | async)"> <ng-container *ngFor="let entry of (metadata$ | async) | keyvalue">
<td>{{metadatum.key}}</td> <tr *ngFor="let value of entry.value">
<td>{{metadatum.value}}</td> <td>{{entry.key}}</td>
<td>{{metadatum.language}}</td> <td>{{value.value}}</td>
</tr> <td>{{value.language}}</td>
</tr>
</ng-container>
</tbody> </tbody>
</table> </table>
<ds-item-page-full-file-section [item]="item"></ds-item-page-full-file-section> <ds-item-page-full-file-section [item]="item"></ds-item-page-full-file-section>

View File

@@ -6,7 +6,7 @@ import { ActivatedRoute } from '@angular/router';
import { Observable } from 'rxjs'; import { Observable } from 'rxjs';
import { ItemPageComponent } from '../simple/item-page.component'; import { ItemPageComponent } from '../simple/item-page.component';
import { Metadatum } from '../../core/shared/metadatum.model'; import { MetadataMap } from '../../core/shared/metadata.interfaces';
import { ItemDataService } from '../../core/data/item-data.service'; import { ItemDataService } from '../../core/data/item-data.service';
import { RemoteData } from '../../core/data/remote-data'; import { RemoteData } from '../../core/data/remote-data';
@@ -34,7 +34,7 @@ export class FullItemPageComponent extends ItemPageComponent implements OnInit {
itemRD$: Observable<RemoteData<Item>>; itemRD$: Observable<RemoteData<Item>>;
metadata$: Observable<Metadatum[]>; metadata$: Observable<MetadataMap>;
constructor(route: ActivatedRoute, items: ItemDataService, metadataService: MetadataService) { constructor(route: ActivatedRoute, items: ItemDataService, metadataService: MetadataService) {
super(route, items, metadataService); super(route, items, metadataService);

View File

@@ -1,3 +1,3 @@
<div class="item-page-specific-field"> <div class="item-page-specific-field">
<ds-metadata-values [values]="item?.filterMetadata(fields)" [separator]="separator" [label]="label"></ds-metadata-values> <ds-metadata-values [values]="item?.allMetadata(fields)" [separator]="separator" [label]="label"></ds-metadata-values>
</div> </div>

View File

@@ -1,3 +1,3 @@
<h2 class="item-page-title-field"> <h2 class="item-page-title-field">
<ds-metadata-values [values]="item?.filterMetadata(fields)"></ds-metadata-values> <ds-metadata-values [values]="item?.allMetadata(fields)"></ds-metadata-values>
</h2> </h2>

View File

@@ -1,3 +1,3 @@
<div class="item-page-specific-field"> <div class="item-page-specific-field">
<ds-metadata-uri-values [values]="item?.filterMetadata(fields)" [separator]="separator" [label]="label"></ds-metadata-uri-values> <ds-metadata-uri-values [values]="item?.allMetadata(fields)" [separator]="separator" [label]="label"></ds-metadata-uri-values>
</div> </div>

View File

@@ -1,5 +1,5 @@
import { autoserialize } from 'cerialize'; import { autoserialize } from 'cerialize';
import { Metadatum } from '../core/shared/metadatum.model'; import { MetadataMap } from '../core/shared/metadata.interfaces';
import { ListableObject } from '../shared/object-collection/shared/listable-object.model'; import { ListableObject } from '../shared/object-collection/shared/listable-object.model';
/** /**
@@ -16,6 +16,6 @@ export class NormalizedSearchResult implements ListableObject {
* The metadata that was used to find this item, hithighlighted * The metadata that was used to find this item, hithighlighted
*/ */
@autoserialize @autoserialize
hitHighlights: Metadatum[]; hitHighlights: MetadataMap;
} }

View File

@@ -1,5 +1,5 @@
import { DSpaceObject } from '../core/shared/dspace-object.model'; import { DSpaceObject } from '../core/shared/dspace-object.model';
import { Metadatum } from '../core/shared/metadatum.model'; import { MetadataMap } from '../core/shared/metadata.interfaces';
import { ListableObject } from '../shared/object-collection/shared/listable-object.model'; import { ListableObject } from '../shared/object-collection/shared/listable-object.model';
/** /**
@@ -14,6 +14,6 @@ export class SearchResult<T extends DSpaceObject> implements ListableObject {
/** /**
* The metadata that was used to find this item, hithighlighted * The metadata that was used to find this item, hithighlighted
*/ */
hitHighlights: Metadatum[]; hitHighlights: MetadataMap;
} }

View File

@@ -38,8 +38,8 @@ import { DSpaceObject } from '../shared/dspace-object.model';
export class BrowseService { export class BrowseService {
protected linkPath = 'browses'; protected linkPath = 'browses';
private static toSearchKeyArray(metadatumKey: string): string[] { private static toSearchKeyArray(metadataKey: string): string[] {
const keyParts = metadatumKey.split('.'); const keyParts = metadataKey.split('.');
const searchFor = []; const searchFor = [];
searchFor.push('*'); searchFor.push('*');
for (let i = 0; i < keyParts.length - 1; i++) { for (let i = 0; i < keyParts.length - 1; i++) {
@@ -47,7 +47,7 @@ export class BrowseService {
const nextPart = [...prevParts, '*'].join('.'); const nextPart = [...prevParts, '*'].join('.');
searchFor.push(nextPart); searchFor.push(nextPart);
} }
searchFor.push(metadatumKey); searchFor.push(metadataKey);
return searchFor; return searchFor;
} }
@@ -179,8 +179,8 @@ export class BrowseService {
return this.rdb.toRemoteDataObservable(requestEntry$, payload$); return this.rdb.toRemoteDataObservable(requestEntry$, payload$);
} }
getBrowseURLFor(metadatumKey: string, linkPath: string): Observable<string> { getBrowseURLFor(metadataKey: string, linkPath: string): Observable<string> {
const searchKeyArray = BrowseService.toSearchKeyArray(metadatumKey); const searchKeyArray = BrowseService.toSearchKeyArray(metadataKey);
return this.getBrowseDefinitions().pipe( return this.getBrowseDefinitions().pipe(
getRemoteDataPayload(), getRemoteDataPayload(),
map((browseDefinitions: BrowseDefinition[]) => browseDefinitions map((browseDefinitions: BrowseDefinition[]) => browseDefinitions
@@ -191,7 +191,7 @@ export class BrowseService {
), ),
map((def: BrowseDefinition) => { map((def: BrowseDefinition) => {
if (isEmpty(def) || isEmpty(def._links) || isEmpty(def._links[linkPath])) { if (isEmpty(def) || isEmpty(def._links) || isEmpty(def._links[linkPath])) {
throw new Error(`A browse endpoint for ${linkPath} on ${metadatumKey} isn't configured`); throw new Error(`A browse endpoint for ${linkPath} on ${metadataKey} isn't configured`);
} else { } else {
return def._links[linkPath]; return def._links[linkPath];
} }

View File

@@ -1,7 +1,6 @@
import { autoserialize, autoserializeAs, deserialize, serialize } from 'cerialize'; import { autoserialize, autoserializeAs, deserialize, serialize } from 'cerialize';
import { DSpaceObject } from '../../shared/dspace-object.model'; import { DSpaceObject } from '../../shared/dspace-object.model';
import { MetadataMap } from '../../shared/metadata.interfaces';
import { Metadatum } from '../../shared/metadatum.model';
import { ResourceType } from '../../shared/resource-type'; import { ResourceType } from '../../shared/resource-type';
import { mapsTo } from '../builders/build-decorators'; import { mapsTo } from '../builders/build-decorators';
import { NormalizedObject } from './normalized-object.model'; import { NormalizedObject } from './normalized-object.model';
@@ -46,10 +45,10 @@ export class NormalizedDSpaceObject extends NormalizedObject {
type: ResourceType; type: ResourceType;
/** /**
* An array containing all metadata of this DSpaceObject * All metadata of this DSpaceObject
*/ */
@autoserializeAs(Metadatum) @autoserialize
metadata: Metadatum[]; metadata: MetadataMap;
/** /**
* An array of DSpaceObjects that are direct parents of this DSpaceObject * An array of DSpaceObjects that are direct parents of this DSpaceObject

View File

@@ -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 { DSpaceRESTv2Serializer } from '../dspace-rest-v2/dspace-rest-v2.serializer';
import { hasValue } from '../../shared/empty.util'; import { hasValue } from '../../shared/empty.util';
import { SearchQueryResponse } from '../../+search-page/search-service/search-query-response.model'; import { SearchQueryResponse } from '../../+search-page/search-service/search-query-response.model';
import { Metadatum } from '../shared/metadatum.model'; import { MetadataMap, MetadataValue } from '../shared/metadata.interfaces';
@Injectable() @Injectable()
export class SearchResponseParsingService implements ResponseParsingService { export class SearchResponseParsingService implements ResponseParsingService {
@@ -16,17 +16,17 @@ export class SearchResponseParsingService implements ResponseParsingService {
parse(request: RestRequest, data: DSpaceRESTV2Response): RestResponse { parse(request: RestRequest, data: DSpaceRESTV2Response): RestResponse {
const payload = data.payload._embedded.searchResult; const payload = data.payload._embedded.searchResult;
const hitHighlights = payload._embedded.objects const hitHighlights: MetadataMap[] = payload._embedded.objects
.map((object) => object.hitHighlights) .map((object) => object.hitHighlights)
.map((hhObject) => { .map((hhObject) => {
const mdMap: MetadataMap = {};
if (hhObject) { if (hhObject) {
return Object.keys(hhObject).map((key) => Object.assign(new Metadatum(), { for (const key of Object.keys(hhObject)) {
key: key, const value: MetadataValue = { value: hhObject[key].join('...'), language: null };
value: hhObject[key].join('...') mdMap[key] = [ value ];
})) }
} else {
return [];
} }
return mdMap;
}); });
const dsoSelfLinks = payload._embedded.objects const dsoSelfLinks = payload._embedded.objects

View File

@@ -91,9 +91,9 @@ export class DSpaceRESTv2Service {
const form: FormData = new FormData(); const form: FormData = new FormData();
form.append('name', dso.name); form.append('name', dso.name);
if (dso.metadata) { if (dso.metadata) {
for (const i of Object.keys(dso.metadata)) { for (const key of Object.keys(dso.metadata)) {
if (isNotEmpty(dso.metadata[i].value)) { for (const value of dso.allMetadataValues(key)) {
form.append(dso.metadata[i].key, dso.metadata[i].value); form.append(key, value);
} }
} }
} }

View File

@@ -294,6 +294,10 @@ export class MetadataService {
} }
} }
private hasType(value: string): boolean {
return this.currentObject.value.hasMetadata('dc.type', { value: value, ignoreCase: true });
}
/** /**
* Returns true if this._item is a dissertation * Returns true if this._item is a dissertation
* *
@@ -301,14 +305,7 @@ export class MetadataService {
* true if this._item has a dc.type equal to 'Thesis' * true if this._item has a dc.type equal to 'Thesis'
*/ */
private isDissertation(): boolean { private isDissertation(): boolean {
let isDissertation = false; return this.hasType('thesis');
for (const metadatum of this.currentObject.value.metadata) {
if (metadatum.key === 'dc.type') {
isDissertation = metadatum.value.toLowerCase() === 'thesis';
break;
}
}
return isDissertation;
} }
/** /**
@@ -318,40 +315,15 @@ export class MetadataService {
* true if this._item has a dc.type equal to 'Technical Report' * true if this._item has a dc.type equal to 'Technical Report'
*/ */
private isTechReport(): boolean { private isTechReport(): boolean {
let isTechReport = false; return this.hasType('technical report');
for (const metadatum of this.currentObject.value.metadata) {
if (metadatum.key === 'dc.type') {
isTechReport = metadatum.value.toLowerCase() === 'technical report';
break;
}
}
return isTechReport;
} }
private getMetaTagValue(key: string): string { private getMetaTagValue(key: string): string {
let value: string; return this.currentObject.value.firstMetadataValue(key);
for (const metadatum of this.currentObject.value.metadata) {
if (metadatum.key === key) {
value = metadatum.value;
}
}
return value;
} }
private getFirstMetaTagValue(keys: string[]): string { private getFirstMetaTagValue(keys: string[]): string {
let value: string; return this.currentObject.value.firstMetadataValue(keys);
for (const metadatum of this.currentObject.value.metadata) {
for (const key of keys) {
if (key === metadatum.key) {
value = metadatum.value;
break;
}
}
if (value !== undefined) {
break;
}
}
return value;
} }
private getMetaTagValuesAndCombine(key: string): string { private getMetaTagValuesAndCombine(key: string): string {
@@ -359,15 +331,7 @@ export class MetadataService {
} }
private getMetaTagValues(keys: string[]): string[] { private getMetaTagValues(keys: string[]): string[] {
const values: string[] = []; return this.currentObject.value.allMetadataValues(keys);
for (const metadatum of this.currentObject.value.metadata) {
for (const key of keys) {
if (key === metadatum.key) {
values.push(metadatum.value);
}
}
}
return values;
} }
private addMetaTag(property: string, content: string): void { private addMetaTag(property: string, content: string): void {

View File

@@ -16,7 +16,7 @@ export class Collection extends DSpaceObject {
* Corresponds to the metadata field dc.description * Corresponds to the metadata field dc.description
*/ */
get introductoryText(): string { get introductoryText(): string {
return this.findMetadata('dc.description'); return this.firstMetadataValue('dc.description');
} }
/** /**
@@ -24,7 +24,7 @@ export class Collection extends DSpaceObject {
* Corresponds to the metadata field dc.description.abstract * Corresponds to the metadata field dc.description.abstract
*/ */
get shortDescription(): string { get shortDescription(): string {
return this.findMetadata('dc.description.abstract'); return this.firstMetadataValue('dc.description.abstract');
} }
/** /**
@@ -32,7 +32,7 @@ export class Collection extends DSpaceObject {
* Corresponds to the metadata field dc.rights * Corresponds to the metadata field dc.rights
*/ */
get copyrightText(): string { get copyrightText(): string {
return this.findMetadata('dc.rights'); return this.firstMetadataValue('dc.rights');
} }
/** /**
@@ -40,7 +40,7 @@ export class Collection extends DSpaceObject {
* Corresponds to the metadata field dc.rights.license * Corresponds to the metadata field dc.rights.license
*/ */
get license(): string { get license(): string {
return this.findMetadata('dc.rights.license'); return this.firstMetadataValue('dc.rights.license');
} }
/** /**
@@ -48,7 +48,7 @@ export class Collection extends DSpaceObject {
* Corresponds to the metadata field dc.description.tableofcontents * Corresponds to the metadata field dc.description.tableofcontents
*/ */
get sidebarText(): string { get sidebarText(): string {
return this.findMetadata('dc.description.tableofcontents'); return this.firstMetadataValue('dc.description.tableofcontents');
} }
/** /**

View File

@@ -17,7 +17,7 @@ export class Community extends DSpaceObject {
* Corresponds to the metadata field dc.description * Corresponds to the metadata field dc.description
*/ */
get introductoryText(): string { get introductoryText(): string {
return this.findMetadata('dc.description'); return this.firstMetadataValue('dc.description');
} }
/** /**
@@ -25,7 +25,7 @@ export class Community extends DSpaceObject {
* Corresponds to the metadata field dc.description.abstract * Corresponds to the metadata field dc.description.abstract
*/ */
get shortDescription(): string { get shortDescription(): string {
return this.findMetadata('dc.description.abstract'); return this.firstMetadataValue('dc.description.abstract');
} }
/** /**
@@ -33,7 +33,7 @@ export class Community extends DSpaceObject {
* Corresponds to the metadata field dc.rights * Corresponds to the metadata field dc.rights
*/ */
get copyrightText(): string { get copyrightText(): string {
return this.findMetadata('dc.rights'); return this.firstMetadataValue('dc.rights');
} }
/** /**
@@ -41,7 +41,7 @@ export class Community extends DSpaceObject {
* Corresponds to the metadata field dc.description.tableofcontents * Corresponds to the metadata field dc.description.tableofcontents
*/ */
get sidebarText(): string { get sidebarText(): string {
return this.findMetadata('dc.description.tableofcontents'); return this.firstMetadataValue('dc.description.tableofcontents');
} }
/** /**

View File

@@ -1,5 +1,5 @@
import { Metadatum } from './metadatum.model' import { MetadataMap, MetadataValue, MetadataValueFilter } from './metadata.interfaces';
import { isEmpty, isNotEmpty } from '../../shared/empty.util'; import { Metadata } from './metadata.model';
import { CacheableObject } from '../cache/object-cache.reducer'; import { CacheableObject } from '../cache/object-cache.reducer';
import { RemoteData } from '../data/remote-data'; import { RemoteData } from '../data/remote-data';
import { ResourceType } from './resource-type'; import { ResourceType } from './resource-type';
@@ -35,14 +35,14 @@ export class DSpaceObject implements CacheableObject, ListableObject {
* The name for this DSpaceObject * The name for this DSpaceObject
*/ */
get name(): string { get name(): string {
return this.findMetadata('dc.title'); return this.firstMetadataValue('dc.title');
} }
/** /**
* An array containing all metadata of this DSpaceObject * All metadata of this DSpaceObject
*/ */
@autoserialize @autoserialize
metadata: Metadatum[] = []; metadata: MetadataMap;
/** /**
* An array of DSpaceObjects that are direct parents of this DSpaceObject * An array of DSpaceObjects that are direct parents of this DSpaceObject
@@ -54,42 +54,29 @@ export class DSpaceObject implements CacheableObject, ListableObject {
*/ */
owner: Observable<RemoteData<DSpaceObject>>; owner: Observable<RemoteData<DSpaceObject>>;
/** /** Gets all matching metadata in this DSpaceObject. See `Metadata.all` for more information. */
* Find a metadata field by key and language allMetadata(keyOrKeys: string | string[], valueFilter?: MetadataValueFilter): MetadataValue[] {
* return Metadata.all(this.metadata, keyOrKeys, valueFilter);
* This method returns the value of the first element
* in the metadata array that matches the provided
* key and language
*
* @param key
* @param language
* @return string
*/
findMetadata(key: string, language?: string): string {
const metadatum = this.metadata.find((m: Metadatum) => {
return m.key === key && (isEmpty(language) || m.language === language)
});
if (isNotEmpty(metadatum)) {
return metadatum.value;
} else {
return undefined;
}
} }
/** /** Like `allMetadata`, but only returns string values. */
* Find metadata by an array of keys allMetadataValues(keyOrKeys: string | string[], valueFilter?: MetadataValueFilter): string[] {
* return Metadata.allValues(this.metadata, keyOrKeys, valueFilter);
* This method returns the values of the element }
* in the metadata array that match the provided
* key(s) /** Gets the first matching metadata in this DSpaceObject, or `undefined`. */
* firstMetadata(keyOrKeys: string | string[], valueFilter?: MetadataValueFilter): MetadataValue {
* @param key(s) return Metadata.first(this.metadata, keyOrKeys, valueFilter);
* @return Array<Metadatum> }
*/
filterMetadata(keys: string[]): Metadatum[] { /** Like `firstMetadata`, but only returns a string value, or `undefined`. */
return this.metadata.filter((metadatum: Metadatum) => { firstMetadataValue(keyOrKeys: string | string[], valueFilter?: MetadataValueFilter): string {
return keys.some((key) => key === metadatum.key); return Metadata.firstValue(this.metadata, keyOrKeys, valueFilter);
}); }
/** Checks for matching metadata in this DSpaceObject. */
hasMetadata(keyOrKeys: string | string[], valueFilter?: MetadataValueFilter): boolean {
return Metadata.has(this.metadata, keyOrKeys, valueFilter);
} }
} }

View File

@@ -0,0 +1,30 @@
/** 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;
}

View File

@@ -0,0 +1,119 @@
import { isEmpty } from '../../shared/empty.util';
import { MetadataMap, MetadataValue, MetadataValueFilter } from './metadata.interfaces';
/**
* Static utility methods for working with DSpace metadata.
*/
export class Metadata {
/**
* Gets all matching metadata.
*
* @param {MetadataMap|MetadataMap[]} mapOrMaps The source map(s). Values will only be returned from one map --
* the first with at least one match.
* @param {string|string[]} keyOrKeys The metadata key(s) in scope. Wildcards are supported, so `'*'` will
* match all keys, `'dc.date.*'` will match all qualified dc dates, and so on. Exact keys will be evaluated
* (and matches returned) in the order they are given in the parameter. When multiple keys match a wildcard,
* they are evaluated in the order they are stored in the map (alphanumeric if obtained from the REST api).
* If duplicate or overlapping keys are specified, the first one takes precedence. For example, specifying
* `['dc.date', 'dc.*', '*']` will cause any `dc.date` values to be evaluated (and returned, if matched)
* first, followed by any other `dc` metadata values, followed by any other (non-dc) metadata values.
* @param {MetadataValueFilter} filter The value filter to use.
* @returns {MetadataValue[]} the matching values or an empty array.
*/
public static all(mapOrMaps: MetadataMap | MetadataMap[], keyOrKeys: string | string[],
filter?: MetadataValueFilter): MetadataValue[] {
const mdMaps: MetadataMap[] = mapOrMaps instanceof Array ? mapOrMaps : [ mapOrMaps ];
const matches: MetadataValue[] = [];
for (const mdMap of mdMaps) {
for (const mdKey of Metadata.resolveKeys(mdMap, keyOrKeys)) {
const candidates = mdMap[mdKey];
if (candidates) {
for (const candidate of candidates) {
if (Metadata.valueMatches(candidate, filter)) {
matches.push(candidate);
}
}
}
}
if (!isEmpty(matches)) {
return matches;
}
}
return matches;
}
/** Like `all`, but only returns string values. */
public static allValues(mapOrMaps: MetadataMap | MetadataMap[], keyOrKeys: string | string[],
filter?: MetadataValueFilter): string[] {
return Metadata.all(mapOrMaps, keyOrKeys, filter).map((mdValue) => mdValue.value);
}
/** Gets the first matching MetadataValue object in a map, or `undefined`. */
public static first(mdMapOrMaps: MetadataMap | MetadataMap[], keyOrKeys: string | string[],
filter?: MetadataValueFilter): MetadataValue {
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];
if (values) {
return values.find((value: MetadataValue) => Metadata.valueMatches(value, filter));
}
}
}
}
/** Like `first`, but only returns a string value, or `undefined`. */
public static firstValue(mdMapOrMaps: MetadataMap | MetadataMap[], keyOrKeys: string | string[],
filter?: MetadataValueFilter): string {
const value = Metadata.first(mdMapOrMaps, keyOrKeys, filter);
return value === undefined ? undefined : value.value;
}
/** Checks the given map for a matching value. */
public static has(mdMapOrMaps: MetadataMap | MetadataMap[], keyOrKeys: string | string[],
filter?: MetadataValueFilter): boolean {
return Metadata.first(mdMapOrMaps, keyOrKeys, filter) !== undefined;
}
/** Checks if a value matches a filter. */
public static valueMatches(mdValue: MetadataValue, filter: MetadataValueFilter) {
if (!filter) {
return true;
} else if (filter.language && filter.language !== mdValue.language) {
return false;
} else if (filter.value) {
let fValue = filter.value;
let mValue = mdValue.value;
if (filter.ignoreCase) {
fValue = filter.value.toLowerCase();
mValue = mdValue.value.toLowerCase();
}
if (filter.substring) {
return mValue.includes(fValue);
} else {
return mValue === fValue;
}
}
return true;
}
/** Gets the list of keys in the map limited by, and in the order given by `keyOrKeys` */
private static resolveKeys(mdMap: MetadataMap, keyOrKeys: string | string[]): string[] {
const inputKeys: string[] = keyOrKeys instanceof Array ? keyOrKeys : [ keyOrKeys ];
const outputKeys: string[] = [];
for (const inputKey of inputKeys) {
if (inputKey.includes('*')) {
const inputKeyRegex = new RegExp('^' + inputKey.replace('.', '\.').replace('*', '.*') + '$');
for (const mapKey of Object.keys(mdMap)) {
if (!outputKeys.includes(mapKey) && inputKeyRegex.test(mapKey)) {
outputKeys.push(mapKey);
}
}
} else if (mdMap.hasOwnProperty(inputKey) && !outputKeys.includes(inputKey)) {
outputKeys.push(inputKey);
}
}
return outputKeys;
}
}

View File

@@ -1,23 +0,0 @@
import { autoserialize } from 'cerialize';
export class Metadatum {
/**
* The metadata field of this Metadatum
*/
@autoserialize
key: string;
/**
* The language of this Metadatum
*/
@autoserialize
language: string;
/**
* The value of this Metadatum
*/
@autoserialize
value: string;
}

View File

@@ -8,6 +8,7 @@ import { FormGroup } from '@angular/forms';
import { DynamicFormControlModel } from '@ng-dynamic-forms/core/src/model/dynamic-form-control.model'; import { DynamicFormControlModel } from '@ng-dynamic-forms/core/src/model/dynamic-form-control.model';
import { TranslateService } from '@ngx-translate/core'; import { TranslateService } from '@ngx-translate/core';
import { DSpaceObject } from '../../../core/shared/dspace-object.model'; import { DSpaceObject } from '../../../core/shared/dspace-object.model';
import { MetadataMap, MetadataValue } from '../../../core/shared/metadata.interfaces';
import { isNotEmpty } from '../../empty.util'; import { isNotEmpty } from '../../empty.util';
import { ResourceType } from '../../../core/shared/resource-type'; import { ResourceType } from '../../../core/shared/resource-type';
@@ -64,7 +65,7 @@ export class ComColFormComponent<T extends DSpaceObject> implements OnInit {
ngOnInit(): void { ngOnInit(): void {
this.formModel.forEach( this.formModel.forEach(
(fieldModel: DynamicInputModel) => { (fieldModel: DynamicInputModel) => {
fieldModel.value = this.dso.findMetadata(fieldModel.name); fieldModel.value = this.dso.firstMetadataValue(fieldModel.name);
} }
); );
this.formGroup = this.formService.createFormGroup(this.formModel); this.formGroup = this.formService.createFormGroup(this.formModel);
@@ -77,20 +78,24 @@ export class ComColFormComponent<T extends DSpaceObject> implements OnInit {
} }
/** /**
* Checks which new fields where added and sends the updated version of the DSO to the parent component * Checks which new fields were added and sends the updated version of the DSO to the parent component
*/ */
onSubmit() { onSubmit() {
const metadata = this.formModel.map( const formMetadata = new Object() as MetadataMap;
(fieldModel: DynamicInputModel) => { this.formModel.forEach((fieldModel: DynamicInputModel) => {
return { key: fieldModel.name, value: fieldModel.value } const value: MetadataValue = { value: fieldModel.value as string, language: null };
if (formMetadata.hasOwnProperty(fieldModel.name)) {
formMetadata[fieldModel.name].push(value);
} else {
formMetadata[fieldModel.name] = [ value ];
} }
); });
const filteredOldMetadata = this.dso.metadata.filter((filter) => !metadata.map((md) => md.key).includes(filter.key));
const filteredNewMetadata = metadata.filter((md) => isNotEmpty(md.value));
const newMetadata = [...filteredOldMetadata, ...filteredNewMetadata];
const updatedDSO = Object.assign({}, this.dso, { const updatedDSO = Object.assign({}, this.dso, {
metadata: newMetadata, metadata: {
...this.dso.metadata,
...formMetadata
},
type: ResourceType.Community type: ResourceType.Community
}); });
this.submitForm.emit(updatedDSO); this.submitForm.emit(updatedDSO);

View File

@@ -1,6 +1,5 @@
import { Component, Inject } from '@angular/core'; import { Component, Inject } from '@angular/core';
import { ListableObject } from '../listable-object.model'; import { ListableObject } from '../listable-object.model';
import { hasValue } from '../../../empty.util';
@Component({ @Component({
selector: 'ds-abstract-object-element', selector: 'ds-abstract-object-element',
@@ -11,8 +10,4 @@ export class AbstractListableElementComponent <T extends ListableObject> {
public constructor(@Inject('objectElementProvider') public listableObject: ListableObject) { public constructor(@Inject('objectElementProvider') public listableObject: ListableObject) {
this.object = listableObject as T; this.object = listableObject as T;
} }
hasValue(data) {
return hasValue(data);
}
} }

View File

@@ -5,19 +5,19 @@
</ds-grid-thumbnail> </ds-grid-thumbnail>
</a> </a>
<div class="card-body"> <div class="card-body">
<h4 class="card-title">{{object.findMetadata('dc.title')}}</h4> <h4 class="card-title">{{object.firstMetadataValue('dc.title')}}</h4>
<ds-truncatable-part [id]="object.id" [minLines]="2"> <ds-truncatable-part [id]="object.id" [minLines]="2">
<p *ngIf="object.filterMetadata(['dc.contributor.author', 'dc.creator', 'dc.contributor.*']).length > 0" class="item-authors card-text text-muted"> <p *ngIf="object.hasMetadata(['dc.contributor.author', 'dc.creator', 'dc.contributor.*'])" class="item-authors card-text text-muted">
<span *ngFor="let authorMd of object.filterMetadata(['dc.contributor.author', 'dc.creator', 'dc.contributor.*']); let last=last;">{{authorMd.value}} <span *ngFor="let author of object.allMetadataValues(['dc.contributor.author', 'dc.creator', 'dc.contributor.*']); let last=last;">{{author}}
<span *ngIf="!last">; </span> <span *ngIf="!last">; </span>
</span> </span>
<span *ngIf="hasValue(object.findMetadata('dc.date.issued'))" class="item-date">{{object.findMetadata("dc.date.issued")}}</span> <span *ngIf="object.hasMetadata('dc.date.issued')" class="item-date">{{object.firstMetadataValue("dc.date.issued")}}</span>
</p> </p>
</ds-truncatable-part> </ds-truncatable-part>
<ds-truncatable-part [id]="object.id" [minLines]="5"> <ds-truncatable-part [id]="object.id" [minLines]="5">
<p *ngIf="object.findMetadata('dc.description.abstract')" class="item-abstract card-text">{{object.findMetadata("dc.description.abstract") }}</p> <p *ngIf="object.hasMetadata('dc.description.abstract')" class="item-abstract card-text">{{object.firstMetadataValue("dc.description.abstract")}}</p>
</ds-truncatable-part> </ds-truncatable-part>
<div class="text-center pt-2"> <div class="text-center pt-2">

View File

@@ -8,20 +8,20 @@
</a> </a>
<div class="card-body"> <div class="card-body">
<ds-truncatable-part [fixedHeight]="true" [id]="dso.id" [minLines]="3" type="h4"> <ds-truncatable-part [fixedHeight]="true" [id]="dso.id" [minLines]="3" type="h4">
<h4 class="card-title" [innerHTML]="dso.findMetadata('dc.title')"></h4> <h4 class="card-title" [innerHTML]="dso.firstMetadataValue('dc.title')"></h4>
</ds-truncatable-part> </ds-truncatable-part>
<p *ngIf="dso.filterMetadata(['dc.contributor.author', 'dc.creator', 'dc.contributor.*']).length > 0" <p *ngIf="dso.hasMetadata(['dc.contributor.author', 'dc.creator', 'dc.contributor.*'])"
class="item-authors card-text text-muted"> class="item-authors card-text text-muted">
<ds-truncatable-part [fixedHeight]="true" [id]="dso.id" [minLines]="1"> <ds-truncatable-part [fixedHeight]="true" [id]="dso.id" [minLines]="1">
<span *ngIf="hasValue(dso.findMetadata('dc.date.issued'))" class="item-date">{{dso.findMetadata("dc.date.issued")}}</span> <span *ngIf="dso.hasMetadata('dc.date.issued')" class="item-date">{{dso.firstMetadataValue('dc.date.issued')}}</span>
<span *ngFor="let authorMd of dso.filterMetadata(['dc.contributor.author', 'dc.creator', 'dc.contributor.*']);">, <span *ngFor="let author of dso.allMetadataValues(['dc.contributor.author', 'dc.creator', 'dc.contributor.*']);">,
<span [innerHTML]="authorMd.value"></span> <span [innerHTML]="author"></span>
</span> </span>
</ds-truncatable-part> </ds-truncatable-part>
</p> </p>
<p class="item-abstract card-text"> <p class="item-abstract card-text">
<ds-truncatable-part [fixedHeight]="true" [id]="dso.id" [minLines]="3"> <ds-truncatable-part [fixedHeight]="true" [id]="dso.id" [minLines]="3">
<span [innerHTML]="getFirstValue('dc.description.abstract')"></span> <span [innerHTML]="firstMetadataValue('dc.description.abstract')"></span>
</ds-truncatable-part> </ds-truncatable-part>
</p> </p>
<div class="text-center"> <div class="text-center">

View File

@@ -2,12 +2,11 @@ import { Component, Inject } from '@angular/core';
import { SearchResult } from '../../../+search-page/search-result.model'; import { SearchResult } from '../../../+search-page/search-result.model';
import { DSpaceObject } from '../../../core/shared/dspace-object.model'; import { DSpaceObject } from '../../../core/shared/dspace-object.model';
import { Metadatum } from '../../../core/shared/metadatum.model';
import { isEmpty, hasNoValue, hasValue } from '../../empty.util';
import { AbstractListableElementComponent } from '../../object-collection/shared/object-collection-element/abstract-listable-element.component'; import { AbstractListableElementComponent } from '../../object-collection/shared/object-collection-element/abstract-listable-element.component';
import { ListableObject } from '../../object-collection/shared/listable-object.model'; import { ListableObject } from '../../object-collection/shared/listable-object.model';
import { TruncatableService } from '../../truncatable/truncatable.service'; import { TruncatableService } from '../../truncatable/truncatable.service';
import { Observable } from 'rxjs'; import { Observable } from 'rxjs';
import { Metadata } from '../../../core/shared/metadata.model';
@Component({ @Component({
selector: 'ds-search-result-grid-element', selector: 'ds-search-result-grid-element',
@@ -22,39 +21,14 @@ export class SearchResultGridElementComponent<T extends SearchResult<K>, K exten
this.dso = this.object.dspaceObject; this.dso = this.object.dspaceObject;
} }
getValues(keys: string[]): string[] { /** Gets all matching metadata string values from hitHighlights or dso metadata, preferring hitHighlights. */
const results: string[] = new Array<string>(); allMetadataValues(keyOrKeys: string | string[]): string[] {
this.object.hitHighlights.forEach( return Metadata.allValues([this.object.hitHighlights, this.dso.metadata], keyOrKeys);
(md: Metadatum) => {
if (keys.indexOf(md.key) > -1) {
results.push(md.value);
}
}
);
if (isEmpty(results)) {
this.dso.filterMetadata(keys).forEach(
(md: Metadatum) => {
results.push(md.value);
}
);
}
return results;
} }
getFirstValue(key: string): string { /** Gets the first matching metadata string value from hitHighlights or dso metadata, preferring hitHighlights. */
let result: string; firstMetadataValue(keyOrKeys: string | string[]): string {
this.object.hitHighlights.some( return Metadata.firstValue([this.object.hitHighlights, this.dso.metadata], keyOrKeys);
(md: Metadatum) => {
if (key === md.key) {
result = md.value;
return true;
}
}
);
if (hasNoValue(result)) {
result = this.dso.findMetadata(key);
}
return result;
} }
isCollapsed(): Observable<boolean> { isCollapsed(): Observable<boolean> {

View File

@@ -1,23 +1,23 @@
<ds-truncatable [id]="object.id"> <ds-truncatable [id]="object.id">
<a [routerLink]="['/items/' + object.id]" class="lead"> <a [routerLink]="['/items/' + object.id]" class="lead">
{{object.findMetadata("dc.title")}} {{object.firstMetadataValue("dc.title")}}
</a> </a>
<div> <div>
<ds-truncatable-part [id]="object.id" [minLines]="1"> <ds-truncatable-part [id]="object.id" [minLines]="1">
<span class="text-muted"> <span class="text-muted">
<span *ngIf="object.filterMetadata(['dc.contributor.author', 'dc.creator', 'dc.contributor.*']).length > 0" <span *ngIf="object.hasMetadata(['dc.contributor.author', 'dc.creator', 'dc.contributor.*'])"
class="item-list-authors"> class="item-list-authors">
<span *ngFor="let authorMd of object.filterMetadata(['dc.contributor.author', 'dc.creator', 'dc.contributor.*']); let last=last;">{{authorMd.value}} <span *ngFor="let author of object.allMetadataValues(['dc.contributor.author', 'dc.creator', 'dc.contributor.*']); let last=last;">{{author}}
<span *ngIf="!last">; </span> <span *ngIf="!last">; </span>
</span> </span>
</span> </span>
(<span *ngIf="hasValue(object.findMetadata('dc.publisher'))" class="item-list-publisher">{{object.findMetadata("dc.publisher")}}, </span><span (<span *ngIf="object.hasMetadata('dc.publisher')" class="item-list-publisher">{{object.firstMetadataValue("dc.publisher")}}, </span><span
*ngIf="hasValue(object.findMetadata('dc.date.issued'))" class="item-list-date">{{object.findMetadata("dc.date.issued")}}</span>) *ngIf="object.hasMetadata('dc.date.issued')" class="item-list-date">{{object.firstMetadataValue("dc.date.issued")}}</span>)
</span> </span>
</ds-truncatable-part> </ds-truncatable-part>
<ds-truncatable-part [id]="object.id" [minLines]="3"> <ds-truncatable-part [id]="object.id" [minLines]="3">
<div *ngIf="object.findMetadata('dc.description.abstract')" class="item-list-abstract"> <div *ngIf="object.hasMetadata('dc.description.abstract')" class="item-list-abstract">
{{object.findMetadata("dc.description.abstract")}} {{object.firstMetadataValue("dc.description.abstract")}}
</div> </div>
</ds-truncatable-part> </ds-truncatable-part>
</div> </div>

View File

@@ -1,2 +1,2 @@
<a [routerLink]="['/collections/' + dso.id]" class="lead" [innerHTML]="getFirstValue('dc.title')"></a> <a [routerLink]="['/collections/' + dso.id]" class="lead" [innerHTML]="firstMetadataValue('dc.title')"></a>
<div *ngIf="dso.shortDescription" class="text-muted abstract-text" [innerHTML]="getFirstValue('dc.description.abstract')"></div> <div *ngIf="dso.shortDescription" class="text-muted abstract-text" [innerHTML]="firstMetadataValue('dc.description.abstract')"></div>

View File

@@ -1,2 +1,2 @@
<a [routerLink]="['/communities/' + dso.id]" class="lead" [innerHTML]="getFirstValue('dc.title')"></a> <a [routerLink]="['/communities/' + dso.id]" class="lead" [innerHTML]="firstMetadataValue('dc.title')"></a>
<div *ngIf="dso.shortDescription" class="text-muted abstract-text" [innerHTML]="getFirstValue('dc.description.abstract')"></div> <div *ngIf="dso.shortDescription" class="text-muted abstract-text" [innerHTML]="firstMetadataValue('dc.description.abstract')"></div>

View File

@@ -1,24 +1,24 @@
<ds-truncatable [id]="dso.id"> <ds-truncatable [id]="dso.id">
<a <a
[routerLink]="['/items/' + dso.id]" class="lead" [routerLink]="['/items/' + dso.id]" class="lead"
[innerHTML]="getFirstValue('dc.title')"></a> [innerHTML]="firstMetadataValue('dc.title')"></a>
<span class="text-muted"> <span class="text-muted">
<ds-truncatable-part [id]="dso.id" [minLines]="1"> <ds-truncatable-part [id]="dso.id" [minLines]="1">
(<span *ngIf="dso.findMetadata('dc.publisher')" class="item-list-publisher" (<span *ngIf="dso.hasMetadata('dc.publisher')" class="item-list-publisher"
[innerHTML]="getFirstValue('dc.publisher') + ', '"></span><span [innerHTML]="firstMetadataValue('dc.publisher') + ', '"></span><span
*ngIf="hasValue(dso.findMetadata('dc.date.issued'))" class="item-list-date" *ngIf="dso.hasMetadata('dc.date.issued')" class="item-list-date"
[innerHTML]="getFirstValue('dc.date.issued')"></span>) [innerHTML]="firstMetadataValue('dc.date.issued')"></span>)
<span *ngIf="dso.filterMetadata(['dc.contributor.author', 'dc.creator', 'dc.contributor.*']).length > 0" <span *ngIf="dso.hasMetadata(['dc.contributor.author', 'dc.creator', 'dc.contributor.*'])"
class="item-list-authors"> class="item-list-authors">
<span *ngFor="let author of getValues(['dc.contributor.author', 'dc.creator', 'dc.contributor.*']); let last=last;"> <span *ngFor="let author of allMetadataValues(['dc.contributor.author', 'dc.creator', 'dc.contributor.*']); let last=last;">
<span [innerHTML]="author"><span [innerHTML]="author"></span></span> <span [innerHTML]="author"><span [innerHTML]="author"></span></span>
</span> </span>
</span> </span>
</ds-truncatable-part> </ds-truncatable-part>
</span> </span>
<div *ngIf="dso.findMetadata('dc.description.abstract')" class="item-list-abstract"> <div *ngIf="dso.hasMetadata('dc.description.abstract')" class="item-list-abstract">
<ds-truncatable-part [id]="dso.id" [minLines]="3"><span <ds-truncatable-part [id]="dso.id" [minLines]="3"><span
[innerHTML]="getFirstValue('dc.description.abstract')"></span> [innerHTML]="firstMetadataValue('dc.description.abstract')"></span>
</ds-truncatable-part> </ds-truncatable-part>
</div> </div>
</ds-truncatable> </ds-truncatable>

View File

@@ -3,11 +3,10 @@ import { Observable } from 'rxjs';
import { SearchResult } from '../../../+search-page/search-result.model'; import { SearchResult } from '../../../+search-page/search-result.model';
import { DSpaceObject } from '../../../core/shared/dspace-object.model'; import { DSpaceObject } from '../../../core/shared/dspace-object.model';
import { Metadatum } from '../../../core/shared/metadatum.model';
import { hasNoValue, isEmpty } from '../../empty.util';
import { ListableObject } from '../../object-collection/shared/listable-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 { AbstractListableElementComponent } from '../../object-collection/shared/object-collection-element/abstract-listable-element.component';
import { TruncatableService } from '../../truncatable/truncatable.service'; import { TruncatableService } from '../../truncatable/truncatable.service';
import { Metadata } from '../../../core/shared/metadata.model';
@Component({ @Component({
selector: 'ds-search-result-list-element', selector: 'ds-search-result-list-element',
@@ -22,39 +21,14 @@ export class SearchResultListElementComponent<T extends SearchResult<K>, K exten
this.dso = this.object.dspaceObject; this.dso = this.object.dspaceObject;
} }
getValues(keys: string[]): string[] { /** Gets all matching metadata string values from hitHighlights or dso metadata, preferring hitHighlights. */
const results: string[] = new Array<string>(); allMetadataValues(keyOrKeys: string | string[]): string[] {
this.object.hitHighlights.forEach( return Metadata.allValues([this.object.hitHighlights, this.dso.metadata], keyOrKeys);
(md: Metadatum) => {
if (keys.indexOf(md.key) > -1) {
results.push(md.value);
}
}
);
if (isEmpty(results)) {
this.dso.filterMetadata(keys).forEach(
(md: Metadatum) => {
results.push(md.value);
}
);
}
return results;
} }
getFirstValue(key: string): string { /** Gets the first matching metadata string value from hitHighlights or dso metadata, preferring hitHighlights. */
let result: string; firstMetadataValue(keyOrKeys: string | string[]): string {
this.object.hitHighlights.some( return Metadata.firstValue([this.object.hitHighlights, this.dso.metadata], keyOrKeys);
(md: Metadatum) => {
if (key === md.key) {
result = md.value;
return true;
}
}
);
if (hasNoValue(result)) {
result = this.dso.findMetadata(key);
}
return result;
} }
isCollapsed(): Observable<boolean> { isCollapsed(): Observable<boolean> {