Skip to content

Commit bd203c9

Browse files
AndreaBarbassoAndrea BarbassoAndrea Barbasso
authored
Add lang attribute to elements showing metadata values (#4776)
* [CST-19328] create dsMetadata directive, use it outside item pages * [CST-19328] add lang attribute in metadata-values.component * [CST-19328] add lang attribute in representation components * [CST-16756] create dsNormalizeLanguageCode pipe * [CST-19328] align with main, solve conflicts * [CST-19328] added lang attribute to metadata-link-view component * [CST-19328] handle language on hitHighlights and titles * [CST-19328] add lang attribute to item title --------- Co-authored-by: Andrea Barbasso <´andrea.barbasso@4science.com´> Co-authored-by: Andrea Barbasso <andrea.barbasso@4science.com>
1 parent 3288d84 commit bd203c9

96 files changed

Lines changed: 796 additions & 134 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

src/app/core/breadcrumbs/dso-name.service.spec.ts

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -110,6 +110,38 @@ describe(`DSONameService`, () => {
110110
});
111111
});
112112

113+
describe(`getNameLanguage`, () => {
114+
it(`should use the OrgUnit language factory for OrgUnit entities`, () => {
115+
const orgUnit = Object.assign(new DSpaceObject(), {
116+
firstMetadata(keyOrKeys: string | string[]): { language: string } {
117+
return { language: 'it' };
118+
},
119+
getRenderTypes(): (string | GenericConstructor<ListableObject>)[] {
120+
return ['OrgUnit', Item, DSpaceObject];
121+
},
122+
});
123+
124+
const result = service.getNameLanguage(orgUnit);
125+
126+
expect(result).toBe('it');
127+
});
128+
129+
it(`should use the Default language factory for regular DSpaceObjects`, () => {
130+
const dso = Object.assign(new DSpaceObject(), {
131+
firstMetadata(keyOrKeys: string | string[]): { language: string } {
132+
return { language: 'en' };
133+
},
134+
getRenderTypes(): (string | GenericConstructor<ListableObject>)[] {
135+
return [DSpaceObject];
136+
},
137+
});
138+
139+
const result = service.getNameLanguage(dso);
140+
141+
expect(result).toBe('en');
142+
});
143+
});
144+
113145
describe(`factories.Person`, () => {
114146
describe(`with person.familyName and person.givenName`, () => {
115147
beforeEach(() => {

src/app/core/breadcrumbs/dso-name.service.ts

Lines changed: 67 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import { Injectable } from '@angular/core';
2+
import { MetadataValue } from '@dspace/core/shared/metadata.models';
23
import {
34
hasValue,
45
isEmpty,
@@ -62,6 +63,15 @@ export class DSONameService {
6263
},
6364
};
6465

66+
private readonly languageFactories = {
67+
OrgUnit: (dso: DSpaceObject): string => {
68+
return dso.firstMetadata('organization.legalName')?.language;
69+
},
70+
Default: (dso: DSpaceObject): string => {
71+
return dso.firstMetadata('dc.title')?.language;
72+
},
73+
};
74+
6575
/**
6676
* Get the name for the given {@link DSpaceObject}
6777
*
@@ -88,6 +98,35 @@ export class DSONameService {
8898
}
8999
}
90100

101+
/**
102+
* Retrieves the language identifier associated with a DSpaceObject's primary display name.
103+
*
104+
* Uses a type-based factory pattern to determine the appropriate language extraction strategy
105+
* based on the object's render types. Currently, supports OrgUnit-specific language extraction,
106+
* with a fallback to the Default factory for all other entity types.
107+
*
108+
* @param dso The {@link DSpaceObject} from which to extract the name language. Can be undefined.
109+
* @returns The language code/identifier of the primary display name metadata,
110+
* or undefined if the DSpaceObject is null/undefined or lacks language metadata.
111+
*/
112+
getNameLanguage(dso: DSpaceObject | undefined): string {
113+
if (dso) {
114+
const types = dso.getRenderTypes();
115+
const match = types
116+
.filter((type) => typeof type === 'string')
117+
.find((type: string) => Object.keys(this.languageFactories).includes(type)) as string;
118+
119+
let language: string;
120+
if (hasValue(match)) {
121+
language = this.languageFactories[match](dso);
122+
}
123+
if (isEmpty(language)) {
124+
language = this.languageFactories.Default(dso);
125+
}
126+
return language;
127+
}
128+
}
129+
91130
/**
92131
* Gets the Hit highlight
93132
*
@@ -97,24 +136,29 @@ export class DSONameService {
97136
*
98137
* @returns {string} html embedded hit highlight.
99138
*/
100-
getHitHighlights(object: any, dso: DSpaceObject, escapeHTML?: boolean): string {
139+
getHitHighlights(object: any, dso: DSpaceObject, escapeHTML?: boolean): MetadataValue {
101140
const types = dso.getRenderTypes();
102141
const entityType = types
103142
.filter((type) => typeof type === 'string')
104143
.find((type: string) => (['Person', 'OrgUnit']).includes(type)) as string;
105144
if (entityType === 'Person') {
106-
const familyName = this.firstMetadataValue(object, dso, 'person.familyName', escapeHTML);
107-
const givenName = this.firstMetadataValue(object, dso, 'person.givenName', escapeHTML);
108-
if (isEmpty(familyName) && isEmpty(givenName)) {
109-
return this.firstMetadataValue(object, dso, 'dc.title', escapeHTML) || dso.name;
110-
} else if (isEmpty(familyName) || isEmpty(givenName)) {
145+
const familyName = this.firstMetadata(object, dso, 'person.familyName', escapeHTML);
146+
const givenName = this.firstMetadata(object, dso, 'person.givenName', escapeHTML);
147+
if (isEmpty(familyName?.value) && isEmpty(givenName?.value)) {
148+
return this.firstMetadata(object, dso, 'dc.title', escapeHTML) ||
149+
(dso.name && Object.assign(new MetadataValue(), { value: dso.name })) ||
150+
Object.assign(new MetadataValue(), { value: this.translateService.instant('person.listelement.no-title') });
151+
} else if (isEmpty(familyName?.value) || isEmpty(givenName?.value)) {
111152
return familyName || givenName;
112153
}
113-
return `${familyName}, ${givenName}`;
154+
return Object.assign(new MetadataValue(), { value: `${familyName.value}, ${givenName.value}` });
114155
} else if (entityType === 'OrgUnit') {
115-
return this.firstMetadataValue(object, dso, 'organization.legalName', escapeHTML);
156+
return this.firstMetadata(object, dso, 'organization.legalName', escapeHTML) ||
157+
Object.assign(new MetadataValue(), { value: this.translateService.instant('orgunit.listelement.no-title') });
116158
}
117-
return this.firstMetadataValue(object, dso, 'dc.title', escapeHTML) || dso.name || this.translateService.instant('dso.name.untitled');
159+
return this.firstMetadata(object, dso, 'dc.title', escapeHTML) ||
160+
(dso.name && Object.assign(new MetadataValue(), { value: dso.name })) ||
161+
Object.assign(new MetadataValue(), { value: this.translateService.instant('dso.name.untitled') });
118162
}
119163

120164
/**
@@ -131,4 +175,18 @@ export class DSONameService {
131175
return Metadata.firstValue(dso.metadata, keyOrKeys, object.hitHighlights, undefined, escapeHTML);
132176
}
133177

178+
/**
179+
* Gets the first matching metadata from hitHighlights or dso metadata, preferring hitHighlights.
180+
*
181+
* @param object
182+
* @param dso
183+
* @param {string|string[]} keyOrKeys The metadata key(s) in scope. Wildcards are supported; see [[Metadata]].
184+
* @param escapeHTML Whether the HTML is used inside a `[innerHTML]` attribute
185+
*
186+
* @returns {string} the first matching metadata, or `undefined`.
187+
*/
188+
firstMetadata(object: any, dso: DSpaceObject, keyOrKeys: string | string[], escapeHTML?: boolean): MetadataValue {
189+
return Metadata.first(dso.metadata, keyOrKeys, object.hitHighlights, undefined, escapeHTML);
190+
}
191+
134192
}

src/app/core/shared/metadata-representation/item/item-metadata-representation.model.ts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,4 +41,11 @@ export class ItemMetadataRepresentation extends Item implements MetadataRepresen
4141
return this.virtualMetadata.value;
4242
}
4343

44+
/**
45+
* Get the language of the value to display
46+
*/
47+
getLanguage(): string {
48+
return this.virtualMetadata.language || null;
49+
}
50+
4451
}

src/app/core/shared/metadata-representation/metadata-representation.model.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,4 +37,9 @@ export interface MetadataRepresentation {
3737
*/
3838
getValue(): string;
3939

40+
/**
41+
* Fetches the language of the metadata
42+
*/
43+
getLanguage(): string;
44+
4045
}

src/app/core/shared/metadata-representation/metadatum/metadatum-representation.model.ts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -48,4 +48,11 @@ export class MetadatumRepresentation extends MetadataValue implements MetadataRe
4848
return this.value;
4949
}
5050

51+
/**
52+
* Get the value language
53+
*/
54+
getLanguage(): string {
55+
return this.language || null;
56+
}
57+
5158
}

src/app/core/shared/metadata.utils.ts

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -55,7 +55,10 @@ export class Metadata {
5555
if (hitHighlights[mdKey]) {
5656
for (const candidate of hitHighlights[mdKey]) {
5757
if (Metadata.valueMatches(candidate as MetadataValue, filter) && (isEmpty(limit) || (hasValue(limit) && matches.length < limit))) {
58-
matches.push(candidate as MetadataValue);
58+
const nonHighlightValues = metadata[mdKey] as MetadataValue[];
59+
const nonHighlightValue = nonHighlightValues?.find((value: MetadataValue) => Metadata.valueMatches(value, filter));
60+
const language = nonHighlightValue?.language ?? candidate.language ?? null;
61+
matches.push(Object.assign(new MetadataValue(), candidate, { language }));
5962
}
6063
}
6164
}
@@ -111,7 +114,13 @@ export class Metadata {
111114
for (const key of Metadata.resolveKeys(hitHighlights, keyOrKeys)) {
112115
const values: MetadataValue[] = hitHighlights[key] as MetadataValue[];
113116
if (values) {
114-
return values.find((value: MetadataValue) => Metadata.valueMatches(value, filter));
117+
const metadataValue = values.find((value: MetadataValue) => Metadata.valueMatches(value, filter));
118+
if (metadataValue) {
119+
const nonHighlightValues = metadata[key] as MetadataValue[];
120+
const nonHighlightValue = nonHighlightValues?.find((value: MetadataValue) => Metadata.valueMatches(value, filter));
121+
const language = nonHighlightValue?.language ?? metadataValue.language ?? null;
122+
return Object.assign(new MetadataValue(), metadataValue, { language });
123+
}
115124
}
116125
}
117126
}
Lines changed: 20 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import { DSpaceObject } from '../shared/dspace-object.model';
2+
import { MetadataValue } from '../shared/metadata.models';
23

34
export const UNDEFINED_NAME = 'Undefined';
45

@@ -7,22 +8,34 @@ export class DSONameServiceMock {
78
return dso?.name || UNDEFINED_NAME;
89
}
910

10-
public getHitHighlights(object: any, dso: DSpaceObject) {
11+
public getHitHighlights(object: any, dso: DSpaceObject): MetadataValue {
1112
if (object.hitHighlights && object.hitHighlights['dc.title']) {
12-
return object.hitHighlights['dc.title'][0].value;
13+
return Object.assign(new MetadataValue(), {
14+
value: object.hitHighlights['dc.title'][0].value,
15+
});
1316
} else if (object.hitHighlights && object.hitHighlights['organization.legalName']) {
14-
return object.hitHighlights['organization.legalName'][0].value;
17+
return Object.assign(new MetadataValue(), {
18+
value: object.hitHighlights['organization.legalName'][0].value,
19+
});
1520
} else if (object.hitHighlights && (object.hitHighlights['person.familyName'] || object.hitHighlights['person.givenName'])) {
1621
if (object.hitHighlights['person.familyName'] && object.hitHighlights['person.givenName']) {
17-
return `${object.hitHighlights['person.familyName'][0].value}, ${object.hitHighlights['person.givenName'][0].value}`;
22+
return Object.assign(new MetadataValue(), {
23+
value: `${object.hitHighlights['person.familyName'][0].value}, ${object.hitHighlights['person.givenName'][0].value}`,
24+
});
1825
}
1926
if (object.hitHighlights['person.familyName']) {
20-
return `${object.hitHighlights['person.familyName'][0].value}`;
27+
return Object.assign(new MetadataValue(), {
28+
value: `${object.hitHighlights['person.familyName'][0].value}`,
29+
});
2130
}
2231
if (object.hitHighlights['person.givenName']) {
23-
return `${object.hitHighlights['person.givenName'][0].value}`;
32+
return Object.assign(new MetadataValue(), {
33+
value: `${object.hitHighlights['person.givenName'][0].value}`,
34+
});
2435
}
2536
}
26-
return UNDEFINED_NAME;
37+
return Object.assign(new MetadataValue(), {
38+
value: UNDEFINED_NAME,
39+
});
2740
}
2841
}

src/app/entity-groups/journal-entities/item-grid-elements/search-result-grid-elements/journal-issue/journal-issue-search-result-grid-element.component.html

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -27,20 +27,20 @@
2727
<ds-badges [object]="dso" [context]="context"></ds-badges>
2828
}
2929
<ds-truncatable-part [id]="dso.id" [minLines]="3" type="h4">
30-
<h4 class="card-title" [innerHTML]="dsoTitle"></h4>
30+
<h4 class="card-title" [dsMetadata]="dsoTitle"></h4>
3131
</ds-truncatable-part>
3232
@if (dso.hasMetadata('creativework.datePublished')) {
3333
<p
3434
class="item-date card-text text-muted">
3535
<ds-truncatable-part [id]="dso.id" [minLines]="1">
36-
<span [innerHTML]="firstMetadataValue('creativework.datePublished')"></span>
36+
<span [dsMetadata]="firstMetadata('creativework.datePublished')"></span>
3737
</ds-truncatable-part>
3838
</p>
3939
}
4040
@if (dso.hasMetadata('journal.title')) {
4141
<p class="item-journal-title card-text">
4242
<ds-truncatable-part [id]="dso.id" [minLines]="3">
43-
<span [innerHTML]="firstMetadataValue('journal.title')"></span>
43+
<span [dsMetadata]="firstMetadata('journal.title')"></span>
4444
</ds-truncatable-part>
4545
</p>
4646
}

src/app/entity-groups/journal-entities/item-grid-elements/search-result-grid-elements/journal-issue/journal-issue-search-result-grid-element.component.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import { ViewMode } from '@dspace/core/shared/view-mode.model';
55
import { TranslateModule } from '@ngx-translate/core';
66

77
import { focusShadow } from '../../../../../shared/animations/focus';
8+
import { MetadataDirective } from '../../../../../shared/metadata.directive';
89
import { ThemedBadgesComponent } from '../../../../../shared/object-collection/shared/badges/themed-badges.component';
910
import { listableObjectComponent } from '../../../../../shared/object-collection/shared/listable-object/listable-object.decorator';
1011
import { ItemSearchResultGridElementComponent } from '../../../../../shared/object-grid/search-result-grid-element/item-search-result/item/item-search-result-grid-element.component';
@@ -20,6 +21,7 @@ import { ThemedThumbnailComponent } from '../../../../../thumbnail/themed-thumbn
2021
animations: [focusShadow],
2122
imports: [
2223
AsyncPipe,
24+
MetadataDirective,
2325
RouterLink,
2426
ThemedBadgesComponent,
2527
ThemedThumbnailComponent,

src/app/entity-groups/journal-entities/item-grid-elements/search-result-grid-elements/journal-volume/journal-volume-search-result-grid-element.component.html

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -27,20 +27,20 @@
2727
<ds-badges [object]="dso" [context]="context"></ds-badges>
2828
}
2929
<ds-truncatable-part [id]="dso.id" [minLines]="3" type="h4">
30-
<h4 class="card-title" [innerHTML]="dsoTitle"></h4>
30+
<h4 class="card-title" [dsMetadata]="dsoTitle"></h4>
3131
</ds-truncatable-part>
3232
@if (dso.hasMetadata('creativework.datePublished')) {
3333
<p
3434
class="item-date card-text text-muted">
3535
<ds-truncatable-part [id]="dso.id" [minLines]="1">
36-
<span [innerHTML]="firstMetadataValue('creativework.datePublished')"></span>
36+
<span [dsMetadata]="firstMetadata('creativework.datePublished')"></span>
3737
</ds-truncatable-part>
3838
</p>
3939
}
4040
@if (dso.hasMetadata('dc.description')) {
4141
<p class="item-description card-text">
4242
<ds-truncatable-part [id]="dso.id" [minLines]="3">
43-
<span [innerHTML]="firstMetadataValue('dc.description')"></span>
43+
<span [dsMetadata]="firstMetadata('dc.description')"></span>
4444
</ds-truncatable-part>
4545
</p>
4646
}

0 commit comments

Comments
 (0)