diff --git a/src/app/collection-page/collection-item-mapper/collection-item-mapper.component.ts b/src/app/collection-page/collection-item-mapper/collection-item-mapper.component.ts index 745e59eb03a..33825e6a71e 100644 --- a/src/app/collection-page/collection-item-mapper/collection-item-mapper.component.ts +++ b/src/app/collection-page/collection-item-mapper/collection-item-mapper.component.ts @@ -159,7 +159,7 @@ export class CollectionItemMapperComponent implements OnInit { this.collectionName$ = this.collectionRD$.pipe( map((rd: RemoteData) => { - return this.dsoNameService.getName(rd.payload); + return this.dsoNameService.getName(rd.payload, true); }), ); this.searchOptions$ = this.searchConfigService.paginatedSearchOptions; diff --git a/src/app/core/breadcrumbs/dso-name.service.spec.ts b/src/app/core/breadcrumbs/dso-name.service.spec.ts index 5f241b1a6cc..4bdd36a0c89 100644 --- a/src/app/core/breadcrumbs/dso-name.service.spec.ts +++ b/src/app/core/breadcrumbs/dso-name.service.spec.ts @@ -78,7 +78,7 @@ describe(`DSONameService`, () => { const result = service.getName(mockPerson); - expect((service as any).factories.Person).toHaveBeenCalledWith(mockPerson); + expect((service as any).factories.Person).toHaveBeenCalledWith(mockPerson, undefined); expect(result).toBe('Bingo!'); }); @@ -87,7 +87,7 @@ describe(`DSONameService`, () => { const result = service.getName(mockOrgUnit); - expect((service as any).factories.OrgUnit).toHaveBeenCalledWith(mockOrgUnit); + expect((service as any).factories.OrgUnit).toHaveBeenCalledWith(mockOrgUnit, undefined); expect(result).toBe('Bingo!'); }); @@ -96,7 +96,7 @@ describe(`DSONameService`, () => { const result = service.getName(mockEPerson); - expect((service as any).factories.EPerson).toHaveBeenCalledWith(mockEPerson); + expect((service as any).factories.EPerson).toHaveBeenCalledWith(mockEPerson, undefined); expect(result).toBe('Bingo!'); }); @@ -105,7 +105,7 @@ describe(`DSONameService`, () => { const result = service.getName(mockDSO); - expect((service as any).factories.Default).toHaveBeenCalledWith(mockDSO); + expect((service as any).factories.Default).toHaveBeenCalledWith(mockDSO, undefined); expect(result).toBe('Bingo!'); }); }); @@ -119,9 +119,9 @@ describe(`DSONameService`, () => { it(`should return 'person.familyName, person.givenName'`, () => { const result = (service as any).factories.Person(mockPerson); expect(result).toBe(mockPersonName); - expect(mockPerson.firstMetadataValue).toHaveBeenCalledWith('person.familyName'); - expect(mockPerson.firstMetadataValue).toHaveBeenCalledWith('person.givenName'); - expect(mockPerson.firstMetadataValue).not.toHaveBeenCalledWith('dc.title'); + expect(mockPerson.firstMetadataValue).toHaveBeenCalledWith('person.familyName', undefined, undefined); + expect(mockPerson.firstMetadataValue).toHaveBeenCalledWith('person.givenName', undefined, undefined); + expect(mockPerson.firstMetadataValue).not.toHaveBeenCalledWith('dc.title', undefined, undefined); }); }); @@ -133,9 +133,9 @@ describe(`DSONameService`, () => { it(`should return dc.title`, () => { const result = (service as any).factories.Person(mockPerson); expect(result).toBe(mockPersonName); - expect(mockPerson.firstMetadataValue).toHaveBeenCalledWith('person.familyName'); - expect(mockPerson.firstMetadataValue).toHaveBeenCalledWith('person.givenName'); - expect(mockPerson.firstMetadataValue).toHaveBeenCalledWith('dc.title'); + expect(mockPerson.firstMetadataValue).toHaveBeenCalledWith('person.familyName', undefined, undefined); + expect(mockPerson.firstMetadataValue).toHaveBeenCalledWith('person.givenName', undefined, undefined); + expect(mockPerson.firstMetadataValue).toHaveBeenCalledWith('dc.title', undefined, undefined); }); }); }); @@ -149,8 +149,8 @@ describe(`DSONameService`, () => { it(`should return 'eperson.firstname' and 'eperson.lastname'`, () => { const result = (service as any).factories.EPerson(mockEPerson); expect(result).toBe(mockEPersonName); - expect(mockEPerson.firstMetadataValue).toHaveBeenCalledWith('eperson.firstname'); - expect(mockEPerson.firstMetadataValue).toHaveBeenCalledWith('eperson.lastname'); + expect(mockEPerson.firstMetadataValue).toHaveBeenCalledWith('eperson.firstname', undefined, undefined); + expect(mockEPerson.firstMetadataValue).toHaveBeenCalledWith('eperson.lastname', undefined, undefined); }); }); @@ -162,8 +162,8 @@ describe(`DSONameService`, () => { it(`should return 'eperson.firstname'`, () => { const result = (service as any).factories.EPerson(mockEPersonFirst); expect(result).toBe(mockEPersonNameFirst); - expect(mockEPersonFirst.firstMetadataValue).toHaveBeenCalledWith('eperson.firstname'); - expect(mockEPersonFirst.firstMetadataValue).toHaveBeenCalledWith('eperson.lastname'); + expect(mockEPersonFirst.firstMetadataValue).toHaveBeenCalledWith('eperson.firstname', undefined, undefined); + expect(mockEPersonFirst.firstMetadataValue).toHaveBeenCalledWith('eperson.lastname', undefined, undefined); }); }); }); @@ -177,7 +177,7 @@ describe(`DSONameService`, () => { it(`should return 'organization.legalName'`, () => { const result = (service as any).factories.OrgUnit(mockOrgUnit); expect(result).toBe(mockOrgUnitName); - expect(mockOrgUnit.firstMetadataValue).toHaveBeenCalledWith('organization.legalName'); + expect(mockOrgUnit.firstMetadataValue).toHaveBeenCalledWith('organization.legalName', undefined, undefined); }); }); @@ -189,7 +189,7 @@ describe(`DSONameService`, () => { it(`should return 'dc.title'`, () => { const result = (service as any).factories.Default(mockDSO); expect(result).toBe(mockDSOName); - expect(mockDSO.firstMetadataValue).toHaveBeenCalledWith('dc.title'); + expect(mockDSO.firstMetadataValue).toHaveBeenCalledWith('dc.title', undefined, undefined); }); }); }); diff --git a/src/app/core/breadcrumbs/dso-name.service.ts b/src/app/core/breadcrumbs/dso-name.service.ts index 988141209f4..b7daa8dd4e2 100644 --- a/src/app/core/breadcrumbs/dso-name.service.ts +++ b/src/app/core/breadcrumbs/dso-name.service.ts @@ -31,9 +31,9 @@ export class DSONameService { * With only two exceptions those solutions seem overkill for now. */ private readonly factories = { - EPerson: (dso: DSpaceObject): string => { - const firstName = dso.firstMetadataValue('eperson.firstname'); - const lastName = dso.firstMetadataValue('eperson.lastname'); + EPerson: (dso: DSpaceObject, escapeHTML?: boolean): string => { + const firstName = dso.firstMetadataValue('eperson.firstname', undefined, escapeHTML); + const lastName = dso.firstMetadataValue('eperson.lastname', undefined, escapeHTML); if (isEmpty(firstName) && isEmpty(lastName)) { return this.translateService.instant('dso.name.unnamed'); } else if (isEmpty(firstName) || isEmpty(lastName)) { @@ -42,23 +42,23 @@ export class DSONameService { return `${firstName} ${lastName}`; } }, - Person: (dso: DSpaceObject): string => { - const familyName = dso.firstMetadataValue('person.familyName'); - const givenName = dso.firstMetadataValue('person.givenName'); + Person: (dso: DSpaceObject, escapeHTML?: boolean): string => { + const familyName = dso.firstMetadataValue('person.familyName', undefined, escapeHTML); + const givenName = dso.firstMetadataValue('person.givenName', undefined, escapeHTML); if (isEmpty(familyName) && isEmpty(givenName)) { - return dso.firstMetadataValue('dc.title') || this.translateService.instant('dso.name.unnamed'); + return dso.firstMetadataValue('dc.title', undefined, escapeHTML) || this.translateService.instant('dso.name.unnamed'); } else if (isEmpty(familyName) || isEmpty(givenName)) { return familyName || givenName; } else { return `${familyName}, ${givenName}`; } }, - OrgUnit: (dso: DSpaceObject): string => { - return dso.firstMetadataValue('organization.legalName') || this.translateService.instant('dso.name.untitled'); + OrgUnit: (dso: DSpaceObject, escapeHTML?: boolean): string => { + return dso.firstMetadataValue('organization.legalName', undefined, escapeHTML); }, - Default: (dso: DSpaceObject): string => { + Default: (dso: DSpaceObject, escapeHTML?: boolean): string => { // If object doesn't have dc.title metadata use name property - return dso.firstMetadataValue('dc.title') || dso.name || this.translateService.instant('dso.name.untitled'); + return dso.firstMetadataValue('dc.title', undefined, escapeHTML) || dso.name || this.translateService.instant('dso.name.untitled'); }, }; @@ -66,8 +66,9 @@ export class DSONameService { * Get the name for the given {@link DSpaceObject} * * @param dso The {@link DSpaceObject} you want a name for + * @param escapeHTML Whether the HTML is used inside a `[innerHTML]` attribute */ - getName(dso: DSpaceObject | undefined): string { + getName(dso: DSpaceObject | undefined, escapeHTML?: boolean): string { if (dso) { const types = dso.getRenderTypes(); const match = types @@ -76,10 +77,10 @@ export class DSONameService { let name; if (hasValue(match)) { - name = this.factories[match](dso); + name = this.factories[match](dso, escapeHTML); } if (isEmpty(name)) { - name = this.factories.Default(dso); + name = this.factories.Default(dso, escapeHTML); } return name; } else { @@ -92,27 +93,28 @@ export class DSONameService { * * @param object * @param dso + * @param escapeHTML Whether the HTML is used inside a `[innerHTML]` attribute * * @returns {string} html embedded hit highlight. */ - getHitHighlights(object: any, dso: DSpaceObject): string { + getHitHighlights(object: any, dso: DSpaceObject, escapeHTML?: boolean): string { const types = dso.getRenderTypes(); const entityType = types .filter((type) => typeof type === 'string') .find((type: string) => (['Person', 'OrgUnit']).includes(type)) as string; if (entityType === 'Person') { - const familyName = this.firstMetadataValue(object, dso, 'person.familyName'); - const givenName = this.firstMetadataValue(object, dso, 'person.givenName'); + const familyName = this.firstMetadataValue(object, dso, 'person.familyName', escapeHTML); + const givenName = this.firstMetadataValue(object, dso, 'person.givenName', escapeHTML); if (isEmpty(familyName) && isEmpty(givenName)) { - return this.firstMetadataValue(object, dso, 'dc.title') || dso.name; + return this.firstMetadataValue(object, dso, 'dc.title', escapeHTML) || dso.name; } else if (isEmpty(familyName) || isEmpty(givenName)) { return familyName || givenName; } return `${familyName}, ${givenName}`; } else if (entityType === 'OrgUnit') { - return this.firstMetadataValue(object, dso, 'organization.legalName') || this.translateService.instant('dso.name.untitled'); + return this.firstMetadataValue(object, dso, 'organization.legalName', escapeHTML); } - return this.firstMetadataValue(object, dso, 'dc.title') || dso.name || this.translateService.instant('dso.name.untitled'); + return this.firstMetadataValue(object, dso, 'dc.title', escapeHTML) || dso.name || this.translateService.instant('dso.name.untitled'); } /** @@ -121,11 +123,12 @@ export class DSONameService { * @param object * @param dso * @param {string|string[]} keyOrKeys The metadata key(s) in scope. Wildcards are supported; see [[Metadata]]. + * @param escapeHTML Whether the HTML is used inside a `[innerHTML]` attribute * * @returns {string} the first matching string value, or `undefined`. */ - firstMetadataValue(object: any, dso: DSpaceObject, keyOrKeys: string | string[]): string { - return Metadata.firstValue([object.hitHighlights, dso.metadata], keyOrKeys); + firstMetadataValue(object: any, dso: DSpaceObject, keyOrKeys: string | string[], escapeHTML?: boolean): string { + return Metadata.firstValue(dso.metadata, keyOrKeys, object.hitHighlights, undefined, escapeHTML); } } diff --git a/src/app/core/shared/dspace-object.model.ts b/src/app/core/shared/dspace-object.model.ts index 7bc05b1d3aa..6ed3ea9105a 100644 --- a/src/app/core/shared/dspace-object.model.ts +++ b/src/app/core/shared/dspace-object.model.ts @@ -118,33 +118,36 @@ export class DSpaceObject extends ListableObject implements CacheableObject { * Gets all matching metadata 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. + * @param {MetadataValueFilter} valueFilter The value filter to use. If unspecified, no filtering will be done. + * @param escapeHTML Whether the HTML is used inside a `[innerHTML]` attribute * @returns {MetadataValue[]} the matching values or an empty array. */ - allMetadata(keyOrKeys: string | string[], valueFilter?: MetadataValueFilter): MetadataValue[] { - return Metadata.all(this.metadata, keyOrKeys, valueFilter); + allMetadata(keyOrKeys: string | string[], valueFilter?: MetadataValueFilter, escapeHTML?: boolean): MetadataValue[] { + return Metadata.all(this.metadata, keyOrKeys, undefined, valueFilter, escapeHTML); } /** * Like [[allMetadata]], but only returns string values. * * @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. + * @param {MetadataValueFilter} valueFilter The value filter to use. If unspecified, no filtering will be done. + * @param escapeHTML Whether the HTML is used inside a `[innerHTML]` attribute * @returns {string[]} the matching string values or an empty array. */ - allMetadataValues(keyOrKeys: string | string[], valueFilter?: MetadataValueFilter): string[] { - return Metadata.allValues(this.metadata, keyOrKeys, valueFilter); + allMetadataValues(keyOrKeys: string | string[], valueFilter?: MetadataValueFilter, escapeHTML?: boolean): string[] { + return Metadata.allValues(this.metadata, keyOrKeys, undefined, valueFilter, escapeHTML); } /** * 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. + * @param {MetadataValueFilter} valueFilter The value filter to use. If unspecified, no filtering will be done. + * @param escapeHTML Whether the HTML is used inside a `[innerHTML]` attribute * @returns {MetadataValue} the first matching value, or `undefined`. */ - firstMetadata(keyOrKeys: string | string[], valueFilter?: MetadataValueFilter): MetadataValue { - return Metadata.first(this.metadata, keyOrKeys, valueFilter); + firstMetadata(keyOrKeys: string | string[], valueFilter?: MetadataValueFilter, escapeHTML?: boolean): MetadataValue { + return Metadata.first(this.metadata, keyOrKeys, undefined, valueFilter, escapeHTML); } /** @@ -152,26 +155,27 @@ export class DSpaceObject extends ListableObject implements CacheableObject { * * @param {string|string[]} keyOrKeys The metadata key(s) in scope. Wildcards are supported; see [[Metadata]]. * @param {MetadataValueFilter} valueFilter The value filter to use. If unspecified, no filtering will be done. + * @param escapeHTML Whether the HTML is used inside a `[innerHTML]` attribute * @returns {string} the first matching string value, or `undefined`. */ - firstMetadataValue(keyOrKeys: string | string[], valueFilter?: MetadataValueFilter): string { - return Metadata.firstValue(this.metadata, keyOrKeys, valueFilter); + firstMetadataValue(keyOrKeys: string | string[], valueFilter?: MetadataValueFilter, escapeHTML?: boolean): string { + return Metadata.firstValue(this.metadata, keyOrKeys, undefined, valueFilter, escapeHTML); } /** * 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. + * @param {MetadataValueFilter} valueFilter 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); + return Metadata.has(this.metadata, keyOrKeys, undefined, valueFilter); } /** * Find metadata on a specific field and order all of them using their "place" property. - * @param key + * @param keyOrKeys The metadata key(s) in scope. Wildcards are supported; see [[Metadata]]. */ findMetadataSortedByPlace(keyOrKeys: string | string[]): MetadataValue[] { return this.allMetadata(keyOrKeys).sort((a: MetadataValue, b: MetadataValue) => { diff --git a/src/app/core/shared/metadata.utils.spec.ts b/src/app/core/shared/metadata.utils.spec.ts index 2ba96201b02..55fbbc78d5f 100644 --- a/src/app/core/shared/metadata.utils.spec.ts +++ b/src/app/core/shared/metadata.utils.spec.ts @@ -50,11 +50,11 @@ const multiViewModelList = [ { key: 'foo', ...bar, order: 0 }, ]; -const testMethod = (fn, resultKind, mapOrMaps, keyOrKeys, expected, filter?) => { +const testMethod = (fn, resultKind, mapOrMaps, keyOrKeys, hitHighlights, 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); + const result = fn(mapOrMaps, keys, hitHighlights, filter); let shouldReturn; if (resultKind === 'boolean') { shouldReturn = expected; @@ -76,107 +76,107 @@ describe('Metadata', () => { describe('all method', () => { - const testAll = (mapOrMaps, keyOrKeys, expected, filter?: MetadataValueFilter) => - testMethod(Metadata.all, 'value', mapOrMaps, keyOrKeys, expected, filter); + const testAll = (mapOrMaps, keyOrKeys, hitHighlights, expected, filter?: MetadataValueFilter) => + testMethod(Metadata.all, 'value', mapOrMaps, keyOrKeys, hitHighlights, expected, filter); describe('with emptyMap', () => { - testAll({}, 'foo', []); - testAll({}, '*', []); + testAll({}, 'foo', undefined, []); + testAll({}, '*', undefined, []); }); describe('with singleMap', () => { - testAll(singleMap, 'foo', []); - testAll(singleMap, '*', [dcTitle0]); - testAll(singleMap, '*', [], { value: 'baz' }); - testAll(singleMap, 'dc.title', [dcTitle0]); - testAll(singleMap, 'dc.*', [dcTitle0]); + testAll(singleMap, 'foo', undefined, []); + testAll(singleMap, '*', undefined, [dcTitle0]); + testAll(singleMap, '*', undefined, [], { value: 'baz' }); + testAll(singleMap, 'dc.title', undefined, [dcTitle0]); + testAll(singleMap, 'dc.*', undefined, [dcTitle0]); }); describe('with multiMap', () => { - testAll(multiMap, 'foo', [bar]); - testAll(multiMap, '*', [dcDescription, dcAbstract, dcTitle1, dcTitle2, bar]); - testAll(multiMap, 'dc.title', [dcTitle1, dcTitle2]); - testAll(multiMap, 'dc.*', [dcDescription, dcAbstract, dcTitle1, dcTitle2]); - testAll(multiMap, ['dc.title', 'dc.*'], [dcTitle1, dcTitle2, dcDescription, dcAbstract]); + testAll(multiMap, 'foo', undefined, [bar]); + testAll(multiMap, '*', undefined, [dcDescription, dcAbstract, dcTitle1, dcTitle2, bar]); + testAll(multiMap, 'dc.title', undefined, [dcTitle1, dcTitle2]); + testAll(multiMap, 'dc.*', undefined, [dcDescription, dcAbstract, dcTitle1, dcTitle2]); + testAll(multiMap, ['dc.title', 'dc.*'], undefined, [dcTitle1, dcTitle2, dcDescription, dcAbstract]); }); describe('with [ singleMap, multiMap ]', () => { - testAll([singleMap, multiMap], 'foo', [bar]); - testAll([singleMap, multiMap], '*', [dcTitle0]); - testAll([singleMap, multiMap], 'dc.title', [dcTitle0]); - testAll([singleMap, multiMap], 'dc.*', [dcTitle0]); + testAll(multiMap, 'foo', singleMap, [bar]); + testAll(multiMap, '*', singleMap, [dcTitle0]); + testAll(multiMap, 'dc.title', singleMap, [dcTitle0]); + testAll(multiMap, 'dc.*', singleMap, [dcTitle0]); }); describe('with [ multiMap, singleMap ]', () => { - testAll([multiMap, singleMap], 'foo', [bar]); - testAll([multiMap, singleMap], '*', [dcDescription, dcAbstract, dcTitle1, dcTitle2, bar]); - testAll([multiMap, singleMap], 'dc.title', [dcTitle1, dcTitle2]); - testAll([multiMap, singleMap], 'dc.*', [dcDescription, dcAbstract, dcTitle1, dcTitle2]); - testAll([multiMap, singleMap], ['dc.title', 'dc.*'], [dcTitle1, dcTitle2, dcDescription, dcAbstract]); + testAll(singleMap, 'foo', multiMap, [bar]); + testAll(singleMap, '*', multiMap, [dcDescription, dcAbstract, dcTitle1, dcTitle2, bar]); + testAll(singleMap, 'dc.title', multiMap, [dcTitle1, dcTitle2]); + testAll(singleMap, 'dc.*', multiMap, [dcDescription, dcAbstract, dcTitle1, dcTitle2]); + testAll(singleMap, ['dc.title', 'dc.*'], multiMap, [dcTitle1, dcTitle2, dcDescription, dcAbstract]); }); describe('with regexTestMap', () => { - testAll(regexTestMap, 'foo.bar.*', []); + testAll(regexTestMap, 'foo.bar.*', undefined, []); }); }); describe('allValues method', () => { - const testAllValues = (mapOrMaps, keyOrKeys, expected) => - testMethod(Metadata.allValues, 'string', mapOrMaps, keyOrKeys, expected); + const testAllValues = (mapOrMaps, keyOrKeys, hitHighlights, expected) => + testMethod(Metadata.allValues, 'string', mapOrMaps, keyOrKeys, hitHighlights, expected); describe('with emptyMap', () => { - testAllValues({}, '*', []); + testAllValues({}, '*', undefined, []); }); describe('with singleMap', () => { - testAllValues([singleMap, multiMap], '*', [dcTitle0.value]); + testAllValues(multiMap, '*', singleMap, [dcTitle0.value]); }); describe('with [ multiMap, singleMap ]', () => { - testAllValues([multiMap, singleMap], '*', [dcDescription.value, dcAbstract.value, dcTitle1.value, dcTitle2.value, bar.value]); + testAllValues(singleMap, '*', multiMap, [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); + const testFirst = (mapOrMaps, keyOrKeys, hitHighlights, expected) => + testMethod(Metadata.first, 'value', mapOrMaps, keyOrKeys, hitHighlights, expected); describe('with emptyMap', () => { - testFirst({}, '*', undefined); + testFirst({}, '*', undefined, undefined); }); describe('with singleMap', () => { - testFirst(singleMap, '*', dcTitle0); + testFirst(singleMap, '*', undefined, dcTitle0); }); describe('with [ multiMap, singleMap ]', () => { - testFirst([multiMap, singleMap], '*', dcDescription); + testFirst(singleMap, '*', multiMap, dcDescription); }); }); describe('firstValue method', () => { - const testFirstValue = (mapOrMaps, keyOrKeys, expected) => - testMethod(Metadata.firstValue, 'value', mapOrMaps, keyOrKeys, expected); + const testFirstValue = (mapOrMaps, keyOrKeys, hitHighlights, expected) => + testMethod(Metadata.firstValue, 'value', mapOrMaps, keyOrKeys, hitHighlights, expected); describe('with emptyMap', () => { - testFirstValue({}, '*', undefined); + testFirstValue({}, '*', undefined, undefined); }); describe('with singleMap', () => { - testFirstValue(singleMap, '*', dcTitle0.value); + testFirstValue(singleMap, '*', undefined, dcTitle0.value); }); describe('with [ multiMap, singleMap ]', () => { - testFirstValue([multiMap, singleMap], '*', dcDescription.value); + testFirstValue(singleMap, '*', multiMap, dcDescription.value); }); }); describe('has method', () => { - const testHas = (mapOrMaps, keyOrKeys, expected, filter?: MetadataValueFilter) => - testMethod(Metadata.has, 'boolean', mapOrMaps, keyOrKeys, expected, filter); + const testHas = (mapOrMaps, keyOrKeys, hitHighlights, expected, filter?: MetadataValueFilter) => + testMethod(Metadata.has, 'boolean', mapOrMaps, keyOrKeys, hitHighlights, expected, filter); describe('with emptyMap', () => { - testHas({}, '*', false); + testHas({}, '*', undefined, false); }); describe('with singleMap', () => { - testHas(singleMap, '*', true); - testHas(singleMap, '*', false, { value: 'baz' }); + testHas(singleMap, '*', undefined, true); + testHas(singleMap, '*', undefined, false, { value: 'baz' }); }); describe('with [ multiMap, singleMap ]', () => { - testHas([multiMap, singleMap], '*', true); + testHas(singleMap, '*', multiMap, true); }); }); diff --git a/src/app/core/shared/metadata.utils.ts b/src/app/core/shared/metadata.utils.ts index a82f218ed94..915ead48dd1 100644 --- a/src/app/core/shared/metadata.utils.ts +++ b/src/app/core/shared/metadata.utils.ts @@ -1,8 +1,8 @@ +import escape from 'lodash/escape'; import groupBy from 'lodash/groupBy'; import sortBy from 'lodash/sortBy'; import { - isEmpty, isNotEmpty, isNotUndefined, isUndefined, @@ -32,94 +32,120 @@ export class Metadata { /** * Gets all matching metadata in the map(s). * - * @param {MetadataMapInterface|MetadataMapInterface[]} 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 metadata The metadata values. * @param {string|string[]} keyOrKeys The metadata key(s) in scope. Wildcards are supported; see above. + * @param hitHighlights The search hit highlights. * @param {MetadataValueFilter} filter The value filter to use. If unspecified, no filtering will be done. + * @param escapeHTML Whether the HTML is used inside a `[innerHTML]` attribute * @returns {MetadataValue[]} the matching values or an empty array. */ - public static all(mapOrMaps: MetadataMapInterface | MetadataMapInterface[], keyOrKeys: string | string[], - filter?: MetadataValueFilter): MetadataValue[] { - const mdMaps: MetadataMapInterface[] = mapOrMaps instanceof Array ? mapOrMaps : [mapOrMaps]; + public static all(metadata: MetadataMapInterface, keyOrKeys: string | string[], hitHighlights?: MetadataMapInterface, filter?: MetadataValueFilter, escapeHTML?: boolean): MetadataValue[] { 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 (isNotEmpty(hitHighlights)) { + for (const mdKey of Metadata.resolveKeys(hitHighlights, keyOrKeys)) { + if (hitHighlights[mdKey]) { + for (const candidate of hitHighlights[mdKey]) { if (Metadata.valueMatches(candidate as MetadataValue, filter)) { matches.push(candidate as MetadataValue); } } } } - if (!isEmpty(matches)) { + if (isNotEmpty(matches)) { return matches; } } + for (const mdKey of Metadata.resolveKeys(metadata, keyOrKeys)) { + if (metadata[mdKey]) { + for (const candidate of metadata[mdKey]) { + if (Metadata.valueMatches(candidate as MetadataValue, filter)) { + if (escapeHTML) { + matches.push(Object.assign(new MetadataValue(), candidate, { + value: escape(candidate.value), + })); + } else { + matches.push(candidate as MetadataValue); + } + } + } + } + } return matches; } /** * Like [[Metadata.all]], but only returns string values. * - * @param {MetadataMapInterface|MetadataMapInterface[]} 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 metadata The metadata values. * @param {string|string[]} keyOrKeys The metadata key(s) in scope. Wildcards are supported; see above. + * @param hitHighlights The search hit highlights. * @param {MetadataValueFilter} filter The value filter to use. If unspecified, no filtering will be done. + * @param escapeHTML Whether the HTML is used inside a `[innerHTML]` attribute * @returns {string[]} the matching string values or an empty array. */ - public static allValues(mapOrMaps: MetadataMapInterface | MetadataMapInterface[], keyOrKeys: string | string[], - filter?: MetadataValueFilter): string[] { - return Metadata.all(mapOrMaps, keyOrKeys, filter).map((mdValue) => mdValue.value); + public static allValues(metadata: MetadataMapInterface, keyOrKeys: string | string[], hitHighlights?: MetadataMapInterface, filter?: MetadataValueFilter, escapeHTML?: boolean): string[] { + return Metadata.all(metadata, keyOrKeys, hitHighlights, filter, escapeHTML).map((mdValue) => mdValue.value); } /** * Gets the first matching MetadataValue object in the map(s), or `undefined`. * - * @param {MetadataMapInterface|MetadataMapInterface[]} mdMapOrMaps The source map(s). + * @param metadata The metadata values. * @param {string|string[]} keyOrKeys The metadata key(s) in scope. Wildcards are supported; see above. + * @param hitHighlights The search hit highlights. * @param {MetadataValueFilter} filter The value filter to use. If unspecified, no filtering will be done. + * @param escapeHTML Whether the HTML is used inside a `[innerHTML]` attribute * @returns {MetadataValue} the first matching value, or `undefined`. */ - public static first(mdMapOrMaps: MetadataMapInterface | MetadataMapInterface[], keyOrKeys: string | string[], - filter?: MetadataValueFilter): MetadataValue { - const mdMaps: MetadataMapInterface[] = mdMapOrMaps instanceof Array ? mdMapOrMaps : [mdMapOrMaps]; - for (const mdMap of mdMaps) { - for (const key of Metadata.resolveKeys(mdMap, keyOrKeys)) { - const values: MetadataValue[] = mdMap[key] as MetadataValue[]; + public static first(metadata: MetadataMapInterface, keyOrKeys: string | string[], hitHighlights?: MetadataMapInterface, filter?: MetadataValueFilter, escapeHTML?: boolean): MetadataValue { + if (isNotEmpty(hitHighlights)) { + for (const key of Metadata.resolveKeys(hitHighlights, keyOrKeys)) { + const values: MetadataValue[] = hitHighlights[key] as MetadataValue[]; if (values) { return values.find((value: MetadataValue) => Metadata.valueMatches(value, filter)); } } } + for (const key of Metadata.resolveKeys(metadata, keyOrKeys)) { + const values: MetadataValue[] = metadata[key] as MetadataValue[]; + if (values) { + const result: MetadataValue = values.find((value: MetadataValue) => Metadata.valueMatches(value, filter)); + if (escapeHTML) { + return Object.assign(new MetadataValue(), result, { + value: escape(result.value), + }); + } + return result; + } + } } /** * Like [[Metadata.first]], but only returns a string value, or `undefined`. * - * @param {MetadataMapInterface|MetadataMapInterface[]} mdMapOrMaps The source map(s). + * @param metadata The metadata values. * @param {string|string[]} keyOrKeys The metadata key(s) in scope. Wildcards are supported; see above. + * @param hitHighlights The search hit highlights. * @param {MetadataValueFilter} filter The value filter to use. If unspecified, no filtering will be done. + * @param escapeHTML Whether the HTML is used inside a `[innerHTML]` attribute * @returns {string} the first matching string value, or `undefined`. */ - public static firstValue(mdMapOrMaps: MetadataMapInterface | MetadataMapInterface[], keyOrKeys: string | string[], - filter?: MetadataValueFilter): string { - const value = Metadata.first(mdMapOrMaps, keyOrKeys, filter); + public static firstValue(metadata: MetadataMapInterface, keyOrKeys: string | string[], hitHighlights?: MetadataMapInterface, filter?: MetadataValueFilter, escapeHTML?: boolean): string { + const value = Metadata.first(metadata, keyOrKeys, hitHighlights, filter, escapeHTML); return isUndefined(value) ? undefined : value.value; } /** * Checks for a matching metadata value in the given map(s). * - * @param {MetadataMapInterface|MetadataMapInterface[]} mdMapOrMaps The source map(s). + * @param metadata The metadata values. * @param {string|string[]} keyOrKeys The metadata key(s) in scope. Wildcards are supported; see above. + * @param hitHighlights The search hit highlights. * @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: MetadataMapInterface | MetadataMapInterface[], keyOrKeys: string | string[], - filter?: MetadataValueFilter): boolean { - return isNotUndefined(Metadata.first(mdMapOrMaps, keyOrKeys, filter)); + public static has(metadata: MetadataMapInterface, keyOrKeys: string | string[], hitHighlights?: MetadataMapInterface, filter?: MetadataValueFilter): boolean { + return isNotUndefined(Metadata.first(metadata, keyOrKeys, hitHighlights, filter)); } /** diff --git a/src/app/entity-groups/research-entities/metadata-representations/org-unit/org-unit-item-metadata-list-element.component.html b/src/app/entity-groups/research-entities/metadata-representations/org-unit/org-unit-item-metadata-list-element.component.html index ec4dbd43236..c661c55bceb 100644 --- a/src/app/entity-groups/research-entities/metadata-representations/org-unit/org-unit-item-metadata-list-element.component.html +++ b/src/app/entity-groups/research-entities/metadata-representations/org-unit/org-unit-item-metadata-list-element.component.html @@ -1,12 +1,12 @@ - + + [ngbTooltip]="mdRepresentation.hasMetadata(['dc.description']) ? descTemplate : null"> diff --git a/src/app/entity-groups/research-entities/metadata-representations/person/person-item-metadata-list-element.component.html b/src/app/entity-groups/research-entities/metadata-representations/person/person-item-metadata-list-element.component.html index 9c09c78c111..1983de20b6e 100644 --- a/src/app/entity-groups/research-entities/metadata-representations/person/person-item-metadata-list-element.component.html +++ b/src/app/entity-groups/research-entities/metadata-representations/person/person-item-metadata-list-element.component.html @@ -3,7 +3,7 @@ @if (mdRepresentation.allMetadata(['person.jobTitle']).length > 0) { - @for (value of mdRepresentation.allMetadataValues(['person.jobTitle']); track value; let last = $last) { + @for (value of mdRepresentation.allMetadataValues(['person.jobTitle'], undefined, true); track value; let last = $last) { @if (!last) { ; diff --git a/src/app/entity-groups/research-entities/metadata-representations/project/project-item-metadata-list-element.component.html b/src/app/entity-groups/research-entities/metadata-representations/project/project-item-metadata-list-element.component.html index acc9173bf7d..4c1f9266d6b 100644 --- a/src/app/entity-groups/research-entities/metadata-representations/project/project-item-metadata-list-element.component.html +++ b/src/app/entity-groups/research-entities/metadata-representations/project/project-item-metadata-list-element.component.html @@ -1,12 +1,12 @@ - + - \ No newline at end of file + [innerHTML]="dsoNameService.getName(mdRepresentation, true)" + [ngbTooltip]="dsoNameService.getName(mdRepresentation, true).length > 0 ? descTemplate : null"> + diff --git a/src/app/item-page/edit-item-page/item-collection-mapper/item-collection-mapper.component.ts b/src/app/item-page/edit-item-page/item-collection-mapper/item-collection-mapper.component.ts index c5fa717ceb8..1fafc9df8c2 100644 --- a/src/app/item-page/edit-item-page/item-collection-mapper/item-collection-mapper.component.ts +++ b/src/app/item-page/edit-item-page/item-collection-mapper/item-collection-mapper.component.ts @@ -146,7 +146,7 @@ export class ItemCollectionMapperComponent implements OnInit { this.itemName$ = this.itemRD$.pipe( filter((rd: RemoteData) => hasValue(rd)), map((rd: RemoteData) => { - return this.dsoNameService.getName(rd.payload); + return this.dsoNameService.getName(rd.payload, true); }), ); this.searchOptions$ = this.searchConfigService.paginatedSearchOptions; diff --git a/src/app/shared/object-detail/my-dspace-result-detail-element/item-detail-preview/item-detail-preview-field/item-detail-preview-field.component.ts b/src/app/shared/object-detail/my-dspace-result-detail-element/item-detail-preview/item-detail-preview-field/item-detail-preview-field.component.ts index 2f78f778f8d..09580b5e66c 100644 --- a/src/app/shared/object-detail/my-dspace-result-detail-element/item-detail-preview/item-detail-preview-field/item-detail-preview-field.component.ts +++ b/src/app/shared/object-detail/my-dspace-result-detail-element/item-detail-preview/item-detail-preview-field/item-detail-preview-field.component.ts @@ -44,6 +44,11 @@ export class ItemDetailPreviewFieldComponent { */ @Input() metadata: string | string[]; + /** + * Escape HTML in the metadata value + */ + @Input() escapeMetadataHTML: boolean; + /** * The placeholder if there are no value to show */ @@ -61,6 +66,6 @@ export class ItemDetailPreviewFieldComponent { * @returns {string[]} the matching string values or an empty array. */ allMetadataValues(keyOrKeys: string | string[]): string[] { - return Metadata.allValues([this.object.hitHighlights, this.item.metadata], keyOrKeys); + return Metadata.allValues(this.item.metadata, keyOrKeys, this.object.hitHighlights, undefined, this.escapeMetadataHTML); } } diff --git a/src/app/shared/object-detail/my-dspace-result-detail-element/item-detail-preview/item-detail-preview-field/themed-item-detail-preview-field.component.ts b/src/app/shared/object-detail/my-dspace-result-detail-element/item-detail-preview/item-detail-preview-field/themed-item-detail-preview-field.component.ts index 7daf5cac938..d1e18c88f88 100644 --- a/src/app/shared/object-detail/my-dspace-result-detail-element/item-detail-preview/item-detail-preview-field/themed-item-detail-preview-field.component.ts +++ b/src/app/shared/object-detail/my-dspace-result-detail-element/item-detail-preview/item-detail-preview-field/themed-item-detail-preview-field.component.ts @@ -28,6 +28,7 @@ export class ThemedItemDetailPreviewFieldComponent extends ThemedComponent diff --git a/src/app/shared/object-detail/my-dspace-result-detail-element/search-result-detail-element.component.ts b/src/app/shared/object-detail/my-dspace-result-detail-element/search-result-detail-element.component.ts index 5dcac201890..bbe6a9ac270 100644 --- a/src/app/shared/object-detail/my-dspace-result-detail-element/search-result-detail-element.component.ts +++ b/src/app/shared/object-detail/my-dspace-result-detail-element/search-result-detail-element.component.ts @@ -37,19 +37,21 @@ export class SearchResultDetailElementComponent, K ext * 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]]. + * @param escapeHTML Whether the HTML is used inside a `[innerHTML]` attribute * @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); + allMetadataValues(keyOrKeys: string | string[], escapeHTML = true): string[] { + return Metadata.allValues(this.dso.metadata, keyOrKeys, this.object.hitHighlights, undefined, escapeHTML); } /** * 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]]. + * @param escapeHTML Whether the HTML is used inside a `[innerHTML]` attribute * @returns {string} the first matching string value, or `undefined`. */ - firstMetadataValue(keyOrKeys: string | string[]): string { - return Metadata.firstValue([this.object.hitHighlights, this.dso.metadata], keyOrKeys); + firstMetadataValue(keyOrKeys: string | string[], escapeHTML = true): string { + return Metadata.firstValue(this.dso.metadata, keyOrKeys, this.object.hitHighlights, undefined, escapeHTML); } } diff --git a/src/app/shared/object-grid/search-result-grid-element/item-search-result/item/item-search-result-grid-element.component.spec.ts b/src/app/shared/object-grid/search-result-grid-element/item-search-result/item/item-search-result-grid-element.component.spec.ts index 86a44640829..6f93429a7f4 100644 --- a/src/app/shared/object-grid/search-result-grid-element/item-search-result/item/item-search-result-grid-element.component.spec.ts +++ b/src/app/shared/object-grid/search-result-grid-element/item-search-result/item/item-search-result-grid-element.component.spec.ts @@ -28,6 +28,7 @@ import { RemoteData } from '../../../../../core/data/remote-data'; import { Bitstream } from '../../../../../core/shared/bitstream.model'; import { HALEndpointService } from '../../../../../core/shared/hal-endpoint.service'; import { Item } from '../../../../../core/shared/item.model'; +import { MetadataValue } from '../../../../../core/shared/metadata.models'; import { PageInfo } from '../../../../../core/shared/page-info.model'; import { UUIDService } from '../../../../../core/shared/uuid.service'; import { ThemedThumbnailComponent } from '../../../../../thumbnail/themed-thumbnail.component'; @@ -43,20 +44,21 @@ import { TruncatePipe } from '../../../../utils/truncate.pipe'; import { ItemSearchResultGridElementComponent } from './item-search-result-grid-element.component'; const mockItemWithMetadata: ItemSearchResult = new ItemSearchResult(); -mockItemWithMetadata.hitHighlights = {}; const dcTitle = 'This is just another title'; -mockItemWithMetadata.indexableObject = Object.assign(new Item(), { - hitHighlights: { - 'dc.title': [{ +mockItemWithMetadata.hitHighlights = { + 'dc.title': [ + Object.assign(new MetadataValue(), { value: dcTitle, - }], - }, + }), + ], +}; +mockItemWithMetadata.indexableObject = Object.assign(new Item(), { bundles: createSuccessfulRemoteDataObject$(buildPaginatedList(new PageInfo(), [])), metadata: { 'dc.title': [ { language: 'en_US', - value: dcTitle, + value: 'This is just another title', }, ], 'dc.contributor.author': [ diff --git a/src/app/shared/object-grid/search-result-grid-element/item-search-result/item/item-search-result-grid-element.component.ts b/src/app/shared/object-grid/search-result-grid-element/item-search-result/item/item-search-result-grid-element.component.ts index 22df63e22bf..e33a528ea37 100644 --- a/src/app/shared/object-grid/search-result-grid-element/item-search-result/item/item-search-result-grid-element.component.ts +++ b/src/app/shared/object-grid/search-result-grid-element/item-search-result/item/item-search-result-grid-element.component.ts @@ -61,6 +61,6 @@ export class ItemSearchResultGridElementComponent extends SearchResultGridElemen ngOnInit(): void { super.ngOnInit(); this.itemPageRoute = getItemPageRoute(this.dso); - this.dsoTitle = this.dsoNameService.getHitHighlights(this.object, this.dso); + this.dsoTitle = this.dsoNameService.getHitHighlights(this.object, this.dso, true); } } diff --git a/src/app/shared/object-grid/search-result-grid-element/search-result-grid-element.component.ts b/src/app/shared/object-grid/search-result-grid-element/search-result-grid-element.component.ts index e8b999fb9fc..7824e759f82 100644 --- a/src/app/shared/object-grid/search-result-grid-element/search-result-grid-element.component.ts +++ b/src/app/shared/object-grid/search-result-grid-element/search-result-grid-element.component.ts @@ -51,20 +51,22 @@ export class SearchResultGridElementComponent, K exten * 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]]. + * @param escapeHTML Whether the HTML is used inside a `[innerHTML]` attribute * @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); + allMetadataValues(keyOrKeys: string | string[], escapeHTML = true): string[] { + return Metadata.allValues(this.dso.metadata, keyOrKeys, this.object.hitHighlights, undefined, escapeHTML); } /** * 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]]. + * @param escapeHTML Whether the HTML is used inside a `[innerHTML]` attribute * @returns {string} the first matching string value, or `undefined`. */ - firstMetadataValue(keyOrKeys: string | string[]): string { - return Metadata.firstValue([this.object.hitHighlights, this.dso.metadata], keyOrKeys); + firstMetadataValue(keyOrKeys: string | string[], escapeHTML = true): string { + return Metadata.firstValue(this.dso.metadata, keyOrKeys, this.object.hitHighlights, undefined, escapeHTML); } private isCollapsed(): Observable { diff --git a/src/app/shared/object-list/my-dspace-result-list-element/item-list-preview/item-list-preview.component.html b/src/app/shared/object-list/my-dspace-result-list-element/item-list-preview/item-list-preview.component.html index b005af46ec3..2c9c7e44bd4 100644 --- a/src/app/shared/object-list/my-dspace-result-list-element/item-list-preview/item-list-preview.component.html +++ b/src/app/shared/object-list/my-dspace-result-list-element/item-list-preview/item-list-preview.component.html @@ -12,16 +12,16 @@ -

+

(@if (item.hasMetadata('dc.publisher')) { + [innerHTML]="item.firstMetadataValue('dc.publisher', undefined, true) + ', '"> } ) + [innerHTML]="item.firstMetadataValue('dc.date.issued', undefined, true) || ('mydspace.results.no-date' | translate)">) @if (item.hasMetadata(['dc.contributor.author', 'dc.creator', 'dc.contributor.*']);) { @@ -30,7 +30,7 @@

@@ -44,8 +44,8 @@

- +

diff --git a/src/app/shared/object-list/my-dspace-result-list-element/item-list-preview/item-list-preview.component.ts b/src/app/shared/object-list/my-dspace-result-list-element/item-list-preview/item-list-preview.component.ts index 9ffb1000b6b..aca51947e21 100644 --- a/src/app/shared/object-list/my-dspace-result-list-element/item-list-preview/item-list-preview.component.ts +++ b/src/app/shared/object-list/my-dspace-result-list-element/item-list-preview/item-list-preview.component.ts @@ -90,7 +90,7 @@ export class ItemListPreviewComponent implements OnInit { ngOnInit(): void { this.showThumbnails = this.appConfig.browseBy.showThumbnails; - this.dsoTitle = this.dsoNameService.getHitHighlights(this.object, this.item); + this.dsoTitle = this.dsoNameService.getHitHighlights(this.object, this.item, true); } diff --git a/src/app/shared/object-list/search-result-list-element/item-search-result/item-types/item/item-search-result-list-element.component.html b/src/app/shared/object-list/search-result-list-element/item-search-result/item-types/item/item-search-result-list-element.component.html index 53ef93bc54c..ba12d01189a 100644 --- a/src/app/shared/object-list/search-result-list-element/item-search-result/item-types/item/item-search-result-list-element.component.html +++ b/src/app/shared/object-list/search-result-list-element/item-search-result/item-types/item/item-search-result-list-element.component.html @@ -36,18 +36,18 @@ } - @if (dso.firstMetadataValue('dc.publisher') || dso.firstMetadataValue('dc.date.issued')) { - (@if (dso.firstMetadataValue('dc.publisher')) { + @if (firstMetadataValue('dc.publisher') || firstMetadataValue('dc.date.issued')) { + (@if (firstMetadataValue('dc.publisher')) { } - @if (dso.firstMetadataValue('dc.publisher') && dso.firstMetadataValue('dc.date.issued')) { + @if (firstMetadataValue('dc.publisher') && firstMetadataValue('dc.date.issued')) { , } - @if (dso.firstMetadataValue('dc.date.issued')) { + @if (firstMetadataValue('dc.date.issued')) { }) } - @if (dso.allMetadata(['dc.contributor.author', 'dc.creator', 'dc.contributor.*']).length > 0) { + @if (allMetadataValues(['dc.contributor.author', 'dc.creator', 'dc.contributor.*']).length > 0) { @for (author of allMetadataValues(['dc.contributor.author', 'dc.creator', 'dc.contributor.*']); track author; let last = $last) { @@ -61,10 +61,10 @@ } - @if (dso.firstMetadataValue('dc.description.abstract')) { + @if (firstMetadataValue('dc.description.abstract'); as abstract) {
+ [innerHTML]="abstract">
} diff --git a/src/app/shared/object-list/search-result-list-element/search-result-list-element.component.ts b/src/app/shared/object-list/search-result-list-element/search-result-list-element.component.ts index c4251c3597f..d45eea80827 100644 --- a/src/app/shared/object-list/search-result-list-element/search-result-list-element.component.ts +++ b/src/app/shared/object-list/search-result-list-element/search-result-list-element.component.ts @@ -41,7 +41,7 @@ export class SearchResultListElementComponent, K exten ngOnInit(): void { if (hasValue(this.object)) { this.dso = this.object.indexableObject; - this.dsoTitle = this.dsoNameService.getHitHighlights(this.object, this.dso); + this.dsoTitle = this.dsoNameService.getHitHighlights(this.object, this.dso, true); } } @@ -49,11 +49,13 @@ export class SearchResultListElementComponent, K exten * Gets all matching metadata string values from hitHighlights or dso metadata. * * @param {string|string[]} keyOrKeys The metadata key(s) in scope. Wildcards are supported; see [[Metadata]]. + * @param escapeHTML Whether the HTML is used inside a `[innerHTML]` attribute. Defaults to `true` because we + * always use `[innerHTML]` in the templates to render metadata due to the hit highlights. * @returns {string[]} the matching string values or an empty array. */ - allMetadataValues(keyOrKeys: string | string[]): string[] { - const dsoMetadata: string[] = Metadata.allValues([this.dso.metadata], keyOrKeys); - const highlights: string[] = Metadata.allValues([this.object.hitHighlights], keyOrKeys); + allMetadataValues(keyOrKeys: string | string[], escapeHTML = true): string[] { + const dsoMetadata: string[] = Metadata.allValues(this.dso.metadata, keyOrKeys, undefined, undefined, escapeHTML); + const highlights: string[] = Metadata.allValues({}, keyOrKeys, this.object.hitHighlights, undefined, escapeHTML); const removedHighlights: string[] = highlights.map(str => str.replace(/<\/?em>/g, '')); for (let i = 0; i < removedHighlights.length; i++) { const index = dsoMetadata.indexOf(removedHighlights[i]); @@ -68,10 +70,12 @@ export class SearchResultListElementComponent, K exten * 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]]. + * @param escapeHTML Whether the HTML is used inside a `[innerHTML]` attribute. Defaults to `true` because we + * always use `[innerHTML]` in the templates to render metadata due to the hit highlights. * @returns {string} the first matching string value, or `undefined`. */ - firstMetadataValue(keyOrKeys: string | string[]): string { - return Metadata.firstValue([this.object.hitHighlights, this.dso.metadata], keyOrKeys); + firstMetadataValue(keyOrKeys: string | string[], escapeHTML = true): string { + return Metadata.firstValue(this.dso.metadata, keyOrKeys, this.object.hitHighlights, undefined, escapeHTML); } /** diff --git a/src/app/shared/object-list/sidebar-search-list-element/sidebar-search-list-element.component.ts b/src/app/shared/object-list/sidebar-search-list-element/sidebar-search-list-element.component.ts index 9f7b1aadd25..00ee2f438ee 100644 --- a/src/app/shared/object-list/sidebar-search-list-element/sidebar-search-list-element.component.ts +++ b/src/app/shared/object-list/sidebar-search-list-element/sidebar-search-list-element.component.ts @@ -91,7 +91,7 @@ export class SidebarSearchListElementComponent, K exte getParentTitle(): Observable { return this.getParent().pipe( map((parentRD: RemoteData) => { - return hasValue(parentRD) && hasValue(parentRD.payload) ? this.dsoNameService.getName(parentRD.payload) : undefined; + return hasValue(parentRD) && hasValue(parentRD.payload) ? this.dsoNameService.getName(parentRD.payload, true) : undefined; }), ); }