Merge pull request #347 from atmire/DS-4107_Metadata_as_map

DS-4107 Metadata as map updates for Angular
This commit is contained in:
Tim Donohue
2019-02-21 14:22:37 -06:00
committed by GitHub
61 changed files with 1222 additions and 826 deletions

View File

@@ -18,42 +18,38 @@ describe('SubCommunityList Component', () => {
const subcommunities = [Object.assign(new Community(), {
id: '123456789-1',
metadata: [
{
key: 'dc.title',
language: 'en_US',
value: 'SubCommunity 1'
}]
metadata: {
'dc.title': [
{ language: 'en_US', value: 'SubCommunity 1' }
]
}
}),
Object.assign(new Community(), {
id: '123456789-2',
metadata: [
{
key: 'dc.title',
language: 'en_US',
value: 'SubCommunity 2'
}]
metadata: {
'dc.title': [
{ language: 'en_US', value: 'SubCommunity 2' }
]
}
})
];
const emptySubCommunitiesCommunity = Object.assign(new Community(), {
metadata: [
{
key: 'dc.title',
language: 'en_US',
value: 'Test title'
}],
metadata: {
'dc.title': [
{ language: 'en_US', value: 'Test title' }
]
},
subcommunities: observableOf(new RemoteData(true, true, true,
undefined, new PaginatedList(new PageInfo(), [])))
});
const mockCommunity = Object.assign(new Community(), {
metadata: [
{
key: 'dc.title',
language: 'en_US',
value: 'Test title'
}],
metadata: {
'dc.title': [
{ language: 'en_US', value: 'Test title' }
]
},
subcommunities: observableOf(new RemoteData(true, true, true,
undefined, new PaginatedList(new PageInfo(), subcommunities)))
})

View File

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

View File

@@ -11,10 +11,14 @@ const mockItem = Object.assign(new Item(), {
id: 'fake-id',
handle: 'fake/handle',
lastModified: '2018',
metadata: [
{key: 'dc.title', value: 'Mock item title', language: 'en'},
{key: 'dc.contributor.author', value: 'Mayer, Ed', language: ''}
metadata: {
'dc.title': [
{ value: 'Mock item title', language: 'en' }
],
'dc.contributor.author': [
{ value: 'Mayer, Ed', language: '' }
]
}
});
describe('ModifyItemOverviewComponent', () => {
@@ -37,19 +41,19 @@ describe('ModifyItemOverviewComponent', () => {
const metadataRows = fixture.debugElement.queryAll(By.css('tr.metadata-row'));
expect(metadataRows.length).toEqual(2);
const titleRow = metadataRows[0].queryAll(By.css('td'));
expect(titleRow.length).toEqual(3);
expect(titleRow[0].nativeElement.innerHTML).toContain('dc.title');
expect(titleRow[1].nativeElement.innerHTML).toContain('Mock item title');
expect(titleRow[2].nativeElement.innerHTML).toContain('en');
const authorRow = metadataRows[1].queryAll(By.css('td'));
const authorRow = metadataRows[0].queryAll(By.css('td'));
expect(authorRow.length).toEqual(3);
expect(authorRow[0].nativeElement.innerHTML).toContain('dc.contributor.author');
expect(authorRow[1].nativeElement.innerHTML).toContain('Mayer, Ed');
expect(authorRow[2].nativeElement.innerHTML).toEqual('');
const titleRow = metadataRows[1].queryAll(By.css('td'));
expect(titleRow.length).toEqual(3);
expect(titleRow[0].nativeElement.innerHTML).toContain('dc.title');
expect(titleRow[1].nativeElement.innerHTML).toContain('Mock item title');
expect(titleRow[2].nativeElement.innerHTML).toContain('en');
});
});

View File

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

View File

@@ -14,12 +14,14 @@ let collectionsComponent: CollectionsComponent;
let fixture: ComponentFixture<CollectionsComponent>;
const mockCollection1: Collection = Object.assign(new Collection(), {
metadata: [
metadata: {
'dc.description.abstract': [
{
key: 'dc.description.abstract',
language: 'en_US',
value: 'Short description'
}]
}
]
}
});
const succeededMockItem: Item = Object.assign(new Item(), {owningCollection: observableOf(new RemoteData(false, false, true, null, mockCollection1))});

View File

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

View File

@@ -1,6 +1,7 @@
import { Component, Input } from '@angular/core';
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.
@@ -18,7 +19,7 @@ export class MetadataUriValuesComponent extends MetadataValuesComponent {
@Input() linktext: any;
@Input() values: any;
@Input() mdValues: MetadataValue[];
@Input() separator: string;

View File

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

View File

@@ -1,5 +1,5 @@
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.
@@ -12,7 +12,7 @@ import { Metadatum } from '../../../core/shared/metadatum.model';
})
export class MetadataValuesComponent {
@Input() values: Metadatum[];
@Input() mdValues: MetadataValue[];
@Input() separator: string;

View File

@@ -17,7 +17,7 @@
<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>
</div>
<div class="col-2">

View File

@@ -9,11 +9,13 @@
</div>
<table class="table table-responsive table-striped">
<tbody>
<tr *ngFor="let metadatum of (metadata$ | async)">
<td>{{metadatum.key}}</td>
<td>{{metadatum.value}}</td>
<td>{{metadatum.language}}</td>
<ng-container *ngFor="let mdEntry of (metadata$ | async) | keyvalue">
<tr *ngFor="let mdValue of mdEntry.value">
<td>{{mdEntry.key}}</td>
<td>{{mdValue.value}}</td>
<td>{{mdValue.language}}</td>
</tr>
</ng-container>
</tbody>
</table>
<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 { 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 { RemoteData } from '../../core/data/remote-data';
@@ -34,7 +34,7 @@ export class FullItemPageComponent extends ItemPageComponent implements OnInit {
itemRD$: Observable<RemoteData<Item>>;
metadata$: Observable<Metadatum[]>;
metadata$: Observable<MetadataMap>;
constructor(route: ActivatedRoute, items: ItemDataService, metadataService: MetadataService) {
super(route, items, metadataService);

View File

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

View File

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

View File

@@ -1,3 +1,3 @@
<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 [mdValues]="item?.allMetadata(fields)" [separator]="separator" [label]="label"></ds-metadata-uri-values>
</div>

View File

@@ -1,5 +1,5 @@
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';
/**
@@ -16,6 +16,6 @@ export class NormalizedSearchResult implements ListableObject {
* The metadata that was used to find this item, hithighlighted
*/
@autoserialize
hitHighlights: Metadatum[];
hitHighlights: MetadataMap;
}

View File

@@ -1,5 +1,5 @@
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';
/**
@@ -14,6 +14,6 @@ export class SearchResult<T extends DSpaceObject> implements ListableObject {
/**
* The metadata that was used to find this item, hithighlighted
*/
hitHighlights: Metadatum[];
hitHighlights: MetadataMap;
}

View File

@@ -111,33 +111,38 @@ export const objects = [
id: '7669c72a-3f2a-451f-a3b9-9210e7a4c02f',
uuid: '7669c72a-3f2a-451f-a3b9-9210e7a4c02f',
type: ResourceType.Community,
metadata: [
metadata: {
'dc.description': [
{
key: 'dc.description',
language: null,
value: ''
},
}
],
'dc.description.abstract': [
{
key: 'dc.description.abstract',
language: null,
value: 'This is a test community to hold content for the OR2017 demostration'
},
}
],
'dc.description.tableofcontents': [
{
key: 'dc.description.tableofcontents',
language: null,
value: ''
},
}
],
'dc.rights': [
{
key: 'dc.rights',
language: null,
value: ''
},
}
],
'dc.title': [
{
key: 'dc.title',
language: null,
value: 'OR2017 - Demonstration'
}
]
}
}),
Object.assign(new Community(),
{
@@ -160,33 +165,38 @@ export const objects = [
id: '9076bd16-e69a-48d6-9e41-0238cb40d863',
uuid: '9076bd16-e69a-48d6-9e41-0238cb40d863',
type: ResourceType.Community,
metadata: [
metadata: {
'dc.description': [
{
key: 'dc.description',
language: null,
value: '<p>This is the introductory text for the <em>Sample Community</em> on the DSpace Demonstration Site. It is editable by System or Community Administrators (of this Community).</p>\r\n<p><strong>DSpace Communities may contain one or more Sub-Communities or Collections (of Items).</strong></p>\r\n<p>This particular Community has its own logo (the <a href=\'http://www.duraspace.org/\'>DuraSpace</a> logo).</p>'
},
}
],
'dc.description.abstract': [
{
key: 'dc.description.abstract',
language: null,
value: 'This is a sample top-level community'
},
}
],
'dc.description.tableofcontents': [
{
key: 'dc.description.tableofcontents',
language: null,
value: '<p>This is the <em>news section</em> for this <em>Sample Community</em>. System or Community Administrators (of this Community) can edit this News field.</p>'
},
}
],
'dc.rights': [
{
key: 'dc.rights',
language: null,
value: '<p><em>If this Community had special copyright text to display, it would be displayed here.</em></p>'
},
}
],
'dc.title': [
{
key: 'dc.title',
language: null,
value: 'Sample Community'
}
]
}
}
)
];

View File

@@ -60,23 +60,26 @@ describe('AuthResponseParsingService', () => {
handle: null,
id: '4dc70ab5-cd73-492f-b007-3179d2d9296b',
lastActive: '2018-05-14T17:03:31.277+0000',
metadata: [
metadata: {
'eperson.firstname': [
{
key: 'eperson.firstname',
language: null,
value: 'User'
},
}
],
'eperson.lastname': [
{
key: 'eperson.lastname',
language: null,
value: 'Test'
},
}
],
'eperson.language': [
{
key: 'eperson.language',
language: null,
value: 'en'
}
],
]
},
name: 'User Test',
netid: 'myself@testshib.org',
requireCertificate: false,

View File

@@ -220,44 +220,44 @@ describe('BrowseService', () => {
}}));
});
it('should return the URL for the given metadatumKey and linkPath', () => {
const metadatumKey = 'dc.date.issued';
it('should return the URL for the given metadataKey and linkPath', () => {
const metadataKey = 'dc.date.issued';
const linkPath = 'items';
const expectedURL = browseDefinitions[0]._links[linkPath];
const result = service.getBrowseURLFor(metadatumKey, linkPath);
const result = service.getBrowseURLFor(metadataKey, linkPath);
const expected = cold('c-d-', { c: undefined, d: expectedURL });
expect(result).toBeObservable(expected);
});
it('should work when the definition uses a wildcard in the metadatumKey', () => {
const metadatumKey = 'dc.contributor.author'; // should match dc.contributor.* in the definition
it('should work when the definition uses a wildcard in the metadataKey', () => {
const metadataKey = 'dc.contributor.author'; // should match dc.contributor.* in the definition
const linkPath = 'items';
const expectedURL = browseDefinitions[1]._links[linkPath];
const result = service.getBrowseURLFor(metadatumKey, linkPath);
const result = service.getBrowseURLFor(metadataKey, linkPath);
const expected = cold('c-d-', { c: undefined, d: expectedURL });
expect(result).toBeObservable(expected);
});
it('should throw an error when the key doesn\'t match', () => {
const metadatumKey = 'dc.title'; // isn't in the definitions
const metadataKey = 'dc.title'; // isn't in the definitions
const linkPath = 'items';
const result = service.getBrowseURLFor(metadatumKey, linkPath);
const expected = cold('c-#-', { c: undefined }, new Error(`A browse endpoint for ${linkPath} on ${metadatumKey} isn't configured`));
const result = service.getBrowseURLFor(metadataKey, linkPath);
const expected = cold('c-#-', { c: undefined }, new Error(`A browse endpoint for ${linkPath} on ${metadataKey} isn't configured`));
expect(result).toBeObservable(expected);
});
it('should throw an error when the link doesn\'t match', () => {
const metadatumKey = 'dc.date.issued';
const metadataKey = 'dc.date.issued';
const linkPath = 'collections'; // isn't in the definitions
const result = service.getBrowseURLFor(metadatumKey, linkPath);
const expected = cold('c-#-', { c: undefined }, new Error(`A browse endpoint for ${linkPath} on ${metadatumKey} isn't configured`));
const result = service.getBrowseURLFor(metadataKey, linkPath);
const expected = cold('c-#-', { c: undefined }, new Error(`A browse endpoint for ${linkPath} on ${metadataKey} isn't configured`));
expect(result).toBeObservable(expected);
});
@@ -272,10 +272,10 @@ describe('BrowseService', () => {
spyOn(service, 'getBrowseDefinitions').and
.returnValue(hot('----'));
const metadatumKey = 'dc.date.issued';
const metadataKey = 'dc.date.issued';
const linkPath = 'items';
const result = service.getBrowseURLFor(metadatumKey, linkPath);
const result = service.getBrowseURLFor(metadataKey, linkPath);
const expected = cold('b---', { b: undefined });
expect(result).toBeObservable(expected);
});

View File

@@ -35,12 +35,15 @@ import { Item } from '../shared/item.model';
import { DSpaceObject } from '../shared/dspace-object.model';
import { BrowseEntrySearchOptions } from './browse-entry-search-options.model';
/**
* Service that performs all actions that have to do with browse.
*/
@Injectable()
export class BrowseService {
protected linkPath = 'browses';
private static toSearchKeyArray(metadatumKey: string): string[] {
const keyParts = metadatumKey.split('.');
private static toSearchKeyArray(metadataKey: string): string[] {
const keyParts = metadataKey.split('.');
const searchFor = [];
searchFor.push('*');
for (let i = 0; i < keyParts.length - 1; i++) {
@@ -48,7 +51,7 @@ export class BrowseService {
const nextPart = [...prevParts, '*'].join('.');
searchFor.push(nextPart);
}
searchFor.push(metadatumKey);
searchFor.push(metadataKey);
return searchFor;
}
@@ -180,8 +183,8 @@ export class BrowseService {
return this.rdb.toRemoteDataObservable(requestEntry$, payload$);
}
getBrowseURLFor(metadatumKey: string, linkPath: string): Observable<string> {
const searchKeyArray = BrowseService.toSearchKeyArray(metadatumKey);
getBrowseURLFor(metadataKey: string, linkPath: string): Observable<string> {
const searchKeyArray = BrowseService.toSearchKeyArray(metadataKey);
return this.getBrowseDefinitions().pipe(
getRemoteDataPayload(),
map((browseDefinitions: BrowseDefinition[]) => browseDefinitions
@@ -192,7 +195,7 @@ export class BrowseService {
),
map((def: BrowseDefinition) => {
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 {
return def._links[linkPath];
}

View File

@@ -8,20 +8,24 @@ import { of as observableOf } from 'rxjs';
const pageInfo = new PageInfo();
const array = [
Object.assign(new Item(), {
metadata: [
metadata: {
'dc.title': [
{
key: 'dc.title',
language: 'en_US',
value: 'Item nr 1'
}]
}
]
}
}),
Object.assign(new Item(), {
metadata: [
metadata: {
'dc.title': [
{
key: 'dc.title',
language: 'en_US',
value: 'Item nr 2'
}]
}
]
}
})
];
const paginatedList = new PaginatedList(pageInfo, array);

View File

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

View File

@@ -24,13 +24,14 @@ describe('BrowseItemsResponseParsingService', () => {
uuid: 'd7b6bc6f-ff6c-444a-a0d3-0cd9b68043e7',
name: 'Development of Local Supply Chain : A Critical Link for Concentrated Solar Power in India',
handle: '10986/17472',
metadata: [
metadata: {
'dc.creator': [
{
key: 'dc.creator',
value: 'World Bank',
language: null
}
],
]
},
inArchive: true,
discoverable: true,
withdrawn: false,
@@ -56,13 +57,14 @@ describe('BrowseItemsResponseParsingService', () => {
uuid: '27c6f976-257c-4ad0-a0ef-c5e34ffe4d5b',
name: 'Development of Local Supply Chain : The Missing Link for Concentrated Solar Power Projects in India',
handle: '10986/17475',
metadata: [
metadata: {
'dc.creator': [
{
key: 'dc.creator',
value: 'World Bank',
language: null
}
],
]
},
inArchive: true,
discoverable: true,
withdrawn: false,
@@ -115,13 +117,14 @@ describe('BrowseItemsResponseParsingService', () => {
uuid: 'd7b6bc6f-ff6c-444a-a0d3-0cd9b68043e7',
name: 'Development of Local Supply Chain : A Critical Link for Concentrated Solar Power in India',
handle: '10986/17472',
metadata: [
metadata: {
'dc.creator': [
{
key: 'dc.creator',
value: 'World Bank',
language: null
}
],
]
},
inArchive: true,
discoverable: true,
withdrawn: false,

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

View File

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

View File

@@ -37,6 +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';
/* tslint:disable:max-classes-per-file */
@Component({
@@ -152,7 +153,7 @@ describe('MetadataService', () => {
expect(title.getTitle()).toEqual('Test PowerPoint Document');
expect(tagStore.get('citation_title')[0].content).toEqual('Test PowerPoint Document');
expect(tagStore.get('citation_author')[0].content).toEqual('Doe, Jane');
expect(tagStore.get('citation_date')[0].content).toEqual('1650-06-26T19:58:25Z');
expect(tagStore.get('citation_date')[0].content).toEqual('1650-06-26');
expect(tagStore.get('citation_issn')[0].content).toEqual('123456789');
expect(tagStore.get('citation_language')[0].content).toEqual('en');
expect(tagStore.get('citation_keywords')[0].content).toEqual('keyword1; keyword2; keyword3');
@@ -216,22 +217,18 @@ describe('MetadataService', () => {
const mockType = (mockItem: Item, type: string): Item => {
const typedMockItem = Object.assign(new Item(), mockItem) as Item;
for (const metadatum of typedMockItem.metadata) {
if (metadatum.key === 'dc.type') {
metadatum.value = type;
break;
}
}
typedMockItem.metadata['dc.type'] = [ { value: type } ] as MetadataValue[];
return typedMockItem;
}
const mockPublisher = (mockItem: Item): Item => {
const publishedMockItem = Object.assign(new Item(), mockItem) as Item;
publishedMockItem.metadata.push({
key: 'dc.publisher',
publishedMockItem.metadata['dc.publisher'] = [
{
language: 'en_US',
value: 'Mock Publisher'
});
}
] as MetadataValue[];
return publishedMockItem;
}

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
*
@@ -301,14 +305,7 @@ export class MetadataService {
* true if this._item has a dc.type equal to 'Thesis'
*/
private isDissertation(): boolean {
let isDissertation = false;
for (const metadatum of this.currentObject.value.metadata) {
if (metadatum.key === 'dc.type') {
isDissertation = metadatum.value.toLowerCase() === 'thesis';
break;
}
}
return isDissertation;
return this.hasType('thesis');
}
/**
@@ -318,40 +315,15 @@ export class MetadataService {
* true if this._item has a dc.type equal to 'Technical Report'
*/
private isTechReport(): boolean {
let isTechReport = false;
for (const metadatum of this.currentObject.value.metadata) {
if (metadatum.key === 'dc.type') {
isTechReport = metadatum.value.toLowerCase() === 'technical report';
break;
}
}
return isTechReport;
return this.hasType('technical report');
}
private getMetaTagValue(key: string): string {
let value: string;
for (const metadatum of this.currentObject.value.metadata) {
if (metadatum.key === key) {
value = metadatum.value;
}
}
return value;
return this.currentObject.value.firstMetadataValue(key);
}
private getFirstMetaTagValue(keys: string[]): string {
let value: string;
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;
return this.currentObject.value.firstMetadataValue(keys);
}
private getMetaTagValuesAndCombine(key: string): string {
@@ -359,15 +331,7 @@ export class MetadataService {
}
private getMetaTagValues(keys: string[]): string[] {
const values: string[] = [];
for (const metadatum of this.currentObject.value.metadata) {
for (const key of keys) {
if (key === metadatum.key) {
values.push(metadatum.value);
}
}
}
return values;
return this.currentObject.value.allMetadataValues(keys);
}
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
*/
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
*/
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
*/
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
*/
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
*/
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
*/
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
*/
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
*/
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
*/
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 { isEmpty, isNotEmpty } from '../../shared/empty.util';
import { MetadataMap, MetadataValue, MetadataValueFilter } from './metadata.interfaces';
import { Metadata } from './metadata.model';
import { CacheableObject } from '../cache/object-cache.reducer';
import { RemoteData } from '../data/remote-data';
import { ResourceType } from './resource-type';
@@ -35,14 +35,14 @@ export class DSpaceObject implements CacheableObject, ListableObject {
* The name for this DSpaceObject
*/
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
metadata: Metadatum[] = [];
metadata: MetadataMap;
/**
* An array of DSpaceObjects that are direct parents of this DSpaceObject
@@ -55,41 +55,58 @@ export class DSpaceObject implements CacheableObject, ListableObject {
owner: Observable<RemoteData<DSpaceObject>>;
/**
* Find a metadata field by key and language
* Gets all matching metadata in this DSpaceObject.
*
* 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
* @param {string|string[]} keyOrKeys The metadata key(s) in scope. Wildcards are supported; see [[Metadata]].
* @param {MetadataValueFilter} filter The value filter to use. If unspecified, no filtering will be done.
* @returns {MetadataValue[]} the matching values or an empty array.
*/
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;
}
allMetadata(keyOrKeys: string | string[], valueFilter?: MetadataValueFilter): MetadataValue[] {
return Metadata.all(this.metadata, keyOrKeys, valueFilter);
}
/**
* Find metadata by an array of keys
* Like [[allMetadata]], but only returns string values.
*
* This method returns the values of the element
* in the metadata array that match the provided
* key(s)
*
* @param key(s)
* @return Array<Metadatum>
* @param {string|string[]} keyOrKeys The metadata key(s) in scope. Wildcards are supported; see [[Metadata]].
* @param {MetadataValueFilter} filter The value filter to use. If unspecified, no filtering will be done.
* @returns {string[]} the matching string values or an empty array.
*/
filterMetadata(keys: string[]): Metadatum[] {
return this.metadata.filter((metadatum: Metadatum) => {
return keys.some((key) => key === metadatum.key);
});
allMetadataValues(keyOrKeys: string | string[], valueFilter?: MetadataValueFilter): string[] {
return Metadata.allValues(this.metadata, keyOrKeys, valueFilter);
}
/**
* Gets the first matching MetadataValue object in this DSpaceObject, or `undefined`.
*
* @param {string|string[]} keyOrKeys The metadata key(s) in scope. Wildcards are supported; see [[Metadata]].
* @param {MetadataValueFilter} filter The value filter to use. If unspecified, no filtering will be done.
* @returns {MetadataValue} the first matching value, or `undefined`.
*/
firstMetadata(keyOrKeys: string | string[], valueFilter?: MetadataValueFilter): MetadataValue {
return Metadata.first(this.metadata, keyOrKeys, valueFilter);
}
/**
* Like [[firstMetadata]], but only returns a string value, or `undefined`.
*
* @param {string|string[]} keyOrKeys The metadata key(s) in scope. Wildcards are supported; see [[Metadata]].
* @param {MetadataValueFilter} filter The value filter to use. If unspecified, no filtering will be done.
* @returns {string} the first matching string value, or `undefined`.
*/
firstMetadataValue(keyOrKeys: string | string[], valueFilter?: MetadataValueFilter): string {
return Metadata.firstValue(this.metadata, keyOrKeys, valueFilter);
}
/**
* Checks for a matching metadata value in this DSpaceObject.
*
* @param {string|string[]} keyOrKeys The metadata key(s) in scope. Wildcards are supported; see [[Metadata]].
* @param {MetadataValueFilter} filter The value filter to use. If unspecified, no filtering will be done.
* @returns {boolean} whether a match is found.
*/
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,175 @@
import { isUndefined } from '../../shared/empty.util';
import { MetadataValue, MetadataValueFilter } from './metadata.interfaces';
import { Metadata } from './metadata.model';
const mdValue = (value: string, language?: string): MetadataValue => {
return { value: value, language: isUndefined(language) ? null : language };
}
const dcDescription = mdValue('Some description');
const dcAbstract = mdValue('Some abstract');
const dcTitle0 = mdValue('Title 0');
const dcTitle1 = mdValue('Title 1');
const dcTitle2 = mdValue('Title 2', 'en_US');
const bar = mdValue('Bar');
const singleMap = { 'dc.title': [ dcTitle0 ] };
const multiMap = {
'dc.description': [ dcDescription ],
'dc.description.abstract': [ dcAbstract ],
'dc.title': [ dcTitle1, dcTitle2 ],
'foo': [ bar ]
};
const testMethod = (fn, resultKind, mapOrMaps, keyOrKeys, expected, filter?) => {
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)), () => {
const result = fn(mapOrMaps, keys, filter);
let shouldReturn;
if (resultKind === 'boolean') {
shouldReturn = expected;
} else if (isUndefined(expected)) {
shouldReturn = 'undefined';
} else if (expected instanceof Array) {
shouldReturn = 'an array with ' + expected.length + ' ' + (expected.length > 1 ? 'ordered ' : '')
+ resultKind + (expected.length !== 1 ? 's' : '');
} else {
shouldReturn = 'a ' + resultKind;
}
it('should return ' + shouldReturn, () => {
expect(result).toEqual(expected);
});
})
};
describe('Metadata', () => {
describe('all method', () => {
const testAll = (mapOrMaps, keyOrKeys, expected, filter?: MetadataValueFilter) =>
testMethod(Metadata.all, 'value', mapOrMaps, keyOrKeys, expected, filter);
describe('with emptyMap', () => {
testAll({}, 'foo', []);
testAll({}, '*', []);
});
describe('with singleMap', () => {
testAll(singleMap, 'foo', []);
testAll(singleMap, '*', [ dcTitle0 ]);
testAll(singleMap, '*', [], { value: 'baz' });
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 ]);
});
describe('with [ singleMap, multiMap ]', () => {
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 ]);
});
});
describe('allValues method', () => {
const testAllValues = (mapOrMaps, keyOrKeys, expected) =>
testMethod(Metadata.allValues, 'string', mapOrMaps, keyOrKeys, expected);
describe('with emptyMap', () => {
testAllValues({}, '*', []);
});
describe('with singleMap', () => {
testAllValues([ singleMap, multiMap ], '*', [ dcTitle0.value ]);
});
describe('with [ multiMap, singleMap ]', () => {
testAllValues([ multiMap, singleMap ], '*', [ dcDescription.value, dcAbstract.value, dcTitle1.value, dcTitle2.value, bar.value ]);
});
});
describe('first method', () => {
const testFirst = (mapOrMaps, keyOrKeys, expected) =>
testMethod(Metadata.first, 'value', mapOrMaps, keyOrKeys, expected);
describe('with emptyMap', () => {
testFirst({}, '*', undefined);
});
describe('with singleMap', () => {
testFirst(singleMap, '*', dcTitle0);
});
describe('with [ multiMap, singleMap ]', () => {
testFirst([ multiMap, singleMap ], '*', dcDescription);
});
});
describe('firstValue method', () => {
const testFirstValue = (mapOrMaps, keyOrKeys, expected) =>
testMethod(Metadata.firstValue, 'value', mapOrMaps, keyOrKeys, expected);
describe('with emptyMap', () => {
testFirstValue({}, '*', undefined);
});
describe('with singleMap', () => {
testFirstValue(singleMap, '*', dcTitle0.value);
});
describe('with [ multiMap, singleMap ]', () => {
testFirstValue([ multiMap, singleMap ], '*', dcDescription.value);
});
});
describe('has method', () => {
const testHas = (mapOrMaps, keyOrKeys, expected, filter?: MetadataValueFilter) =>
testMethod(Metadata.has, 'boolean', mapOrMaps, keyOrKeys, expected, filter);
describe('with emptyMap', () => {
testHas({}, '*', false);
});
describe('with singleMap', () => {
testHas(singleMap, '*', true);
testHas(singleMap, '*', false, { value: 'baz' });
});
describe('with [ multiMap, singleMap ]', () => {
testHas([ multiMap, singleMap ], '*', true);
});
});
describe('valueMatches method', () => {
const testValueMatches = (value: MetadataValue, expected: boolean, filter?: MetadataValueFilter) => {
describe('with value ' + JSON.stringify(value) + ' and filter '
+ (isUndefined(filter) ? 'undefined' : JSON.stringify(filter)), () => {
const result = Metadata.valueMatches(value, filter);
it('should return ' + expected, () => {
expect(result).toEqual(expected);
});
});
};
testValueMatches(mdValue('a'), true);
testValueMatches(mdValue('a'), true, { value: 'a' });
testValueMatches(mdValue('a'), false, { value: 'A' });
testValueMatches(mdValue('a'), true, { value: 'A', ignoreCase: true });
testValueMatches(mdValue('ab'), false, { value: 'b' });
testValueMatches(mdValue('ab'), true, { value: 'b', substring: true });
testValueMatches(mdValue('a'), true, { language: null });
testValueMatches(mdValue('a'), false, { language: 'en_US' });
testValueMatches(mdValue('a', 'en_US'), true, { language: 'en_US' });
});
});

View File

@@ -0,0 +1,163 @@
import { isEmpty, isNotUndefined, isUndefined } from '../../shared/empty.util';
import { MetadataMap, MetadataValue, MetadataValueFilter } from './metadata.interfaces';
/**
* Utility class for working with DSpace object metadata.
*
* When specifying metadata keys, 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.
*
* When multiple keys in a map match a given 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.
*/
export class Metadata {
/**
* Gets all matching metadata in the map(s).
*
* @param {MetadataMap|MetadataMap[]} mapOrMaps The source map(s). When multiple maps are given, they will be
* checked in order, and only values from the first with at least one match will be returned.
* @param {string|string[]} keyOrKeys The metadata key(s) in scope. Wildcards are supported; see above.
* @param {MetadataValueFilter} filter The value filter to use. If unspecified, no filtering will be done.
* @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 [[Metadata.all]], but only returns string values.
*
* @param {MetadataMap|MetadataMap[]} mapOrMaps The source map(s). When multiple maps are given, they will be
* checked in order, and only values from the first with at least one match will be returned.
* @param {string|string[]} keyOrKeys The metadata key(s) in scope. Wildcards are supported; see above.
* @param {MetadataValueFilter} filter The value filter to use. If unspecified, no filtering will be done.
* @returns {string[]} the matching string values or an empty array.
*/
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 the map(s), or `undefined`.
*
* @param {MetadataMap|MetadataMap[]} mapOrMaps The source map(s).
* @param {string|string[]} keyOrKeys The metadata key(s) in scope. Wildcards are supported; see above.
* @param {MetadataValueFilter} filter The value filter to use. If unspecified, no filtering will be done.
* @returns {MetadataValue} the first matching value, 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 [[Metadata.first]], but only returns a string value, or `undefined`.
*
* @param {MetadataMap|MetadataMap[]} mapOrMaps The source map(s).
* @param {string|string[]} keyOrKeys The metadata key(s) in scope. Wildcards are supported; see above.
* @param {MetadataValueFilter} filter The value filter to use. If unspecified, no filtering will be done.
* @returns {string} the first matching string value, or `undefined`.
*/
public static firstValue(mdMapOrMaps: MetadataMap | MetadataMap[], keyOrKeys: string | string[],
filter?: MetadataValueFilter): string {
const value = Metadata.first(mdMapOrMaps, keyOrKeys, filter);
return isUndefined(value) ? undefined : value.value;
}
/**
* Checks for a matching metadata value in the given map(s).
*
* @param {MetadataMap|MetadataMap[]} mapOrMaps The source map(s).
* @param {string|string[]} keyOrKeys The metadata key(s) in scope. Wildcards are supported; see above.
* @param {MetadataValueFilter} filter The value filter to use. If unspecified, no filtering will be done.
* @returns {boolean} whether a match is found.
*/
public static has(mdMapOrMaps: MetadataMap | MetadataMap[], keyOrKeys: string | string[],
filter?: MetadataValueFilter): boolean {
return isNotUndefined(Metadata.first(mdMapOrMaps, keyOrKeys, filter));
}
/**
* Checks if a value matches a filter.
*
* @param {MetadataValue} mdValue the value to check.
* @param {MetadataValueFilter} filter the filter to use.
* @returns {boolean} whether the filter matches, or true if no filter is given.
*/
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`.
*
* @param {MetadataMap} mdMap The source map.
* @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 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

@@ -11,7 +11,6 @@ import { ResourceType } from '../../../core/shared/resource-type';
import { ComColFormComponent } from './comcol-form.component';
import { DSpaceObject } from '../../../core/shared/dspace-object.model';
import { hasValue } from '../../empty.util';
import { Metadatum } from '../../../core/shared/metadatum.model';
describe('ComColFormComponent', () => {
let comp: ComColFormComponent<DSpaceObject>;
@@ -29,23 +28,24 @@ describe('ComColFormComponent', () => {
return undefined;
}
};
const titleMD = { key: 'dc.title', value: 'Community Title' } as Metadatum;
const randomMD = { key: 'dc.random', value: 'Random metadata excluded from form' } as Metadatum;
const abstractMD = {
key: 'dc.description.abstract',
value: 'Community description'
} as Metadatum;
const newTitleMD = { key: 'dc.title', value: 'New Community Title' } as Metadatum;
const dcTitle = 'dc.title';
const dcRandom = 'dc.random';
const dcAbstract = 'dc.description.abstract';
const titleMD = { [dcTitle]: [ { value: 'Community Title', language: null } ] };
const randomMD = { [dcRandom]: [ { value: 'Random metadata excluded from form', language: null } ] };
const abstractMD = { [dcAbstract]: [ { value: 'Community description', language: null } ] };
const newTitleMD = { [dcTitle]: [ { value: 'New Community Title', language: null } ] };
const formModel = [
new DynamicInputModel({
id: 'title',
name: newTitleMD.key,
value: 'New Community Title'
name: dcTitle,
value: newTitleMD[dcTitle][0].value
}),
new DynamicInputModel({
id: 'abstract',
name: abstractMD.key,
value: abstractMD.value
name: dcAbstract,
value: abstractMD[dcAbstract][0].value
})
];
@@ -87,10 +87,10 @@ describe('ComColFormComponent', () => {
comp.dso = Object.assign(
new Community(),
{
metadata: [
titleMD,
randomMD
]
metadata: {
...titleMD,
...randomMD
}
}
);
@@ -101,11 +101,11 @@ describe('ComColFormComponent', () => {
{},
new Community(),
{
metadata: [
randomMD,
newTitleMD,
abstractMD
],
metadata: {
...newTitleMD,
...randomMD,
...abstractMD
},
type: ResourceType.Community
},
)

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 { TranslateService } from '@ngx-translate/core';
import { DSpaceObject } from '../../../core/shared/dspace-object.model';
import { MetadataMap, MetadataValue } from '../../../core/shared/metadata.interfaces';
import { isNotEmpty } from '../../empty.util';
import { ResourceType } from '../../../core/shared/resource-type';
@@ -64,7 +65,7 @@ export class ComColFormComponent<T extends DSpaceObject> implements OnInit {
ngOnInit(): void {
this.formModel.forEach(
(fieldModel: DynamicInputModel) => {
fieldModel.value = this.dso.findMetadata(fieldModel.name);
fieldModel.value = this.dso.firstMetadataValue(fieldModel.name);
}
);
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() {
const metadata = this.formModel.map(
(fieldModel: DynamicInputModel) => {
return { key: fieldModel.name, value: fieldModel.value }
const formMetadata = new Object() as MetadataMap;
this.formModel.forEach((fieldModel: DynamicInputModel) => {
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, {
metadata: newMetadata,
metadata: {
...this.dso.metadata,
...formMetadata
},
type: ResourceType.Community
});
this.submitForm.emit(updatedDSO);

View File

@@ -51,13 +51,14 @@ export const MockItem: Item = Object.assign(new Item(), {
id: 'cf9b0c8e-a1eb-4b65-afd0-567366448713',
uuid: 'cf9b0c8e-a1eb-4b65-afd0-567366448713',
type: 'bitstream',
metadata: [
metadata: {
'dc.title': [
{
key: 'dc.title',
language: null,
value: 'test_word.docx'
}
]
}
},
{
sizeBytes: 31302,
@@ -85,14 +86,15 @@ export const MockItem: Item = Object.assign(new Item(), {
id: '99b00f3c-1cc6-4689-8158-91965bee6b28',
uuid: '99b00f3c-1cc6-4689-8158-91965bee6b28',
type: 'bitstream',
metadata: [
metadata: {
'dc.title': [
{
key: 'dc.title',
language: null,
value: 'test_pdf.pdf'
}
]
}
}
]
}
}),
@@ -100,98 +102,106 @@ export const MockItem: Item = Object.assign(new Item(), {
id: '0ec7ff22-f211-40ab-a69e-c819b0b1f357',
uuid: '0ec7ff22-f211-40ab-a69e-c819b0b1f357',
type: 'item',
metadata: [
metadata: {
'dc.creator': [
{
key: 'dc.creator',
language: 'en_US',
value: 'Doe, Jane'
},
}
],
'dc.date.accessioned': [
{
key: 'dc.date.accessioned',
language: null,
value: '1650-06-26T19:58:25Z'
},
}
],
'dc.date.available': [
{
key: 'dc.date.available',
language: null,
value: '1650-06-26T19:58:25Z'
},
}
],
'dc.date.issued': [
{
key: 'dc.date.issued',
language: null,
value: '1650-06-26'
},
}
],
'dc.identifier.issn': [
{
key: 'dc.identifier.issn',
language: 'en_US',
value: '123456789'
},
}
],
'dc.identifier.uri': [
{
key: 'dc.identifier.uri',
language: null,
value: 'http://dspace7.4science.it/xmlui/handle/10673/6'
},
}
],
'dc.description.abstract': [
{
key: 'dc.description.abstract',
language: 'en_US',
value: 'This is really just a sample abstract. If it was a real abstract it would contain useful information about this test document. Sorry though, nothing useful in this paragraph. You probably shouldn\'t have even bothered to read it!'
},
}
],
'dc.description.provenance': [
{
key: 'dc.description.provenance',
language: 'en',
value: 'Made available in DSpace on 2012-06-26T19:58:25Z (GMT). No. of bitstreams: 2\r\ntest_ppt.ppt: 12707328 bytes, checksum: a353fc7d29b3c558c986f7463a41efd3 (MD5)\r\ntest_ppt.pptx: 12468572 bytes, checksum: 599305edb4ebee329667f2c35b14d1d6 (MD5)'
},
{
key: 'dc.description.provenance',
language: 'en',
value: 'Restored into DSpace on 2013-06-13T09:17:34Z (GMT).'
},
{
key: 'dc.description.provenance',
language: 'en',
value: 'Restored into DSpace on 2013-06-13T11:04:16Z (GMT).'
},
{
key: 'dc.description.provenance',
language: 'en',
value: 'Restored into DSpace on 2017-04-24T19:44:08Z (GMT).'
},
}
],
'dc.language': [
{
key: 'dc.language',
language: 'en_US',
value: 'en'
},
}
],
'dc.rights': [
{
key: 'dc.rights',
language: 'en_US',
value: '© Jane Doe'
},
}
],
'dc.subject': [
{
key: 'dc.subject',
language: 'en_US',
value: 'keyword1'
},
{
key: 'dc.subject',
language: 'en_US',
value: 'keyword2'
},
{
key: 'dc.subject',
language: 'en_US',
value: 'keyword3'
},
}
],
'dc.title': [
{
key: 'dc.title',
language: 'en_US',
value: 'Test PowerPoint Document'
},
}
],
'dc.type': [
{
key: 'dc.type',
language: 'en_US',
value: 'text'
}
],
]
},
owningCollection: observableOf({
self: 'https://dspace7.4science.it/dspace-spring-rest/api/core/collections/1c11f3f1-ba1f-4f36-908a-3f1ea9a557eb',
requestPending: false,

View File

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

View File

@@ -8,21 +8,25 @@ let collectionGridElementComponent: CollectionGridElementComponent;
let fixture: ComponentFixture<CollectionGridElementComponent>;
const mockCollectionWithAbstract: Collection = Object.assign(new Collection(), {
metadata: [
metadata: {
'dc.description.abstract': [
{
key: 'dc.description.abstract',
language: 'en_US',
value: 'Short description'
}]
}
]
}
});
const mockCollectionWithoutAbstract: Collection = Object.assign(new Collection(), {
metadata: [
metadata: {
'dc.title': [
{
key: 'dc.title',
language: 'en_US',
value: 'Test title'
}]
}
]
}
});
describe('CollectionGridElementComponent', () => {

View File

@@ -8,21 +8,25 @@ let communityGridElementComponent: CommunityGridElementComponent;
let fixture: ComponentFixture<CommunityGridElementComponent>;
const mockCommunityWithAbstract: Community = Object.assign(new Community(), {
metadata: [
metadata: {
'dc.description.abstract': [
{
key: 'dc.description.abstract',
language: 'en_US',
value: 'Short description'
}]
}
]
}
});
const mockCommunityWithoutAbstract: Community = Object.assign(new Community(), {
metadata: [
metadata: {
'dc.title': [
{
key: 'dc.title',
language: 'en_US',
value: 'Test title'
}]
}
]
}
});
describe('CommunityGridElementComponent', () => {

View File

@@ -5,19 +5,19 @@
</ds-grid-thumbnail>
</a>
<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">
<p *ngIf="object.filterMetadata(['dc.contributor.author', 'dc.creator', 'dc.contributor.*']).length > 0" 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}}
<p *ngIf="object.hasMetadata(['dc.contributor.author', 'dc.creator', 'dc.contributor.*'])" class="item-authors card-text text-muted">
<span *ngFor="let author of object.allMetadataValues(['dc.contributor.author', 'dc.creator', 'dc.contributor.*']); let last=last;">{{author}}
<span *ngIf="!last">; </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>
</ds-truncatable-part>
<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>
<div class="text-center pt-2">

View File

@@ -11,31 +11,37 @@ let fixture: ComponentFixture<ItemGridElementComponent>;
const mockItemWithAuthorAndDate: Item = Object.assign(new Item(), {
bitstreams: observableOf({}),
metadata: [
metadata: {
'dc.contributor.author': [
{
key: 'dc.contributor.author',
language: 'en_US',
value: 'Smith, Donald'
},
}
],
'dc.date.issued': [
{
key: 'dc.date.issued',
language: null,
value: '2015-06-26'
}]
}
]
}
});
const mockItemWithoutAuthorAndDate: Item = Object.assign(new Item(), {
bitstreams: observableOf({}),
metadata: [
metadata: {
'dc.title': [
{
key: 'dc.title',
language: 'en_US',
value: 'This is just another title'
},
}
],
'dc.type': [
{
key: 'dc.type',
language: null,
value: 'Article'
}]
}
]
}
});
describe('ItemGridElementComponent', () => {

View File

@@ -16,25 +16,29 @@ const truncatableServiceStub: any = {
};
const mockCollectionWithAbstract: CollectionSearchResult = new CollectionSearchResult();
mockCollectionWithAbstract.hitHighlights = [];
mockCollectionWithAbstract.hitHighlights = {};
mockCollectionWithAbstract.dspaceObject = Object.assign(new Collection(), {
metadata: [
metadata: {
'dc.description.abstract': [
{
key: 'dc.description.abstract',
language: 'en_US',
value: 'Short description'
} ]
}
]
}
});
const mockCollectionWithoutAbstract: CollectionSearchResult = new CollectionSearchResult();
mockCollectionWithoutAbstract.hitHighlights = [];
mockCollectionWithoutAbstract.hitHighlights = {};
mockCollectionWithoutAbstract.dspaceObject = Object.assign(new Collection(), {
metadata: [
metadata: {
'dc.title': [
{
key: 'dc.title',
language: 'en_US',
value: 'Test title'
} ]
}
]
}
});
describe('CollectionSearchResultGridElementComponent', () => {

View File

@@ -16,25 +16,29 @@ const truncatableServiceStub: any = {
};
const mockCommunityWithAbstract: CommunitySearchResult = new CommunitySearchResult();
mockCommunityWithAbstract.hitHighlights = [];
mockCommunityWithAbstract.hitHighlights = {};
mockCommunityWithAbstract.dspaceObject = Object.assign(new Community(), {
metadata: [
metadata: {
'dc.description.abstract': [
{
key: 'dc.description.abstract',
language: 'en_US',
value: 'Short description'
} ]
}
]
}
});
const mockCommunityWithoutAbstract: CommunitySearchResult = new CommunitySearchResult();
mockCommunityWithoutAbstract.hitHighlights = [];
mockCommunityWithoutAbstract.hitHighlights = {};
mockCommunityWithoutAbstract.dspaceObject = Object.assign(new Community(), {
metadata: [
metadata: {
'dc.title': [
{
key: 'dc.title',
language: 'en_US',
value: 'Test title'
} ]
}
]
}
});
describe('CommunitySearchResultGridElementComponent', () => {

View File

@@ -8,20 +8,20 @@
</a>
<div class="card-body">
<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>
<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">
<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 *ngFor="let authorMd of dso.filterMetadata(['dc.contributor.author', 'dc.creator', 'dc.contributor.*']);">,
<span [innerHTML]="authorMd.value"></span>
<span *ngIf="dso.hasMetadata('dc.date.issued')" class="item-date">{{dso.firstMetadataValue('dc.date.issued')}}</span>
<span *ngFor="let author of dso.allMetadataValues(['dc.contributor.author', 'dc.creator', 'dc.contributor.*']);">,
<span [innerHTML]="author"></span>
</span>
</ds-truncatable-part>
</p>
<p class="item-abstract card-text">
<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>
</p>
<div class="text-center">

View File

@@ -17,37 +17,43 @@ const truncatableServiceStub: any = {
};
const mockItemWithAuthorAndDate: ItemSearchResult = new ItemSearchResult();
mockItemWithAuthorAndDate.hitHighlights = [];
mockItemWithAuthorAndDate.hitHighlights = {};
mockItemWithAuthorAndDate.dspaceObject = Object.assign(new Item(), {
bitstreams: observableOf({}),
metadata: [
metadata: {
'dc.contributor.author': [
{
key: 'dc.contributor.author',
language: 'en_US',
value: 'Smith, Donald'
},
}
],
'dc.date.issued': [
{
key: 'dc.date.issued',
language: null,
value: '2015-06-26'
}]
}
]
}
});
const mockItemWithoutAuthorAndDate: ItemSearchResult = new ItemSearchResult();
mockItemWithoutAuthorAndDate.hitHighlights = [];
mockItemWithoutAuthorAndDate.hitHighlights = {};
mockItemWithoutAuthorAndDate.dspaceObject = Object.assign(new Item(), {
bitstreams: observableOf({}),
metadata: [
metadata: {
'dc.title': [
{
key: 'dc.title',
language: 'en_US',
value: 'This is just another title'
},
}
],
'dc.type': [
{
key: 'dc.type',
language: null,
value: 'Article'
}]
}
]
}
});
describe('ItemSearchResultGridElementComponent', () => {

View File

@@ -2,12 +2,11 @@ import { Component, Inject } from '@angular/core';
import { SearchResult } from '../../../+search-page/search-result.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 { 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';
@Component({
selector: 'ds-search-result-grid-element',
@@ -22,39 +21,24 @@ export class SearchResultGridElementComponent<T extends SearchResult<K>, K exten
this.dso = this.object.dspaceObject;
}
getValues(keys: string[]): string[] {
const results: string[] = new Array<string>();
this.object.hitHighlights.forEach(
(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;
/**
* Gets all matching metadata string values from hitHighlights or dso metadata, preferring hitHighlights.
*
* @param {string|string[]} keyOrKeys The metadata key(s) in scope. Wildcards are supported; see [[Metadata]].
* @returns {string[]} the matching string values or an empty array.
*/
allMetadataValues(keyOrKeys: string | string[]): string[] {
return Metadata.allValues([this.object.hitHighlights, this.dso.metadata], keyOrKeys);
}
getFirstValue(key: string): string {
let result: string;
this.object.hitHighlights.some(
(md: Metadatum) => {
if (key === md.key) {
result = md.value;
return true;
}
}
);
if (hasNoValue(result)) {
result = this.dso.findMetadata(key);
}
return result;
/**
* Gets the first matching metadata string value from hitHighlights or dso metadata, preferring hitHighlights.
*
* @param {string|string[]} keyOrKeys The metadata key(s) in scope. Wildcards are supported; see [[Metadata]].
* @returns {string} the first matching string value, or `undefined`.
*/
firstMetadataValue(keyOrKeys: string | string[]): string {
return Metadata.firstValue([this.object.hitHighlights, this.dso.metadata], keyOrKeys);
}
isCollapsed(): Observable<boolean> {

View File

@@ -2,7 +2,6 @@ import { async, ComponentFixture, TestBed } from '@angular/core/testing';
import { ChangeDetectionStrategy, NO_ERRORS_SCHEMA } from '@angular/core';
import { By } from '@angular/platform-browser';
import { TruncatePipe } from '../../utils/truncate.pipe';
import { Metadatum } from '../../../core/shared/metadatum.model';
import { BrowseEntryListElementComponent } from './browse-entry-list-element.component';
import { BrowseEntry } from '../../../core/shared/browse-entry.model';
@@ -33,7 +32,7 @@ describe('MetadataListElementComponent', () => {
browseEntryListElementComponent = fixture.componentInstance;
}));
describe('When the metadatum is loaded', () => {
describe('When the metadata is loaded', () => {
beforeEach(() => {
browseEntryListElementComponent.object = mockValue;
fixture.detectChanges();

View File

@@ -8,21 +8,25 @@ let collectionListElementComponent: CollectionListElementComponent;
let fixture: ComponentFixture<CollectionListElementComponent>;
const mockCollectionWithAbstract: Collection = Object.assign(new Collection(), {
metadata: [
metadata: {
'dc.description.abstract': [
{
key: 'dc.description.abstract',
language: 'en_US',
value: 'Short description'
}]
}
]
}
});
const mockCollectionWithoutAbstract: Collection = Object.assign(new Collection(), {
metadata: [
metadata: {
'dc.title': [
{
key: 'dc.title',
language: 'en_US',
value: 'Test title'
}]
}
]
}
});
describe('CollectionListElementComponent', () => {

View File

@@ -8,21 +8,25 @@ let communityListElementComponent: CommunityListElementComponent;
let fixture: ComponentFixture<CommunityListElementComponent>;
const mockCommunityWithAbstract: Community = Object.assign(new Community(), {
metadata: [
metadata: {
'dc.description.abstract': [
{
key: 'dc.description.abstract',
language: 'en_US',
value: 'Short description'
}]
}
]
}
});
const mockCommunityWithoutAbstract: Community = Object.assign(new Community(), {
metadata: [
metadata: {
'dc.title': [
{
key: 'dc.title',
language: 'en_US',
value: 'Test title'
}]
}
]
}
});
describe('CommunityListElementComponent', () => {

View File

@@ -1,23 +1,23 @@
<ds-truncatable [id]="object.id">
<a [routerLink]="['/items/' + object.id]" class="lead">
{{object.findMetadata("dc.title")}}
{{object.firstMetadataValue("dc.title")}}
</a>
<div>
<ds-truncatable-part [id]="object.id" [minLines]="1">
<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">
<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>
</span>
(<span *ngIf="hasValue(object.findMetadata('dc.publisher'))" class="item-list-publisher">{{object.findMetadata("dc.publisher")}}, </span><span
*ngIf="hasValue(object.findMetadata('dc.date.issued'))" class="item-list-date">{{object.findMetadata("dc.date.issued")}}</span>)
(<span *ngIf="object.hasMetadata('dc.publisher')" class="item-list-publisher">{{object.firstMetadataValue("dc.publisher")}}, </span><span
*ngIf="object.hasMetadata('dc.date.issued')" class="item-list-date">{{object.firstMetadataValue("dc.date.issued")}}</span>)
</span>
</ds-truncatable-part>
<ds-truncatable-part [id]="object.id" [minLines]="3">
<div *ngIf="object.findMetadata('dc.description.abstract')" class="item-list-abstract">
{{object.findMetadata("dc.description.abstract")}}
<div *ngIf="object.hasMetadata('dc.description.abstract')" class="item-list-abstract">
{{object.firstMetadataValue("dc.description.abstract")}}
</div>
</ds-truncatable-part>
</div>

View File

@@ -11,31 +11,37 @@ let fixture: ComponentFixture<ItemListElementComponent>;
const mockItemWithAuthorAndDate: Item = Object.assign(new Item(), {
bitstreams: observableOf({}),
metadata: [
metadata: {
'dc.contributor.author': [
{
key: 'dc.contributor.author',
language: 'en_US',
value: 'Smith, Donald'
},
}
],
'dc.date.issued': [
{
key: 'dc.date.issued',
language: null,
value: '2015-06-26'
}]
}
]
}
});
const mockItemWithoutAuthorAndDate: Item = Object.assign(new Item(), {
bitstreams: observableOf({}),
metadata: [
metadata: {
'dc.title': [
{
key: 'dc.title',
language: 'en_US',
value: 'This is just another title'
},
}
],
'dc.type': [
{
key: 'dc.type',
language: null,
value: 'Article'
}]
}
]
}
});
describe('ItemListElementComponent', () => {

View File

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

View File

@@ -16,25 +16,29 @@ const truncatableServiceStub: any = {
};
const mockCollectionWithAbstract: CollectionSearchResult = new CollectionSearchResult();
mockCollectionWithAbstract.hitHighlights = [];
mockCollectionWithAbstract.hitHighlights = {};
mockCollectionWithAbstract.dspaceObject = Object.assign(new Collection(), {
metadata: [
metadata: {
'dc.description.abstract': [
{
key: 'dc.description.abstract',
language: 'en_US',
value: 'Short description'
} ]
}
]
}
});
const mockCollectionWithoutAbstract: CollectionSearchResult = new CollectionSearchResult();
mockCollectionWithoutAbstract.hitHighlights = [];
mockCollectionWithoutAbstract.hitHighlights = {};
mockCollectionWithoutAbstract.dspaceObject = Object.assign(new Collection(), {
metadata: [
metadata: {
'dc.title': [
{
key: 'dc.title',
language: 'en_US',
value: 'Test title'
} ]
}
]
}
});
describe('CollectionSearchResultListElementComponent', () => {

View File

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

View File

@@ -16,25 +16,29 @@ const truncatableServiceStub: any = {
};
const mockCommunityWithAbstract: CommunitySearchResult = new CommunitySearchResult();
mockCommunityWithAbstract.hitHighlights = [];
mockCommunityWithAbstract.hitHighlights = {};
mockCommunityWithAbstract.dspaceObject = Object.assign(new Community(), {
metadata: [
metadata: {
'dc.description.abstract': [
{
key: 'dc.description.abstract',
language: 'en_US',
value: 'Short description'
} ]
}
]
}
});
const mockCommunityWithoutAbstract: CommunitySearchResult = new CommunitySearchResult();
mockCommunityWithoutAbstract.hitHighlights = [];
mockCommunityWithoutAbstract.hitHighlights = {};
mockCommunityWithoutAbstract.dspaceObject = Object.assign(new Community(), {
metadata: [
metadata: {
'dc.title': [
{
key: 'dc.title',
language: 'en_US',
value: 'Test title'
} ]
}
]
}
});
describe('CommunitySearchResultListElementComponent', () => {

View File

@@ -1,24 +1,24 @@
<ds-truncatable [id]="dso.id">
<a
[routerLink]="['/items/' + dso.id]" class="lead"
[innerHTML]="getFirstValue('dc.title')"></a>
[innerHTML]="firstMetadataValue('dc.title')"></a>
<span class="text-muted">
<ds-truncatable-part [id]="dso.id" [minLines]="1">
(<span *ngIf="dso.findMetadata('dc.publisher')" class="item-list-publisher"
[innerHTML]="getFirstValue('dc.publisher') + ', '"></span><span
*ngIf="hasValue(dso.findMetadata('dc.date.issued'))" class="item-list-date"
[innerHTML]="getFirstValue('dc.date.issued')"></span>)
<span *ngIf="dso.filterMetadata(['dc.contributor.author', 'dc.creator', 'dc.contributor.*']).length > 0"
(<span *ngIf="dso.hasMetadata('dc.publisher')" class="item-list-publisher"
[innerHTML]="firstMetadataValue('dc.publisher') + ', '"></span><span
*ngIf="dso.hasMetadata('dc.date.issued')" class="item-list-date"
[innerHTML]="firstMetadataValue('dc.date.issued')"></span>)
<span *ngIf="dso.hasMetadata(['dc.contributor.author', 'dc.creator', 'dc.contributor.*'])"
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>
</span>
</ds-truncatable-part>
</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
[innerHTML]="getFirstValue('dc.description.abstract')"></span>
[innerHTML]="firstMetadataValue('dc.description.abstract')"></span>
</ds-truncatable-part>
</div>
</ds-truncatable>

View File

@@ -17,37 +17,43 @@ const truncatableServiceStub: any = {
};
const mockItemWithAuthorAndDate: ItemSearchResult = new ItemSearchResult();
mockItemWithAuthorAndDate.hitHighlights = [];
mockItemWithAuthorAndDate.hitHighlights = {};
mockItemWithAuthorAndDate.dspaceObject = Object.assign(new Item(), {
bitstreams: observableOf({}),
metadata: [
metadata: {
'dc.contributor.author': [
{
key: 'dc.contributor.author',
language: 'en_US',
value: 'Smith, Donald'
},
}
],
'dc.date.issued': [
{
key: 'dc.date.issued',
language: null,
value: '2015-06-26'
}]
}
]
}
});
const mockItemWithoutAuthorAndDate: ItemSearchResult = new ItemSearchResult();
mockItemWithoutAuthorAndDate.hitHighlights = [];
mockItemWithoutAuthorAndDate.hitHighlights = {};
mockItemWithoutAuthorAndDate.dspaceObject = Object.assign(new Item(), {
bitstreams: observableOf({}),
metadata: [
metadata: {
'dc.title': [
{
key: 'dc.title',
language: 'en_US',
value: 'This is just another title'
},
}
],
'dc.type': [
{
key: 'dc.type',
language: null,
value: 'Article'
}]
}
]
}
});
describe('ItemSearchResultListElementComponent', () => {

View File

@@ -3,11 +3,10 @@ import { Observable } from 'rxjs';
import { SearchResult } from '../../../+search-page/search-result.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 { 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';
@Component({
selector: 'ds-search-result-list-element',
@@ -22,39 +21,24 @@ export class SearchResultListElementComponent<T extends SearchResult<K>, K exten
this.dso = this.object.dspaceObject;
}
getValues(keys: string[]): string[] {
const results: string[] = new Array<string>();
this.object.hitHighlights.forEach(
(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;
/**
* Gets all matching metadata string values from hitHighlights or dso metadata, preferring hitHighlights.
*
* @param {string|string[]} keyOrKeys The metadata key(s) in scope. Wildcards are supported; see [[Metadata]].
* @returns {string[]} the matching string values or an empty array.
*/
allMetadataValues(keyOrKeys: string | string[]): string[] {
return Metadata.allValues([this.object.hitHighlights, this.dso.metadata], keyOrKeys);
}
getFirstValue(key: string): string {
let result: string;
this.object.hitHighlights.some(
(md: Metadatum) => {
if (key === md.key) {
result = md.value;
return true;
}
}
);
if (hasNoValue(result)) {
result = this.dso.findMetadata(key);
}
return result;
/**
* Gets the first matching metadata string value from hitHighlights or dso metadata, preferring hitHighlights.
*
* @param {string|string[]} keyOrKeys The metadata key(s) in scope. Wildcards are supported; see [[Metadata]].
* @returns {string} the first matching string value, or `undefined`.
*/
firstMetadataValue(keyOrKeys: string | string[]): string {
return Metadata.firstValue([this.object.hitHighlights, this.dso.metadata], keyOrKeys);
}
isCollapsed(): Observable<boolean> {

View File

@@ -30,6 +30,7 @@ describe('SearchFormComponent', () => {
});
it('should display scopes when available with default and all scopes', () => {
comp.scopes = objects;
fixture.detectChanges();
const select: HTMLElement = de.query(By.css('select')).nativeElement;
@@ -121,33 +122,38 @@ export const objects: DSpaceObject[] = [
id: '7669c72a-3f2a-451f-a3b9-9210e7a4c02f',
uuid: '7669c72a-3f2a-451f-a3b9-9210e7a4c02f',
type: ResourceType.Community,
metadata: [
metadata: {
'dc.description': [
{
key: 'dc.description',
language: null,
value: ''
},
}
],
'dc.description.abstract': [
{
key: 'dc.description.abstract',
language: null,
value: 'This is a test community to hold content for the OR2017 demostration'
},
}
],
'dc.description.tableofcontents': [
{
key: 'dc.description.tableofcontents',
language: null,
value: ''
},
}
],
'dc.rights': [
{
key: 'dc.rights',
language: null,
value: ''
},
}
],
'dc.title': [
{
key: 'dc.title',
language: null,
value: 'OR2017 - Demonstration'
}
]
}
}),
Object.assign(new Community(),
{
@@ -170,33 +176,38 @@ export const objects: DSpaceObject[] = [
id: '9076bd16-e69a-48d6-9e41-0238cb40d863',
uuid: '9076bd16-e69a-48d6-9e41-0238cb40d863',
type: ResourceType.Community,
metadata: [
metadata: {
'dc.description': [
{
key: 'dc.description',
language: null,
value: '<p>This is the introductory text for the <em>Sample Community</em> on the DSpace Demonstration Site. It is editable by System or Community Administrators (of this Community).</p>\r\n<p><strong>DSpace Communities may contain one or more Sub-Communities or Collections (of Items).</strong></p>\r\n<p>This particular Community has its own logo (the <a href=\'http://www.duraspace.org/\'>DuraSpace</a> logo).</p>'
},
}
],
'dc.description.abstract': [
{
key: 'dc.description.abstract',
language: null,
value: 'This is a sample top-level community'
},
}
],
'dc.description.tableofcontents': [
{
key: 'dc.description.tableofcontents',
language: null,
value: '<p>This is the <em>news section</em> for this <em>Sample Community</em>. System or Community Administrators (of this Community) can edit this News field.</p>'
},
}
],
'dc.rights': [
{
key: 'dc.rights',
language: null,
value: '<p><em>If this Community had special copyright text to display, it would be displayed here.</em></p>'
},
}
],
'dc.title': [
{
key: 'dc.title',
language: null,
value: 'Sample Community'
}
]
}
}
)
];