From f9c54e591e9b1158e208ad3784624998242e02d4 Mon Sep 17 00:00:00 2001 From: Andrea Barbasso <´andrea.barbasso@4science.com´> Date: Wed, 15 Oct 2025 16:18:55 +0200 Subject: [PATCH 1/4] [CST-19328] create dsMetadata directive, use it outside item pages --- ...-search-result-grid-element.component.html | 4 +- ...ue-search-result-grid-element.component.ts | 2 + ...-search-result-grid-element.component.html | 4 +- ...me-search-result-grid-element.component.ts | 2 + ...-search-result-grid-element.component.html | 6 +- ...al-search-result-grid-element.component.ts | 2 + ...-search-result-list-element.component.html | 8 +- ...ue-search-result-list-element.component.ts | 2 + ...-search-result-list-element.component.html | 8 +- ...me-search-result-list-element.component.ts | 2 + ...-search-result-list-element.component.html | 4 +- ...al-search-result-list-element.component.ts | 2 + ...-search-result-grid-element.component.html | 7 +- ...it-search-result-grid-element.component.ts | 2 + ...-search-result-grid-element.component.html | 4 +- ...on-search-result-grid-element.component.ts | 2 + ...-search-result-grid-element.component.html | 2 +- ...ct-search-result-grid-element.component.ts | 2 + ...-search-result-list-element.component.html | 2 +- ...it-search-result-list-element.component.ts | 2 + ...-search-result-list-element.component.html | 4 +- ...on-search-result-list-element.component.ts | 2 + ...-item-metadata-list-element.component.html | 2 +- ...it-item-metadata-list-element.component.ts | 2 + ...-item-metadata-list-element.component.html | 4 +- ...on-item-metadata-list-element.component.ts | 2 + ...ult-list-submission-element.component.html | 4 +- ...esult-list-submission-element.component.ts | 2 + ...ult-list-submission-element.component.html | 4 +- ...esult-list-submission-element.component.ts | 2 + .../full-file-section.component.html | 6 +- .../full-file-section.component.ts | 2 + src/app/shared/metadata.directive.spec.ts | 92 +++++++++++++++++++ src/app/shared/metadata.directive.ts | 44 +++++++++ .../item-detail-preview-field.component.html | 15 ++- .../item-detail-preview-field.component.ts | 24 ++++- .../search-result-detail-element.component.ts | 30 ++++++ ...-search-result-grid-element.component.html | 10 +- ...em-search-result-grid-element.component.ts | 2 + .../search-result-grid-element.component.ts | 30 ++++++ .../item-list-preview.component.html | 30 +++--- .../item-list-preview.component.ts | 2 + ...-search-result-list-element.component.html | 2 +- ...on-search-result-list-element.component.ts | 2 + ...-search-result-list-element.component.html | 2 +- ...ty-search-result-list-element.component.ts | 2 + ...-search-result-list-element.component.html | 12 +-- ...em-search-result-list-element.component.ts | 2 + .../search-result-list-element.component.ts | 30 ++++++ .../item-select/item-select.component.html | 8 +- .../item-select/item-select.component.ts | 2 + .../full-file-section.component.ts | 2 + .../item-detail-preview-field.component.ts | 2 + .../item-list-preview.component.ts | 2 + ...em-search-result-list-element.component.ts | 2 + 55 files changed, 379 insertions(+), 75 deletions(-) create mode 100644 src/app/shared/metadata.directive.spec.ts create mode 100644 src/app/shared/metadata.directive.ts diff --git a/src/app/entity-groups/journal-entities/item-grid-elements/search-result-grid-elements/journal-issue/journal-issue-search-result-grid-element.component.html b/src/app/entity-groups/journal-entities/item-grid-elements/search-result-grid-elements/journal-issue/journal-issue-search-result-grid-element.component.html index bc688e90922..c04657887e2 100644 --- a/src/app/entity-groups/journal-entities/item-grid-elements/search-result-grid-elements/journal-issue/journal-issue-search-result-grid-element.component.html +++ b/src/app/entity-groups/journal-entities/item-grid-elements/search-result-grid-elements/journal-issue/journal-issue-search-result-grid-element.component.html @@ -33,14 +33,14 @@

- +

} @if (dso.hasMetadata('journal.title')) {

- +

} diff --git a/src/app/entity-groups/journal-entities/item-grid-elements/search-result-grid-elements/journal-issue/journal-issue-search-result-grid-element.component.ts b/src/app/entity-groups/journal-entities/item-grid-elements/search-result-grid-elements/journal-issue/journal-issue-search-result-grid-element.component.ts index 5b4424821f8..31cd146321e 100644 --- a/src/app/entity-groups/journal-entities/item-grid-elements/search-result-grid-elements/journal-issue/journal-issue-search-result-grid-element.component.ts +++ b/src/app/entity-groups/journal-entities/item-grid-elements/search-result-grid-elements/journal-issue/journal-issue-search-result-grid-element.component.ts @@ -5,6 +5,7 @@ import { TranslateModule } from '@ngx-translate/core'; import { ViewMode } from '../../../../../core/shared/view-mode.model'; import { focusShadow } from '../../../../../shared/animations/focus'; +import { MetadataDirective } from '../../../../../shared/metadata.directive'; import { ThemedBadgesComponent } from '../../../../../shared/object-collection/shared/badges/themed-badges.component'; import { listableObjectComponent } from '../../../../../shared/object-collection/shared/listable-object/listable-object.decorator'; import { ItemSearchResultGridElementComponent } from '../../../../../shared/object-grid/search-result-grid-element/item-search-result/item/item-search-result-grid-element.component'; @@ -21,6 +22,7 @@ import { ThemedThumbnailComponent } from '../../../../../thumbnail/themed-thumbn standalone: true, imports: [ AsyncPipe, + MetadataDirective, RouterLink, ThemedBadgesComponent, ThemedThumbnailComponent, diff --git a/src/app/entity-groups/journal-entities/item-grid-elements/search-result-grid-elements/journal-volume/journal-volume-search-result-grid-element.component.html b/src/app/entity-groups/journal-entities/item-grid-elements/search-result-grid-elements/journal-volume/journal-volume-search-result-grid-element.component.html index 3207942ec2a..48c7f23061f 100644 --- a/src/app/entity-groups/journal-entities/item-grid-elements/search-result-grid-elements/journal-volume/journal-volume-search-result-grid-element.component.html +++ b/src/app/entity-groups/journal-entities/item-grid-elements/search-result-grid-elements/journal-volume/journal-volume-search-result-grid-element.component.html @@ -33,14 +33,14 @@

- +

} @if (dso.hasMetadata('dc.description')) {

- +

} diff --git a/src/app/entity-groups/journal-entities/item-grid-elements/search-result-grid-elements/journal-volume/journal-volume-search-result-grid-element.component.ts b/src/app/entity-groups/journal-entities/item-grid-elements/search-result-grid-elements/journal-volume/journal-volume-search-result-grid-element.component.ts index ea8c755b117..df8f4d7e887 100644 --- a/src/app/entity-groups/journal-entities/item-grid-elements/search-result-grid-elements/journal-volume/journal-volume-search-result-grid-element.component.ts +++ b/src/app/entity-groups/journal-entities/item-grid-elements/search-result-grid-elements/journal-volume/journal-volume-search-result-grid-element.component.ts @@ -5,6 +5,7 @@ import { TranslateModule } from '@ngx-translate/core'; import { ViewMode } from '../../../../../core/shared/view-mode.model'; import { focusShadow } from '../../../../../shared/animations/focus'; +import { MetadataDirective } from '../../../../../shared/metadata.directive'; import { ThemedBadgesComponent } from '../../../../../shared/object-collection/shared/badges/themed-badges.component'; import { listableObjectComponent } from '../../../../../shared/object-collection/shared/listable-object/listable-object.decorator'; import { ItemSearchResultGridElementComponent } from '../../../../../shared/object-grid/search-result-grid-element/item-search-result/item/item-search-result-grid-element.component'; @@ -21,6 +22,7 @@ import { ThemedThumbnailComponent } from '../../../../../thumbnail/themed-thumbn standalone: true, imports: [ AsyncPipe, + MetadataDirective, RouterLink, ThemedBadgesComponent, ThemedThumbnailComponent, diff --git a/src/app/entity-groups/journal-entities/item-grid-elements/search-result-grid-elements/journal/journal-search-result-grid-element.component.html b/src/app/entity-groups/journal-entities/item-grid-elements/search-result-grid-elements/journal/journal-search-result-grid-element.component.html index f9b0a923b3a..90833ad17c9 100644 --- a/src/app/entity-groups/journal-entities/item-grid-elements/search-result-grid-elements/journal/journal-search-result-grid-element.component.html +++ b/src/app/entity-groups/journal-entities/item-grid-elements/search-result-grid-elements/journal/journal-search-result-grid-element.component.html @@ -33,11 +33,11 @@

- {{firstMetadataValue('creativework.editor')}} + @if (dso.hasMetadata('creativework.publisher')) { , - {{firstMetadataValue('creativework.publisher')}} + } @@ -46,7 +46,7 @@

@if (dso.hasMetadata('dc.description')) {

- +

} diff --git a/src/app/entity-groups/journal-entities/item-grid-elements/search-result-grid-elements/journal/journal-search-result-grid-element.component.ts b/src/app/entity-groups/journal-entities/item-grid-elements/search-result-grid-elements/journal/journal-search-result-grid-element.component.ts index a92232586e8..a8b5dba67d6 100644 --- a/src/app/entity-groups/journal-entities/item-grid-elements/search-result-grid-elements/journal/journal-search-result-grid-element.component.ts +++ b/src/app/entity-groups/journal-entities/item-grid-elements/search-result-grid-elements/journal/journal-search-result-grid-element.component.ts @@ -5,6 +5,7 @@ import { TranslateModule } from '@ngx-translate/core'; import { ViewMode } from '../../../../../core/shared/view-mode.model'; import { focusShadow } from '../../../../../shared/animations/focus'; +import { MetadataDirective } from '../../../../../shared/metadata.directive'; import { ThemedBadgesComponent } from '../../../../../shared/object-collection/shared/badges/themed-badges.component'; import { listableObjectComponent } from '../../../../../shared/object-collection/shared/listable-object/listable-object.decorator'; import { ItemSearchResultGridElementComponent } from '../../../../../shared/object-grid/search-result-grid-element/item-search-result/item/item-search-result-grid-element.component'; @@ -21,6 +22,7 @@ import { ThemedThumbnailComponent } from '../../../../../thumbnail/themed-thumbn standalone: true, imports: [ AsyncPipe, + MetadataDirective, RouterLink, ThemedBadgesComponent, ThemedThumbnailComponent, diff --git a/src/app/entity-groups/journal-entities/item-list-elements/search-result-list-elements/journal-issue/journal-issue-search-result-list-element.component.html b/src/app/entity-groups/journal-entities/item-list-elements/search-result-list-elements/journal-issue/journal-issue-search-result-list-element.component.html index baca36538b8..8d30584deeb 100644 --- a/src/app/entity-groups/journal-entities/item-list-elements/search-result-list-elements/journal-issue/journal-issue-search-result-list-element.component.html +++ b/src/app/entity-groups/journal-entities/item-list-elements/search-result-list-elements/journal-issue/journal-issue-search-result-list-element.component.html @@ -38,9 +38,9 @@ @if (dso.allMetadata(['publicationvolume.volumeNumber']).length > 0) { - @for (value of allMetadataValues(['publicationvolume.volumeNumber']); track value; let last = $last) { + @for (value of allMetadata(['publicationvolume.volumeNumber']); track value; let last = $last) { - @if (!last) { + @if (!last) { ; } @@ -48,11 +48,11 @@ @if (dso.allMetadata(['publicationissue.issueNumber']).length > 0) { - @for (value of allMetadataValues(['publicationissue.issueNumber']); track value; let first = $first; let last = $last) { + @for (value of allMetadata(['publicationissue.issueNumber']); track value; let first = $first; let last = $last) { @if (first) { - - }@if (!last) { + }@if (!last) { ; } diff --git a/src/app/entity-groups/journal-entities/item-list-elements/search-result-list-elements/journal-issue/journal-issue-search-result-list-element.component.ts b/src/app/entity-groups/journal-entities/item-list-elements/search-result-list-elements/journal-issue/journal-issue-search-result-list-element.component.ts index 59dc23014d5..76f6df407b6 100644 --- a/src/app/entity-groups/journal-entities/item-list-elements/search-result-list-elements/journal-issue/journal-issue-search-result-list-element.component.ts +++ b/src/app/entity-groups/journal-entities/item-list-elements/search-result-list-elements/journal-issue/journal-issue-search-result-list-element.component.ts @@ -6,6 +6,7 @@ import { Component } from '@angular/core'; import { RouterLink } from '@angular/router'; import { ViewMode } from '../../../../../core/shared/view-mode.model'; +import { MetadataDirective } from '../../../../../shared/metadata.directive'; import { ThemedBadgesComponent } from '../../../../../shared/object-collection/shared/badges/themed-badges.component'; import { listableObjectComponent } from '../../../../../shared/object-collection/shared/listable-object/listable-object.decorator'; import { ItemSearchResultListElementComponent } from '../../../../../shared/object-list/search-result-list-element/item-search-result/item-types/item/item-search-result-list-element.component'; @@ -21,6 +22,7 @@ import { ThemedThumbnailComponent } from '../../../../../thumbnail/themed-thumbn standalone: true, imports: [ AsyncPipe, + MetadataDirective, NgClass, RouterLink, ThemedBadgesComponent, diff --git a/src/app/entity-groups/journal-entities/item-list-elements/search-result-list-elements/journal-volume/journal-volume-search-result-list-element.component.html b/src/app/entity-groups/journal-entities/item-list-elements/search-result-list-elements/journal-volume/journal-volume-search-result-list-element.component.html index 115c1af4494..0727b5fa0f1 100644 --- a/src/app/entity-groups/journal-entities/item-list-elements/search-result-list-elements/journal-volume/journal-volume-search-result-list-element.component.html +++ b/src/app/entity-groups/journal-entities/item-list-elements/search-result-list-elements/journal-volume/journal-volume-search-result-list-element.component.html @@ -38,9 +38,9 @@ @if (dso.allMetadata(['journal.title']).length > 0) { - @for (value of allMetadataValues(['journal.title']); track value; let last = $last) { + @for (value of allMetadata(['journal.title']); track value; let last = $last) { - @if (!last) { + @if (!last) { ; } @@ -50,9 +50,9 @@ @if (dso.allMetadata(['publicationvolume.volumeNumber']).length > 0) { - @for (value of allMetadataValues(['publicationvolume.volumeNumber']); track value; let last = $last) { + @for (value of allMetadata(['publicationvolume.volumeNumber']); track value; let last = $last) { - ()@if (!last) { + ()@if (!last) { ; } diff --git a/src/app/entity-groups/journal-entities/item-list-elements/search-result-list-elements/journal-volume/journal-volume-search-result-list-element.component.ts b/src/app/entity-groups/journal-entities/item-list-elements/search-result-list-elements/journal-volume/journal-volume-search-result-list-element.component.ts index e23fadafa2d..61856845405 100644 --- a/src/app/entity-groups/journal-entities/item-list-elements/search-result-list-elements/journal-volume/journal-volume-search-result-list-element.component.ts +++ b/src/app/entity-groups/journal-entities/item-list-elements/search-result-list-elements/journal-volume/journal-volume-search-result-list-element.component.ts @@ -6,6 +6,7 @@ import { Component } from '@angular/core'; import { RouterLink } from '@angular/router'; import { ViewMode } from '../../../../../core/shared/view-mode.model'; +import { MetadataDirective } from '../../../../../shared/metadata.directive'; import { ThemedBadgesComponent } from '../../../../../shared/object-collection/shared/badges/themed-badges.component'; import { listableObjectComponent } from '../../../../../shared/object-collection/shared/listable-object/listable-object.decorator'; import { ItemSearchResultListElementComponent } from '../../../../../shared/object-list/search-result-list-element/item-search-result/item-types/item/item-search-result-list-element.component'; @@ -21,6 +22,7 @@ import { ThemedThumbnailComponent } from '../../../../../thumbnail/themed-thumbn standalone: true, imports: [ AsyncPipe, + MetadataDirective, NgClass, RouterLink, ThemedBadgesComponent, diff --git a/src/app/entity-groups/journal-entities/item-list-elements/search-result-list-elements/journal/journal-search-result-list-element.component.html b/src/app/entity-groups/journal-entities/item-list-elements/search-result-list-elements/journal/journal-search-result-list-element.component.html index 535e516b582..2a638aab12a 100644 --- a/src/app/entity-groups/journal-entities/item-list-elements/search-result-list-elements/journal/journal-search-result-list-element.component.html +++ b/src/app/entity-groups/journal-entities/item-list-elements/search-result-list-elements/journal/journal-search-result-list-element.component.html @@ -36,9 +36,9 @@ @if (dso.allMetadata(['creativeworkseries.issn']).length > 0) { - @for (value of allMetadataValues(['creativeworkseries.issn']); track value; let last = $last) { + @for (value of allMetadata(['creativeworkseries.issn']); track value; let last = $last) { - @if (!last) { + @if (!last) { ; } diff --git a/src/app/entity-groups/journal-entities/item-list-elements/search-result-list-elements/journal/journal-search-result-list-element.component.ts b/src/app/entity-groups/journal-entities/item-list-elements/search-result-list-elements/journal/journal-search-result-list-element.component.ts index bda7a6ba57a..e05356836f6 100644 --- a/src/app/entity-groups/journal-entities/item-list-elements/search-result-list-elements/journal/journal-search-result-list-element.component.ts +++ b/src/app/entity-groups/journal-entities/item-list-elements/search-result-list-elements/journal/journal-search-result-list-element.component.ts @@ -6,6 +6,7 @@ import { Component } from '@angular/core'; import { RouterLink } from '@angular/router'; import { ViewMode } from '../../../../../core/shared/view-mode.model'; +import { MetadataDirective } from '../../../../../shared/metadata.directive'; import { ThemedBadgesComponent } from '../../../../../shared/object-collection/shared/badges/themed-badges.component'; import { listableObjectComponent } from '../../../../../shared/object-collection/shared/listable-object/listable-object.decorator'; import { ItemSearchResultListElementComponent } from '../../../../../shared/object-list/search-result-list-element/item-search-result/item-types/item/item-search-result-list-element.component'; @@ -21,6 +22,7 @@ import { ThemedThumbnailComponent } from '../../../../../thumbnail/themed-thumbn standalone: true, imports: [ AsyncPipe, + MetadataDirective, NgClass, RouterLink, ThemedBadgesComponent, diff --git a/src/app/entity-groups/research-entities/item-grid-elements/search-result-grid-elements/org-unit/org-unit-search-result-grid-element.component.html b/src/app/entity-groups/research-entities/item-grid-elements/search-result-grid-elements/org-unit/org-unit-search-result-grid-element.component.html index 883995cbe98..616128e9235 100644 --- a/src/app/entity-groups/research-entities/item-grid-elements/search-result-grid-elements/org-unit/org-unit-search-result-grid-element.component.html +++ b/src/app/entity-groups/research-entities/item-grid-elements/search-result-grid-elements/org-unit/org-unit-search-result-grid-element.component.html @@ -33,7 +33,7 @@

- +

} @@ -41,12 +41,11 @@

- {{firstMetadataValue('organization.address.addressCountry')}} + @if (dso.hasMetadata('organization.address.addressLocality')) { , - {{firstMetadataValue('organization.address.addressLocality')}} + } diff --git a/src/app/entity-groups/research-entities/item-grid-elements/search-result-grid-elements/org-unit/org-unit-search-result-grid-element.component.ts b/src/app/entity-groups/research-entities/item-grid-elements/search-result-grid-elements/org-unit/org-unit-search-result-grid-element.component.ts index 41ba821d1de..21815c03b03 100644 --- a/src/app/entity-groups/research-entities/item-grid-elements/search-result-grid-elements/org-unit/org-unit-search-result-grid-element.component.ts +++ b/src/app/entity-groups/research-entities/item-grid-elements/search-result-grid-elements/org-unit/org-unit-search-result-grid-element.component.ts @@ -5,6 +5,7 @@ import { TranslateModule } from '@ngx-translate/core'; import { ViewMode } from '../../../../../core/shared/view-mode.model'; import { focusShadow } from '../../../../../shared/animations/focus'; +import { MetadataDirective } from '../../../../../shared/metadata.directive'; import { ThemedBadgesComponent } from '../../../../../shared/object-collection/shared/badges/themed-badges.component'; import { listableObjectComponent } from '../../../../../shared/object-collection/shared/listable-object/listable-object.decorator'; import { ItemSearchResultGridElementComponent } from '../../../../../shared/object-grid/search-result-grid-element/item-search-result/item/item-search-result-grid-element.component'; @@ -21,6 +22,7 @@ import { ThemedThumbnailComponent } from '../../../../../thumbnail/themed-thumbn standalone: true, imports: [ AsyncPipe, + MetadataDirective, RouterLink, ThemedBadgesComponent, ThemedThumbnailComponent, diff --git a/src/app/entity-groups/research-entities/item-grid-elements/search-result-grid-elements/person/person-search-result-grid-element.component.html b/src/app/entity-groups/research-entities/item-grid-elements/search-result-grid-elements/person/person-search-result-grid-element.component.html index 9a0faaff150..fcecafbe172 100644 --- a/src/app/entity-groups/research-entities/item-grid-elements/search-result-grid-elements/person/person-search-result-grid-element.component.html +++ b/src/app/entity-groups/research-entities/item-grid-elements/search-result-grid-elements/person/person-search-result-grid-element.component.html @@ -32,14 +32,14 @@

@if (dso.hasMetadata('person.email')) { } @if (dso.hasMetadata('person.jobTitle')) {

- +

} diff --git a/src/app/entity-groups/research-entities/item-grid-elements/search-result-grid-elements/person/person-search-result-grid-element.component.ts b/src/app/entity-groups/research-entities/item-grid-elements/search-result-grid-elements/person/person-search-result-grid-element.component.ts index 0294875d7e2..5e491921e41 100644 --- a/src/app/entity-groups/research-entities/item-grid-elements/search-result-grid-elements/person/person-search-result-grid-element.component.ts +++ b/src/app/entity-groups/research-entities/item-grid-elements/search-result-grid-elements/person/person-search-result-grid-element.component.ts @@ -5,6 +5,7 @@ import { TranslateModule } from '@ngx-translate/core'; import { ViewMode } from '../../../../../core/shared/view-mode.model'; import { focusShadow } from '../../../../../shared/animations/focus'; +import { MetadataDirective } from '../../../../../shared/metadata.directive'; import { ThemedBadgesComponent } from '../../../../../shared/object-collection/shared/badges/themed-badges.component'; import { listableObjectComponent } from '../../../../../shared/object-collection/shared/listable-object/listable-object.decorator'; import { ItemSearchResultGridElementComponent } from '../../../../../shared/object-grid/search-result-grid-element/item-search-result/item/item-search-result-grid-element.component'; @@ -21,6 +22,7 @@ import { ThemedThumbnailComponent } from '../../../../../thumbnail/themed-thumbn standalone: true, imports: [ AsyncPipe, + MetadataDirective, RouterLink, ThemedBadgesComponent, ThemedThumbnailComponent, diff --git a/src/app/entity-groups/research-entities/item-grid-elements/search-result-grid-elements/project/project-search-result-grid-element.component.html b/src/app/entity-groups/research-entities/item-grid-elements/search-result-grid-elements/project/project-search-result-grid-element.component.html index 416495f164b..8a785e64e06 100644 --- a/src/app/entity-groups/research-entities/item-grid-elements/search-result-grid-elements/project/project-search-result-grid-element.component.html +++ b/src/app/entity-groups/research-entities/item-grid-elements/search-result-grid-elements/project/project-search-result-grid-element.component.html @@ -32,7 +32,7 @@

@if (dso.hasMetadata('dc.description')) {

- +

} diff --git a/src/app/entity-groups/research-entities/item-grid-elements/search-result-grid-elements/project/project-search-result-grid-element.component.ts b/src/app/entity-groups/research-entities/item-grid-elements/search-result-grid-elements/project/project-search-result-grid-element.component.ts index 90f5ae8cab2..655b6d687f3 100644 --- a/src/app/entity-groups/research-entities/item-grid-elements/search-result-grid-elements/project/project-search-result-grid-element.component.ts +++ b/src/app/entity-groups/research-entities/item-grid-elements/search-result-grid-elements/project/project-search-result-grid-element.component.ts @@ -5,6 +5,7 @@ import { TranslateModule } from '@ngx-translate/core'; import { ViewMode } from '../../../../../core/shared/view-mode.model'; import { focusShadow } from '../../../../../shared/animations/focus'; +import { MetadataDirective } from '../../../../../shared/metadata.directive'; import { ThemedBadgesComponent } from '../../../../../shared/object-collection/shared/badges/themed-badges.component'; import { listableObjectComponent } from '../../../../../shared/object-collection/shared/listable-object/listable-object.decorator'; import { ItemSearchResultGridElementComponent } from '../../../../../shared/object-grid/search-result-grid-element/item-search-result/item/item-search-result-grid-element.component'; @@ -21,6 +22,7 @@ import { ThemedThumbnailComponent } from '../../../../../thumbnail/themed-thumbn standalone: true, imports: [ AsyncPipe, + MetadataDirective, RouterLink, ThemedBadgesComponent, ThemedThumbnailComponent, diff --git a/src/app/entity-groups/research-entities/item-list-elements/search-result-list-elements/org-unit/org-unit-search-result-list-element.component.html b/src/app/entity-groups/research-entities/item-list-elements/search-result-list-elements/org-unit/org-unit-search-result-list-element.component.html index 51c5c42b5bc..591e54caef6 100644 --- a/src/app/entity-groups/research-entities/item-list-elements/search-result-list-elements/org-unit/org-unit-search-result-list-element.component.html +++ b/src/app/entity-groups/research-entities/item-list-elements/search-result-list-elements/org-unit/org-unit-search-result-list-element.component.html @@ -44,7 +44,7 @@ + [dsMetadata]="firstMetadata('dc.description')">
} diff --git a/src/app/entity-groups/research-entities/item-list-elements/search-result-list-elements/org-unit/org-unit-search-result-list-element.component.ts b/src/app/entity-groups/research-entities/item-list-elements/search-result-list-elements/org-unit/org-unit-search-result-list-element.component.ts index e629691636b..4616f5a39ee 100644 --- a/src/app/entity-groups/research-entities/item-list-elements/search-result-list-elements/org-unit/org-unit-search-result-list-element.component.ts +++ b/src/app/entity-groups/research-entities/item-list-elements/search-result-list-elements/org-unit/org-unit-search-result-list-element.component.ts @@ -7,6 +7,7 @@ import { RouterLink } from '@angular/router'; import { TranslateModule } from '@ngx-translate/core'; import { ViewMode } from '../../../../../core/shared/view-mode.model'; +import { MetadataDirective } from '../../../../../shared/metadata.directive'; import { ThemedBadgesComponent } from '../../../../../shared/object-collection/shared/badges/themed-badges.component'; import { listableObjectComponent } from '../../../../../shared/object-collection/shared/listable-object/listable-object.decorator'; import { ItemSearchResultListElementComponent } from '../../../../../shared/object-list/search-result-list-element/item-search-result/item-types/item/item-search-result-list-element.component'; @@ -22,6 +23,7 @@ import { ThemedThumbnailComponent } from '../../../../../thumbnail/themed-thumbn standalone: true, imports: [ AsyncPipe, + MetadataDirective, NgClass, RouterLink, ThemedBadgesComponent, diff --git a/src/app/entity-groups/research-entities/item-list-elements/search-result-list-elements/person/person-search-result-list-element.component.html b/src/app/entity-groups/research-entities/item-list-elements/search-result-list-elements/person/person-search-result-list-element.component.html index 926f89a2e37..24276972d48 100644 --- a/src/app/entity-groups/research-entities/item-list-elements/search-result-list-elements/person/person-search-result-list-element.component.html +++ b/src/app/entity-groups/research-entities/item-list-elements/search-result-list-elements/person/person-search-result-list-element.component.html @@ -44,9 +44,9 @@ @if (dso.allMetadata(['person.jobTitle']).length > 0) { - @for (value of allMetadataValues(['person.jobTitle']); track value; let last = $last) { + @for (value of allMetadata(['person.jobTitle']); track value; let last = $last) { - + } diff --git a/src/app/entity-groups/research-entities/item-list-elements/search-result-list-elements/person/person-search-result-list-element.component.ts b/src/app/entity-groups/research-entities/item-list-elements/search-result-list-elements/person/person-search-result-list-element.component.ts index 95a4337f78d..419f177a1cf 100644 --- a/src/app/entity-groups/research-entities/item-list-elements/search-result-list-elements/person/person-search-result-list-element.component.ts +++ b/src/app/entity-groups/research-entities/item-list-elements/search-result-list-elements/person/person-search-result-list-element.component.ts @@ -16,6 +16,7 @@ import { } from '../../../../../../config/app-config.interface'; import { DSONameService } from '../../../../../core/breadcrumbs/dso-name.service'; import { ViewMode } from '../../../../../core/shared/view-mode.model'; +import { MetadataDirective } from '../../../../../shared/metadata.directive'; import { ThemedBadgesComponent } from '../../../../../shared/object-collection/shared/badges/themed-badges.component'; import { listableObjectComponent } from '../../../../../shared/object-collection/shared/listable-object/listable-object.decorator'; import { ItemSearchResultListElementComponent } from '../../../../../shared/object-list/search-result-list-element/item-search-result/item-types/item/item-search-result-list-element.component'; @@ -32,6 +33,7 @@ import { ThemedThumbnailComponent } from '../../../../../thumbnail/themed-thumbn standalone: true, imports: [ AsyncPipe, + MetadataDirective, NgClass, RouterLink, ThemedBadgesComponent, 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..d35996f309f 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,7 +1,7 @@ - + diff --git a/src/app/entity-groups/research-entities/metadata-representations/org-unit/org-unit-item-metadata-list-element.component.ts b/src/app/entity-groups/research-entities/metadata-representations/org-unit/org-unit-item-metadata-list-element.component.ts index 8a2195505cf..8e0777ad005 100644 --- a/src/app/entity-groups/research-entities/metadata-representations/org-unit/org-unit-item-metadata-list-element.component.ts +++ b/src/app/entity-groups/research-entities/metadata-representations/org-unit/org-unit-item-metadata-list-element.component.ts @@ -2,6 +2,7 @@ import { Component } from '@angular/core'; import { RouterLink } from '@angular/router'; import { NgbTooltipModule } from '@ng-bootstrap/ng-bootstrap'; +import { MetadataDirective } from '../../../../shared/metadata.directive'; import { ItemMetadataRepresentationListElementComponent } from '../../../../shared/object-list/metadata-representation-list-element/item/item-metadata-representation-list-element.component'; import { TruncatableComponent } from '../../../../shared/truncatable/truncatable.component'; @@ -10,6 +11,7 @@ import { TruncatableComponent } from '../../../../shared/truncatable/truncatable templateUrl: './org-unit-item-metadata-list-element.component.html', standalone: true, imports: [ + MetadataDirective, NgbTooltipModule, RouterLink, TruncatableComponent, 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..7cc00199f1e 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,9 +3,9 @@ @if (mdRepresentation.allMetadata(['person.jobTitle']).length > 0) { - @for (value of mdRepresentation.allMetadataValues(['person.jobTitle']); track value; let last = $last) { + @for (value of mdRepresentation.allMetadata(['person.jobTitle']); track value; let last = $last) { - @if (!last) { + @if (!last) { ; } diff --git a/src/app/entity-groups/research-entities/metadata-representations/person/person-item-metadata-list-element.component.ts b/src/app/entity-groups/research-entities/metadata-representations/person/person-item-metadata-list-element.component.ts index 0cba98fe609..2758adb4c45 100644 --- a/src/app/entity-groups/research-entities/metadata-representations/person/person-item-metadata-list-element.component.ts +++ b/src/app/entity-groups/research-entities/metadata-representations/person/person-item-metadata-list-element.component.ts @@ -3,6 +3,7 @@ import { Component } from '@angular/core'; import { RouterLink } from '@angular/router'; import { NgbTooltipModule } from '@ng-bootstrap/ng-bootstrap'; +import { MetadataDirective } from '../../../../shared/metadata.directive'; import { ItemMetadataRepresentationListElementComponent } from '../../../../shared/object-list/metadata-representation-list-element/item/item-metadata-representation-list-element.component'; import { OrcidBadgeAndTooltipComponent } from '../../../../shared/orcid-badge-and-tooltip/orcid-badge-and-tooltip.component'; import { TruncatableComponent } from '../../../../shared/truncatable/truncatable.component'; @@ -12,6 +13,7 @@ import { TruncatableComponent } from '../../../../shared/truncatable/truncatable templateUrl: './person-item-metadata-list-element.component.html', standalone: true, imports: [ + MetadataDirective, NgbTooltipModule, OrcidBadgeAndTooltipComponent, RouterLink, diff --git a/src/app/entity-groups/research-entities/submission/item-list-elements/org-unit/org-unit-search-result-list-submission-element.component.html b/src/app/entity-groups/research-entities/submission/item-list-elements/org-unit/org-unit-search-result-list-submission-element.component.html index 90ec0261e25..ceed1205902 100644 --- a/src/app/entity-groups/research-entities/submission/item-list-elements/org-unit/org-unit-search-result-list-submission-element.component.html +++ b/src/app/entity-groups/research-entities/submission/item-list-elements/org-unit/org-unit-search-result-list-submission-element.component.html @@ -16,7 +16,7 @@ @if (dso.allMetadata('organization.address.addressLocality').length > 0) { - @if (dso.allMetadata('organization.address.addressCountry').length > 0) { + @if (dso.allMetadata('organization.address.addressCountry').length > 0) { , } @@ -24,7 +24,7 @@ @if (dso.allMetadata('organization.address.addressCountry').length > 0) { - + } diff --git a/src/app/entity-groups/research-entities/submission/item-list-elements/org-unit/org-unit-search-result-list-submission-element.component.ts b/src/app/entity-groups/research-entities/submission/item-list-elements/org-unit/org-unit-search-result-list-submission-element.component.ts index 98948ea6d02..b71c034ca31 100644 --- a/src/app/entity-groups/research-entities/submission/item-list-elements/org-unit/org-unit-search-result-list-submission-element.component.ts +++ b/src/app/entity-groups/research-entities/submission/item-list-elements/org-unit/org-unit-search-result-list-submission-element.component.ts @@ -21,6 +21,7 @@ import { Context } from '../../../../../core/shared/context.model'; import { Item } from '../../../../../core/shared/item.model'; import { MetadataValue } from '../../../../../core/shared/metadata.models'; import { ViewMode } from '../../../../../core/shared/view-mode.model'; +import { MetadataDirective } from '../../../../../shared/metadata.directive'; import { NotificationsService } from '../../../../../shared/notifications/notifications.service'; import { ItemSearchResult } from '../../../../../shared/object-collection/shared/item-search-result.model'; import { listableObjectComponent } from '../../../../../shared/object-collection/shared/listable-object/listable-object.decorator'; @@ -39,6 +40,7 @@ import { OrgUnitInputSuggestionsComponent } from './org-unit-suggestions/org-uni standalone: true, imports: [ FormsModule, + MetadataDirective, OrgUnitInputSuggestionsComponent, ], }) diff --git a/src/app/entity-groups/research-entities/submission/item-list-elements/person/person-search-result-list-submission-element.component.html b/src/app/entity-groups/research-entities/submission/item-list-elements/person/person-search-result-list-submission-element.component.html index cc6e09ab7f7..aa1d32e64f9 100644 --- a/src/app/entity-groups/research-entities/submission/item-list-elements/person/person-search-result-list-submission-element.component.html +++ b/src/app/entity-groups/research-entities/submission/item-list-elements/person/person-search-result-list-submission-element.component.html @@ -32,9 +32,9 @@ @if (dso.allMetadata(['person.jobTitle']).length > 0) { - @for (value of allMetadataValues(['person.jobTitle']); track value; let last = $last) { + @for (value of allMetadata(['person.jobTitle']); track value; let last = $last) { - @if (!last) { + @if (!last) { ; } diff --git a/src/app/entity-groups/research-entities/submission/item-list-elements/person/person-search-result-list-submission-element.component.ts b/src/app/entity-groups/research-entities/submission/item-list-elements/person/person-search-result-list-submission-element.component.ts index c0bc3214ce1..8b49e4e2228 100644 --- a/src/app/entity-groups/research-entities/submission/item-list-elements/person/person-search-result-list-submission-element.component.ts +++ b/src/app/entity-groups/research-entities/submission/item-list-elements/person/person-search-result-list-submission-element.component.ts @@ -22,6 +22,7 @@ import { Context } from '../../../../../core/shared/context.model'; import { Item } from '../../../../../core/shared/item.model'; import { MetadataValue } from '../../../../../core/shared/metadata.models'; import { ViewMode } from '../../../../../core/shared/view-mode.model'; +import { MetadataDirective } from '../../../../../shared/metadata.directive'; import { ItemSearchResult } from '../../../../../shared/object-collection/shared/item-search-result.model'; import { listableObjectComponent } from '../../../../../shared/object-collection/shared/listable-object/listable-object.decorator'; import { SearchResultListElementComponent } from '../../../../../shared/object-list/search-result-list-element/search-result-list-element.component'; @@ -40,6 +41,7 @@ import { PersonInputSuggestionsComponent } from './person-suggestions/person-inp imports: [ AsyncPipe, FormsModule, + MetadataDirective, NgClass, PersonInputSuggestionsComponent, ThemedThumbnailComponent, diff --git a/src/app/item-page/full/field-components/file-section/full-file-section.component.html b/src/app/item-page/full/field-components/file-section/full-file-section.component.html index 0d7a0ef6dab..c8166415e5b 100644 --- a/src/app/item-page/full/field-components/file-section/full-file-section.component.html +++ b/src/app/item-page/full/field-components/file-section/full-file-section.component.html @@ -33,8 +33,7 @@

{{ "item.page.filesection.description" | translate }}
-
- {{ file.firstMetadataValue("dc.description") }} +
} @@ -86,8 +85,7 @@

{{ "item.page.filesection.description" | translate }}
-
- {{ file.firstMetadataValue("dc.description") }} +
diff --git a/src/app/item-page/full/field-components/file-section/full-file-section.component.ts b/src/app/item-page/full/field-components/file-section/full-file-section.component.ts index a1ef48ec7bc..9d9577d9ce2 100644 --- a/src/app/item-page/full/field-components/file-section/full-file-section.component.ts +++ b/src/app/item-page/full/field-components/file-section/full-file-section.component.ts @@ -32,6 +32,7 @@ import { isEmpty, } from '../../../../shared/empty.util'; import { ThemedFileDownloadLinkComponent } from '../../../../shared/file-download-link/themed-file-download-link.component'; +import { MetadataDirective } from '../../../../shared/metadata.directive'; import { MetadataFieldWrapperComponent } from '../../../../shared/metadata-field-wrapper/metadata-field-wrapper.component'; import { NotificationsService } from '../../../../shared/notifications/notifications.service'; import { PaginationComponent } from '../../../../shared/pagination/pagination.component'; @@ -54,6 +55,7 @@ import { FileSectionComponent } from '../../../simple/field-components/file-sect imports: [ AsyncPipe, FileSizePipe, + MetadataDirective, MetadataFieldWrapperComponent, PaginationComponent, ThemedFileDownloadLinkComponent, diff --git a/src/app/shared/metadata.directive.spec.ts b/src/app/shared/metadata.directive.spec.ts new file mode 100644 index 00000000000..43267c96dd8 --- /dev/null +++ b/src/app/shared/metadata.directive.spec.ts @@ -0,0 +1,92 @@ +import { Component } from '@angular/core'; +import { + ComponentFixture, + TestBed, +} from '@angular/core/testing'; + +import { MetadataValue } from '../core/shared/metadata.models'; +import { MetadataDirective } from './metadata.directive'; + +@Component({ + standalone: true, + imports: [ + MetadataDirective, + ], + template: ` + + `, +}) +class HostComponent { + mv?: MetadataValue | null; +} + +describe('MetadataDirective', () => { + let fixture: ComponentFixture; + let host: HostComponent; + let span: HTMLSpanElement; + + function createMetadata(value?: string, language?: string): MetadataValue { + return { uuid: '123', value: value as any, language: language as any, place: undefined as any, authority: undefined as any, confidence: undefined as any } as MetadataValue; + } + + beforeEach(async () => { + await TestBed.configureTestingModule({ + imports: [HostComponent], + }).compileComponents(); + + fixture = TestBed.createComponent(HostComponent); + host = fixture.componentInstance; + fixture.detectChanges(); + span = fixture.nativeElement.querySelector('span'); + }); + + it('is empty and has no lang by default', () => { + expect(span.innerHTML).toBe(''); + expect(span.hasAttribute('lang')).toBeFalse(); + }); + + it('renders value and lang when metadata provided', () => { + host.mv = createMetadata('Hello', 'en'); + fixture.detectChanges(); + expect(span.innerHTML).toBe('Hello'); + expect(span.getAttribute('lang')).toBe('en'); + }); + + it('updates value and lang when metadata changes', () => { + host.mv = createMetadata('First', 'en'); + fixture.detectChanges(); + host.mv = createMetadata('Deuxième', 'fr'); + fixture.detectChanges(); + expect(span.innerHTML).toBe('Deuxième'); + expect(span.getAttribute('lang')).toBe('fr'); + }); + + it('clears content and lang when metadata set to null', () => { + host.mv = createMetadata('Hello', 'en'); + fixture.detectChanges(); + host.mv = null; + fixture.detectChanges(); + expect(span.innerHTML).toBe(''); + expect(span.hasAttribute('lang')).toBeFalse(); + }); + + it('removes lang attribute when language missing', () => { + host.mv = createMetadata('Value', undefined as any); + fixture.detectChanges(); + expect(span.innerHTML).toBe('Value'); + expect(span.hasAttribute('lang')).toBeFalse(); + }); + + it('renders empty string when value is undefined', () => { + host.mv = createMetadata(undefined as any, 'en'); + fixture.detectChanges(); + expect(span.innerHTML).toBe(''); + expect(span.getAttribute('lang')).toBe('en'); + }); + + it('sets innerHTML allowing markup', () => { + host.mv = createMetadata('Italic', 'en'); + fixture.detectChanges(); + expect(span.innerHTML.toLowerCase()).toBe('italic'); + }); +}); diff --git a/src/app/shared/metadata.directive.ts b/src/app/shared/metadata.directive.ts new file mode 100644 index 00000000000..bf2b1cee8f8 --- /dev/null +++ b/src/app/shared/metadata.directive.ts @@ -0,0 +1,44 @@ +import { + Directive, + ElementRef, + inject, + Input, + Renderer2, +} from '@angular/core'; + +import { MetadataValue } from '../core/shared/metadata.models'; + +@Directive({ + selector: '[dsMetadata]', + standalone: true, +}) +export class MetadataDirective { + private _metadataValue?: MetadataValue; + + private el = inject(ElementRef); + private renderer = inject(Renderer2); + + /** + * Accepts a MetadataValue. Sets the host element's innerHTML to the metadata value and the lang attribute to the metadata language. + */ + @Input() set dsMetadata(value: MetadataValue | null | undefined) { + this._metadataValue = value ?? undefined; + this.updateHost(); + } + + private updateHost(): void { + if (this._metadataValue) { + const val = this._metadataValue.value ?? ''; + this.renderer.setProperty(this.el.nativeElement, 'innerHTML', val); + if (this._metadataValue.language) { + this.renderer.setAttribute(this.el.nativeElement, 'lang', this._metadataValue.language); + } else { + this.renderer.removeAttribute(this.el.nativeElement, 'lang'); + } + } else { + this.renderer.setProperty(this.el.nativeElement, 'innerHTML', ''); + this.renderer.removeAttribute(this.el.nativeElement, 'lang'); + } + } +} + 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.html b/src/app/shared/object-detail/my-dspace-result-detail-element/item-detail-preview/item-detail-preview-field/item-detail-preview-field.component.html index 0fa8da1f72c..4cc42d0e572 100644 --- a/src/app/shared/object-detail/my-dspace-result-detail-element/item-detail-preview/item-detail-preview-field/item-detail-preview-field.component.html +++ b/src/app/shared/object-detail/my-dspace-result-detail-element/item-detail-preview/item-detail-preview-field/item-detail-preview-field.component.html @@ -1,14 +1,13 @@ @if (item.hasMetadata(metadata)) { - @for (mdValue of allMetadataValues(metadata); track mdValue; let last = $last) { - - {{mdValue}}@if (!last) { + @for (mdValue of allMetadata(metadata); track mdValue; let last = $last) { + + @if (!last) { } - + } + } + @if (!item.hasMetadata(metadata)) { + {{ (placeholder | translate) }} } -} -@if (!item.hasMetadata(metadata)) { - {{(placeholder | translate)}} -} 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..c1df4d17bb6 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 @@ -1,4 +1,3 @@ - import { Component, Input, @@ -6,7 +5,9 @@ import { import { TranslateModule } from '@ngx-translate/core'; import { Item } from '../../../../../core/shared/item.model'; +import { MetadataValue } from '../../../../../core/shared/metadata.models'; import { Metadata } from '../../../../../core/shared/metadata.utils'; +import { MetadataDirective } from '../../../../metadata.directive'; import { MetadataFieldWrapperComponent } from '../../../../metadata-field-wrapper/metadata-field-wrapper.component'; import { SearchResult } from '../../../../search/models/search-result.model'; @@ -18,6 +19,7 @@ import { SearchResult } from '../../../../search/models/search-result.model'; templateUrl: './item-detail-preview-field.component.html', standalone: true, imports: [ + MetadataDirective, MetadataFieldWrapperComponent, TranslateModule, ], @@ -54,6 +56,26 @@ export class ItemDetailPreviewFieldComponent { */ @Input() separator: string; + + /** + * Gets all matching metadata values from hitHighlights or dso metadata. + * + * @param {string|string[]} keyOrKeys The metadata key(s) in scope. Wildcards are supported; see [[Metadata]]. + * @returns {MetadataValue[]} the matching values or an empty array. + */ + allMetadata(keyOrKeys: string | string[]): MetadataValue[] { + const dsoMetadata: MetadataValue[] = Metadata.all([this.item.metadata], keyOrKeys); + const highlights: MetadataValue[] = Metadata.all([this.object.hitHighlights], keyOrKeys); + const removedHighlights: string[] = highlights.map(mv => mv.value.replace(/<\/?em>/g, '')); + for (let i = 0; i < removedHighlights.length; i++) { + const index = dsoMetadata.findIndex(mv => mv.value === removedHighlights[i]); + if (index !== -1) { + dsoMetadata[index] = highlights[i]; + } + } + return dsoMetadata; + } + /** * Gets all matching metadata string values from hitHighlights or dso metadata, preferring hitHighlights. * 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..859a910dd86 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 @@ -4,6 +4,7 @@ import { } from '@angular/core'; import { DSpaceObject } from '../../../core/shared/dspace-object.model'; +import { MetadataValue } from '../../../core/shared/metadata.models'; import { Metadata } from '../../../core/shared/metadata.utils'; import { hasValue } from '../../empty.util'; import { AbstractListableElementComponent } from '../../object-collection/shared/object-collection-element/abstract-listable-element.component'; @@ -33,6 +34,25 @@ export class SearchResultDetailElementComponent, K ext } } + /** + * Gets all matching metadata values from hitHighlights or dso metadata. + * + * @param {string|string[]} keyOrKeys The metadata key(s) in scope. Wildcards are supported; see [[Metadata]]. + * @returns {MetadataValue[]} the matching values or an empty array. + */ + allMetadata(keyOrKeys: string | string[]): MetadataValue[] { + const dsoMetadata: MetadataValue[] = Metadata.all([this.dso.metadata], keyOrKeys); + const highlights: MetadataValue[] = Metadata.all([this.object.hitHighlights], keyOrKeys); + const removedHighlights: string[] = highlights.map(mv => mv.value.replace(/<\/?em>/g, '')); + for (let i = 0; i < removedHighlights.length; i++) { + const index = dsoMetadata.findIndex(mv => mv.value === removedHighlights[i]); + if (index !== -1) { + dsoMetadata[index] = highlights[i]; + } + } + return dsoMetadata; + } + /** * Gets all matching metadata string values from hitHighlights or dso metadata, preferring hitHighlights. * @@ -43,6 +63,16 @@ export class SearchResultDetailElementComponent, K ext return Metadata.allValues([this.object.hitHighlights, this.dso.metadata], keyOrKeys); } + /** + * Gets the first matching metadata value from hitHighlights or dso metadata, preferring hitHighlights. + * + * @param {string|string[]} keyOrKeys The metadata key(s) in scope. Wildcards are supported; see [[Metadata]]. + * @returns {MetadataValue} the first matching value, or `undefined`. + */ + firstMetadata(keyOrKeys: string | string[]): MetadataValue { + return Metadata.first([this.object.hitHighlights, this.dso.metadata], keyOrKeys); + } + /** * Gets the first matching metadata string value from hitHighlights or dso metadata, preferring hitHighlights. * diff --git a/src/app/shared/object-grid/search-result-grid-element/item-search-result/item/item-search-result-grid-element.component.html b/src/app/shared/object-grid/search-result-grid-element/item-search-result/item/item-search-result-grid-element.component.html index 6acfa839f09..e30b436e6ed 100644 --- a/src/app/shared/object-grid/search-result-grid-element/item-search-result/item/item-search-result-grid-element.component.html +++ b/src/app/shared/object-grid/search-result-grid-element/item-search-result/item/item-search-result-grid-element.component.html @@ -30,12 +30,12 @@

@if (dso.hasMetadata(['dc.contributor.author', 'dc.creator', 'dc.contributor.*'])) {

- @if (dso.hasMetadata('dc.date.issued')) { - {{firstMetadataValue('dc.date.issued')}} + @if (dso.firstMetadata('dc.date.issued'); as dateIssued) { + } - @for (author of allMetadataValues(['dc.contributor.author', 'dc.creator', 'dc.contributor.*']); track author) { + @for (author of allMetadata(['dc.contributor.author', 'dc.creator', 'dc.contributor.*']); track author) { , - + }

@@ -44,7 +44,7 @@

@if (dso.hasMetadata('dc.description.abstract')) {

- +

} 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..025610b47a8 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 @@ -13,6 +13,7 @@ import { ViewMode } from '../../../../../core/shared/view-mode.model'; import { getItemPageRoute } from '../../../../../item-page/item-page-routing-paths'; import { ThemedThumbnailComponent } from '../../../../../thumbnail/themed-thumbnail.component'; import { focusShadow } from '../../../../animations/focus'; +import { MetadataDirective } from '../../../../metadata.directive'; import { ThemedBadgesComponent } from '../../../../object-collection/shared/badges/themed-badges.component'; import { ItemSearchResult } from '../../../../object-collection/shared/item-search-result.model'; import { listableObjectComponent } from '../../../../object-collection/shared/listable-object/listable-object.decorator'; @@ -31,6 +32,7 @@ import { SearchResultGridElementComponent } from '../../search-result-grid-eleme standalone: true, imports: [ AsyncPipe, + MetadataDirective, RouterLink, ThemedBadgesComponent, ThemedThumbnailComponent, 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..839e6477d95 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 @@ -7,6 +7,7 @@ import { Observable } from 'rxjs'; import { DSONameService } from '../../../core/breadcrumbs/dso-name.service'; import { BitstreamDataService } from '../../../core/data/bitstream-data.service'; import { DSpaceObject } from '../../../core/shared/dspace-object.model'; +import { MetadataValue } from '../../../core/shared/metadata.models'; import { Metadata } from '../../../core/shared/metadata.utils'; import { hasValue } from '../../empty.util'; import { AbstractListableElementComponent } from '../../object-collection/shared/object-collection-element/abstract-listable-element.component'; @@ -47,6 +48,25 @@ export class SearchResultGridElementComponent, K exten } } + /** + * Gets all matching metadata values from hitHighlights or dso metadata. + * + * @param {string|string[]} keyOrKeys The metadata key(s) in scope. Wildcards are supported; see [[Metadata]]. + * @returns {MetadataValue[]} the matching values or an empty array. + */ + allMetadata(keyOrKeys: string | string[]): MetadataValue[] { + const dsoMetadata: MetadataValue[] = Metadata.all([this.dso.metadata], keyOrKeys); + const highlights: MetadataValue[] = Metadata.all([this.object.hitHighlights], keyOrKeys); + const removedHighlights: string[] = highlights.map(mv => mv.value.replace(/<\/?em>/g, '')); + for (let i = 0; i < removedHighlights.length; i++) { + const index = dsoMetadata.findIndex(mv => mv.value === removedHighlights[i]); + if (index !== -1) { + dsoMetadata[index] = highlights[i]; + } + } + return dsoMetadata; + } + /** * Gets all matching metadata string values from hitHighlights or dso metadata, preferring hitHighlights. * @@ -57,6 +77,16 @@ export class SearchResultGridElementComponent, K exten return Metadata.allValues([this.object.hitHighlights, this.dso.metadata], keyOrKeys); } + /** + * Gets the first matching metadata value from hitHighlights or dso metadata, preferring hitHighlights. + * + * @param {string|string[]} keyOrKeys The metadata key(s) in scope. Wildcards are supported; see [[Metadata]]. + * @returns {MetadataValue} the first matching value, or `undefined`. + */ + firstMetadata(keyOrKeys: string | string[]): MetadataValue { + return Metadata.first([this.object.hitHighlights, this.dso.metadata], keyOrKeys); + } + /** * Gets the first matching metadata string value from hitHighlights or dso metadata, preferring hitHighlights. * 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..fef888d9caf 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 @@ -18,22 +18,25 @@

(@if (item.hasMetadata('dc.publisher')) { + [dsMetadata]="item.firstMetadata('dc.publisher')">, } - ) - @if (item.hasMetadata(['dc.contributor.author', 'dc.creator', 'dc.contributor.*']);) { + @if (item.hasMetadata('dc.date.issued')) { + + } @else { + {{ 'mydspace.results.no-date' | translate }} + }) + @if (item.hasMetadata(['dc.contributor.author', 'dc.creator', 'dc.contributor.*']);) { - @if (item.allMetadataValues(['dc.contributor.author', 'dc.creator', 'dc.contributor.*']).length === 0) { - {{'mydspace.results.no-authors' - | translate}} + @let authors = item.allMetadata(['dc.contributor.author', 'dc.creator', 'dc.contributor.*']); + @if (authors.length === 0) { + {{'mydspace.results.no-authors' | translate}} } - @for (author of item.allMetadataValues(['dc.contributor.author', 'dc.creator', 'dc.contributor.*']); track author; let last = $last) { + @for (author of authors; track author; let last = $last) { - + @if (!last) { ; } @@ -44,8 +47,11 @@

- + @if (item.firstMetadata('dc.description.abstract'); as abstract) { + + } @else { + {{ 'mydspace.results.no-abstract' | translate }} + } 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..b8c37864116 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 @@ -20,6 +20,7 @@ import { DSONameService } from '../../../../core/breadcrumbs/dso-name.service'; import { Item } from '../../../../core/shared/item.model'; import { ThemedThumbnailComponent } from '../../../../thumbnail/themed-thumbnail.component'; import { fadeInOut } from '../../../animations/fade'; +import { MetadataDirective } from '../../../metadata.directive'; import { ThemedBadgesComponent } from '../../../object-collection/shared/badges/themed-badges.component'; import { ItemCollectionComponent } from '../../../object-collection/shared/mydspace-item-collection/item-collection.component'; import { ItemSubmitterComponent } from '../../../object-collection/shared/mydspace-item-submitter/item-submitter.component'; @@ -40,6 +41,7 @@ import { TruncatablePartComponent } from '../../../truncatable/truncatable-part/ AsyncPipe, ItemCollectionComponent, ItemSubmitterComponent, + MetadataDirective, NgClass, ThemedBadgesComponent, ThemedThumbnailComponent, diff --git a/src/app/shared/object-list/search-result-list-element/collection-search-result/collection-search-result-list-element.component.html b/src/app/shared/object-list/search-result-list-element/collection-search-result/collection-search-result-list-element.component.html index c506050ece7..b92d56f8e8e 100644 --- a/src/app/shared/object-list/search-result-list-element/collection-search-result/collection-search-result-list-element.component.html +++ b/src/app/shared/object-list/search-result-list-element/collection-search-result/collection-search-result-list-element.component.html @@ -10,7 +10,7 @@ } @if (dso.shortDescription) { -
+
} diff --git a/src/app/shared/object-list/search-result-list-element/collection-search-result/collection-search-result-list-element.component.ts b/src/app/shared/object-list/search-result-list-element/collection-search-result/collection-search-result-list-element.component.ts index bb615635b3f..fa5adf8fb75 100644 --- a/src/app/shared/object-list/search-result-list-element/collection-search-result/collection-search-result-list-element.component.ts +++ b/src/app/shared/object-list/search-result-list-element/collection-search-result/collection-search-result-list-element.component.ts @@ -7,6 +7,7 @@ import { RouterLink } from '@angular/router'; import { Collection } from '../../../../core/shared/collection.model'; import { ViewMode } from '../../../../core/shared/view-mode.model'; +import { MetadataDirective } from '../../../metadata.directive'; import { ThemedBadgesComponent } from '../../../object-collection/shared/badges/themed-badges.component'; import { CollectionSearchResult } from '../../../object-collection/shared/collection-search-result.model'; import { listableObjectComponent } from '../../../object-collection/shared/listable-object/listable-object.decorator'; @@ -18,6 +19,7 @@ import { SearchResultListElementComponent } from '../search-result-list-element. templateUrl: 'collection-search-result-list-element.component.html', standalone: true, imports: [ + MetadataDirective, NgClass, RouterLink, ThemedBadgesComponent, diff --git a/src/app/shared/object-list/search-result-list-element/community-search-result/community-search-result-list-element.component.html b/src/app/shared/object-list/search-result-list-element/community-search-result/community-search-result-list-element.component.html index 392a369efbd..b435806aabc 100644 --- a/src/app/shared/object-list/search-result-list-element/community-search-result/community-search-result-list-element.component.html +++ b/src/app/shared/object-list/search-result-list-element/community-search-result/community-search-result-list-element.component.html @@ -10,7 +10,7 @@ } @if (dso.shortDescription) { -
+
} diff --git a/src/app/shared/object-list/search-result-list-element/community-search-result/community-search-result-list-element.component.ts b/src/app/shared/object-list/search-result-list-element/community-search-result/community-search-result-list-element.component.ts index 6b413ff14d6..fe0b578c921 100644 --- a/src/app/shared/object-list/search-result-list-element/community-search-result/community-search-result-list-element.component.ts +++ b/src/app/shared/object-list/search-result-list-element/community-search-result/community-search-result-list-element.component.ts @@ -7,6 +7,7 @@ import { RouterLink } from '@angular/router'; import { Community } from '../../../../core/shared/community.model'; import { ViewMode } from '../../../../core/shared/view-mode.model'; +import { MetadataDirective } from '../../../metadata.directive'; import { ThemedBadgesComponent } from '../../../object-collection/shared/badges/themed-badges.component'; import { CommunitySearchResult } from '../../../object-collection/shared/community-search-result.model'; import { listableObjectComponent } from '../../../object-collection/shared/listable-object/listable-object.decorator'; @@ -18,6 +19,7 @@ import { SearchResultListElementComponent } from '../search-result-list-element. templateUrl: 'community-search-result-list-element.component.html', standalone: true, imports: [ + MetadataDirective, NgClass, RouterLink, ThemedBadgesComponent, 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..065c4dfe436 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 @@ -38,20 +38,20 @@ @if (dso.firstMetadataValue('dc.publisher') || dso.firstMetadataValue('dc.date.issued')) { (@if (dso.firstMetadataValue('dc.publisher')) { - + } @if (dso.firstMetadataValue('dc.publisher') && dso.firstMetadataValue('dc.date.issued')) { , } @if (dso.firstMetadataValue('dc.date.issued')) { - + }) } @if (dso.allMetadata(['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) { + @for (author of allMetadata(['dc.contributor.author', 'dc.creator', 'dc.contributor.*']); track author; let last = $last) { - + @if (!last) { ; } @@ -63,8 +63,8 @@ @if (dso.firstMetadataValue('dc.description.abstract')) {
- + +
} diff --git a/src/app/shared/object-list/search-result-list-element/item-search-result/item-types/item/item-search-result-list-element.component.ts b/src/app/shared/object-list/search-result-list-element/item-search-result/item-types/item/item-search-result-list-element.component.ts index bc0a5c605fc..b73010b608b 100644 --- a/src/app/shared/object-list/search-result-list-element/item-search-result/item-types/item/item-search-result-list-element.component.ts +++ b/src/app/shared/object-list/search-result-list-element/item-search-result/item-types/item/item-search-result-list-element.component.ts @@ -12,6 +12,7 @@ import { Item } from '../../../../../../core/shared/item.model'; import { ViewMode } from '../../../../../../core/shared/view-mode.model'; import { getItemPageRoute } from '../../../../../../item-page/item-page-routing-paths'; import { ThemedThumbnailComponent } from '../../../../../../thumbnail/themed-thumbnail.component'; +import { MetadataDirective } from '../../../../../metadata.directive'; import { ThemedBadgesComponent } from '../../../../../object-collection/shared/badges/themed-badges.component'; import { ItemSearchResult } from '../../../../../object-collection/shared/item-search-result.model'; import { listableObjectComponent } from '../../../../../object-collection/shared/listable-object/listable-object.decorator'; @@ -28,6 +29,7 @@ import { SearchResultListElementComponent } from '../../../search-result-list-el standalone: true, imports: [ AsyncPipe, + MetadataDirective, NgClass, RouterLink, ThemedBadgesComponent, 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..54152a2e6de 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 @@ -11,6 +11,7 @@ import { } from '../../../../config/app-config.interface'; import { DSONameService } from '../../../core/breadcrumbs/dso-name.service'; import { DSpaceObject } from '../../../core/shared/dspace-object.model'; +import { MetadataValue } from '../../../core/shared/metadata.models'; import { Metadata } from '../../../core/shared/metadata.utils'; import { hasValue } from '../../empty.util'; import { AbstractListableElementComponent } from '../../object-collection/shared/object-collection-element/abstract-listable-element.component'; @@ -45,6 +46,25 @@ export class SearchResultListElementComponent, K exten } } + /** + * Gets all matching metadata values from hitHighlights or dso metadata. + * + * @param {string|string[]} keyOrKeys The metadata key(s) in scope. Wildcards are supported; see [[Metadata]]. + * @returns {MetadataValue[]} the matching values or an empty array. + */ + allMetadata(keyOrKeys: string | string[]): MetadataValue[] { + const dsoMetadata: MetadataValue[] = Metadata.all([this.dso.metadata], keyOrKeys); + const highlights: MetadataValue[] = Metadata.all([this.object.hitHighlights], keyOrKeys); + const removedHighlights: string[] = highlights.map(mv => mv.value.replace(/<\/?em>/g, '')); + for (let i = 0; i < removedHighlights.length; i++) { + const index = dsoMetadata.findIndex(mv => mv.value === removedHighlights[i]); + if (index !== -1) { + dsoMetadata[index] = highlights[i]; + } + } + return dsoMetadata; + } + /** * Gets all matching metadata string values from hitHighlights or dso metadata. * @@ -64,6 +84,16 @@ export class SearchResultListElementComponent, K exten return dsoMetadata; } + /** + * Gets the first matching metadata value from hitHighlights or dso metadata, preferring hitHighlights. + * + * @param {string|string[]} keyOrKeys The metadata key(s) in scope. Wildcards are supported; see [[Metadata]]. + * @returns {MetadataValue} the first matching value, or `undefined`. + */ + firstMetadata(keyOrKeys: string | string[]): MetadataValue { + return Metadata.first([this.object.hitHighlights, this.dso.metadata], keyOrKeys); + } + /** * Gets the first matching metadata string value from hitHighlights or dso metadata, preferring hitHighlights. * diff --git a/src/app/shared/object-select/item-select/item-select.component.html b/src/app/shared/object-select/item-select/item-select.component.html index 5c3a908038f..4f6568476c1 100644 --- a/src/app/shared/object-select/item-select/item-select.component.html +++ b/src/app/shared/object-select/item-select/item-select.component.html @@ -33,9 +33,11 @@
} - @if (selectItem.dso.hasMetadata(['dc.contributor.author', 'dc.creator', 'dc.contributor.*'])) { - {{selectItem.dso.firstMetadataValue(['dc.contributor.author', 'dc.creator', 'dc.contributor.*'])}} - } + + @if (selectItem.dso.firstMetadata(['dc.contributor.author', 'dc.creator', 'dc.contributor.*']); as author) { + + } + {{ dsoNameService.getName(selectItem.dso) }} } diff --git a/src/app/shared/object-select/item-select/item-select.component.ts b/src/app/shared/object-select/item-select/item-select.component.ts index f53ff5a2f10..9e553946c34 100644 --- a/src/app/shared/object-select/item-select/item-select.component.ts +++ b/src/app/shared/object-select/item-select/item-select.component.ts @@ -24,6 +24,7 @@ import { } from '../../empty.util'; import { ErrorComponent } from '../../error/error.component'; import { ThemedLoadingComponent } from '../../loading/themed-loading.component'; +import { MetadataDirective } from '../../metadata.directive'; import { PaginationComponent } from '../../pagination/pagination.component'; import { VarDirective } from '../../utils/var.directive'; import { DSpaceObjectSelect } from '../object-select.model'; @@ -38,6 +39,7 @@ import { ObjectSelectComponent } from '../object-select/object-select.component' BtnDisabledDirective, ErrorComponent, FormsModule, + MetadataDirective, NgClass, PaginationComponent, RouterLink, diff --git a/src/themes/custom/app/item-page/full/field-components/file-section/full-file-section.component.ts b/src/themes/custom/app/item-page/full/field-components/file-section/full-file-section.component.ts index e6b33d0b630..f5c006346ce 100644 --- a/src/themes/custom/app/item-page/full/field-components/file-section/full-file-section.component.ts +++ b/src/themes/custom/app/item-page/full/field-components/file-section/full-file-section.component.ts @@ -4,6 +4,7 @@ import { TranslateModule } from '@ngx-translate/core'; import { FullFileSectionComponent as BaseComponent } from '../../../../../../../app/item-page/full/field-components/file-section/full-file-section.component'; import { ThemedFileDownloadLinkComponent } from '../../../../../../../app/shared/file-download-link/themed-file-download-link.component'; +import { MetadataDirective } from '../../../../../../../app/shared/metadata.directive'; import { MetadataFieldWrapperComponent } from '../../../../../../../app/shared/metadata-field-wrapper/metadata-field-wrapper.component'; import { PaginationComponent } from '../../../../../../../app/shared/pagination/pagination.component'; import { FileSizePipe } from '../../../../../../../app/shared/utils/file-size-pipe'; @@ -20,6 +21,7 @@ import { ThemedThumbnailComponent } from '../../../../../../../app/thumbnail/the imports: [ AsyncPipe, FileSizePipe, + MetadataDirective, MetadataFieldWrapperComponent, PaginationComponent, ThemedFileDownloadLinkComponent, diff --git a/src/themes/custom/app/shared/object-detail/my-dspace-result-detail-element/item-detail-preview/item-detail-preview-field/item-detail-preview-field.component.ts b/src/themes/custom/app/shared/object-detail/my-dspace-result-detail-element/item-detail-preview/item-detail-preview-field/item-detail-preview-field.component.ts index 951d10bb3d0..5ca3ee43b04 100644 --- a/src/themes/custom/app/shared/object-detail/my-dspace-result-detail-element/item-detail-preview/item-detail-preview-field/item-detail-preview-field.component.ts +++ b/src/themes/custom/app/shared/object-detail/my-dspace-result-detail-element/item-detail-preview/item-detail-preview-field/item-detail-preview-field.component.ts @@ -1,6 +1,7 @@ import { Component } from '@angular/core'; import { TranslateModule } from '@ngx-translate/core'; +import { MetadataDirective } from '../../../../../../../../app/shared/metadata.directive'; import { MetadataFieldWrapperComponent } from '../../../../../../../../app/shared/metadata-field-wrapper/metadata-field-wrapper.component'; import { ItemDetailPreviewFieldComponent as BaseComponent } from '../../../../../../../../app/shared/object-detail/my-dspace-result-detail-element/item-detail-preview/item-detail-preview-field/item-detail-preview-field.component'; @@ -10,6 +11,7 @@ import { ItemDetailPreviewFieldComponent as BaseComponent } from '../../../../.. templateUrl: '../../../../../../../../app/shared/object-detail/my-dspace-result-detail-element/item-detail-preview/item-detail-preview-field/item-detail-preview-field.component.html', standalone: true, imports: [ + MetadataDirective, MetadataFieldWrapperComponent, TranslateModule, ], diff --git a/src/themes/custom/app/shared/object-list/my-dspace-result-list-element/item-list-preview/item-list-preview.component.ts b/src/themes/custom/app/shared/object-list/my-dspace-result-list-element/item-list-preview/item-list-preview.component.ts index cf3cbbabe67..a0988cd1d2c 100644 --- a/src/themes/custom/app/shared/object-list/my-dspace-result-list-element/item-list-preview/item-list-preview.component.ts +++ b/src/themes/custom/app/shared/object-list/my-dspace-result-list-element/item-list-preview/item-list-preview.component.ts @@ -6,6 +6,7 @@ import { Component } from '@angular/core'; import { TranslateModule } from '@ngx-translate/core'; import { fadeInOut } from '../../../../../../../app/shared/animations/fade'; +import { MetadataDirective } from '../../../../../../../app/shared/metadata.directive'; import { ThemedBadgesComponent } from '../../../../../../../app/shared/object-collection/shared/badges/themed-badges.component'; import { ItemCollectionComponent } from '../../../../../../../app/shared/object-collection/shared/mydspace-item-collection/item-collection.component'; import { ItemSubmitterComponent } from '../../../../../../../app/shared/object-collection/shared/mydspace-item-submitter/item-submitter.component'; @@ -26,6 +27,7 @@ import { ThemedThumbnailComponent } from '../../../../../../../app/thumbnail/the AsyncPipe, ItemCollectionComponent, ItemSubmitterComponent, + MetadataDirective, NgClass, ThemedBadgesComponent, ThemedThumbnailComponent, diff --git a/src/themes/custom/app/shared/object-list/search-result-list-element/item-search-result/item-types/item/item-search-result-list-element.component.ts b/src/themes/custom/app/shared/object-list/search-result-list-element/item-search-result/item-types/item/item-search-result-list-element.component.ts index db6f4ae5c08..54c45d38e14 100644 --- a/src/themes/custom/app/shared/object-list/search-result-list-element/item-search-result/item-types/item/item-search-result-list-element.component.ts +++ b/src/themes/custom/app/shared/object-list/search-result-list-element/item-search-result/item-types/item/item-search-result-list-element.component.ts @@ -7,6 +7,7 @@ import { RouterLink } from '@angular/router'; import { Context } from '../../../../../../../../../app/core/shared/context.model'; import { ViewMode } from '../../../../../../../../../app/core/shared/view-mode.model'; +import { MetadataDirective } from '../../../../../../../../../app/shared/metadata.directive'; import { ThemedBadgesComponent } from '../../../../../../../../../app/shared/object-collection/shared/badges/themed-badges.component'; import { ItemSearchResult } from '../../../../../../../../../app/shared/object-collection/shared/item-search-result.model'; import { listableObjectComponent } from '../../../../../../../../../app/shared/object-collection/shared/listable-object/listable-object.decorator'; @@ -26,6 +27,7 @@ import { ThemedThumbnailComponent } from '../../../../../../../../../app/thumbna standalone: true, imports: [ AsyncPipe, + MetadataDirective, NgClass, RouterLink, ThemedBadgesComponent, From 5203ffaeefdca8dcc80d4704efef105aae4f1e35 Mon Sep 17 00:00:00 2001 From: Andrea Barbasso <´andrea.barbasso@4science.com´> Date: Wed, 15 Oct 2025 17:14:17 +0200 Subject: [PATCH 2/4] [CST-19328] add lang attribute in metadata-values.component --- .../metadata-values.component.html | 25 +++++++++++-------- .../metadata-values.component.spec.ts | 16 ++++++++++++ 2 files changed, 30 insertions(+), 11 deletions(-) diff --git a/src/app/item-page/field-components/metadata-values/metadata-values.component.html b/src/app/item-page/field-components/metadata-values/metadata-values.component.html index 60fca0a8b71..5cc8c4cff92 100644 --- a/src/app/item-page/field-components/metadata-values/metadata-values.component.html +++ b/src/app/item-page/field-components/metadata-values/metadata-values.component.html @@ -4,7 +4,7 @@ Choose a template. Priority: markdown, link, browse link. --> + context: {value: mdValue.value, img, lang: mdValue.language}"> @if (!last) { @@ -13,14 +13,15 @@ - - + + - + {{value}} + [attr.lang]="lang" + [routerLink]="['/browse', browseDefinition.id]" + [queryParams]="getQueryParams(value)" role="link" tabindex="0">{{ value }} diff --git a/src/app/item-page/field-components/metadata-values/metadata-values.component.spec.ts b/src/app/item-page/field-components/metadata-values/metadata-values.component.spec.ts index 56701287336..02f014749c0 100644 --- a/src/app/item-page/field-components/metadata-values/metadata-values.component.spec.ts +++ b/src/app/item-page/field-components/metadata-values/metadata-values.component.spec.ts @@ -99,4 +99,20 @@ describe('MetadataValuesComponent', () => { expect(result.rel).toBe('noopener noreferrer'); }); + it('should set the lang attribute for each rendered metadata value', () => { + const valueSpans = fixture.debugElement.queryAll(By.css('span.dont-break-out.preserve-line-breaks')); + expect(valueSpans.length).toBe(mockMetadata.length); + valueSpans.forEach(spanDebugEl => { + expect(spanDebugEl.attributes.lang).toBe('en_US'); + }); + }); + + it('should not set the lang attribute when a metadata value language is missing', () => { + comp.mdValues = [{ value: 'No language value' } as MetadataValue]; + fixture.detectChanges(); + const valueSpans = fixture.debugElement.queryAll(By.css('span.dont-break-out.preserve-line-breaks')); + expect(valueSpans.length).toBe(1); + expect(valueSpans[0].attributes.lang).toBeUndefined(); + }); + }); From 8ca81ed45980f4c16b2a6bcfd826a85bfa7c4dbf Mon Sep 17 00:00:00 2001 From: Andrea Barbasso <´andrea.barbasso@4science.com´> Date: Wed, 15 Oct 2025 18:34:52 +0200 Subject: [PATCH 3/4] [CST-19328] add lang attribute in representation components --- .../item-metadata-representation.model.ts | 7 ++++ .../metadata-representation.model.ts | 5 +++ .../metadatum-representation.model.ts | 7 ++++ ...ta-representation-loader.component.spec.ts | 4 +++ src/app/shared/metadata.directive.spec.ts | 13 ++++++-- src/app/shared/metadata.directive.ts | 33 +++++++++++++++++-- ...-link-metadata-list-element.component.html | 1 + ...nk-metadata-list-element.component.spec.ts | 16 +++++++++ ...-text-metadata-list-element.component.html | 6 ++-- ...xt-metadata-list-element.component.spec.ts | 16 +++++++++ 10 files changed, 101 insertions(+), 7 deletions(-) diff --git a/src/app/core/shared/metadata-representation/item/item-metadata-representation.model.ts b/src/app/core/shared/metadata-representation/item/item-metadata-representation.model.ts index 9ccece13f2e..e02adc84b13 100644 --- a/src/app/core/shared/metadata-representation/item/item-metadata-representation.model.ts +++ b/src/app/core/shared/metadata-representation/item/item-metadata-representation.model.ts @@ -41,4 +41,11 @@ export class ItemMetadataRepresentation extends Item implements MetadataRepresen return this.virtualMetadata.value; } + /** + * Get the language of the value to display + */ + getLanguage(): string { + return this.virtualMetadata.language || null; + } + } diff --git a/src/app/core/shared/metadata-representation/metadata-representation.model.ts b/src/app/core/shared/metadata-representation/metadata-representation.model.ts index 379a3d1be84..e34c8e1d2e0 100644 --- a/src/app/core/shared/metadata-representation/metadata-representation.model.ts +++ b/src/app/core/shared/metadata-representation/metadata-representation.model.ts @@ -37,4 +37,9 @@ export interface MetadataRepresentation { */ getValue(): string; + /** + * Fetches the language of the metadata + */ + getLanguage(): string; + } diff --git a/src/app/core/shared/metadata-representation/metadatum/metadatum-representation.model.ts b/src/app/core/shared/metadata-representation/metadatum/metadatum-representation.model.ts index 9ca24edd57d..69f46bfff94 100644 --- a/src/app/core/shared/metadata-representation/metadatum/metadatum-representation.model.ts +++ b/src/app/core/shared/metadata-representation/metadatum/metadatum-representation.model.ts @@ -47,4 +47,11 @@ export class MetadatumRepresentation extends MetadataValue implements MetadataRe return this.value; } + /** + * Get the value language + */ + getLanguage(): string { + return this.language || null; + } + } diff --git a/src/app/shared/metadata-representation/metadata-representation-loader.component.spec.ts b/src/app/shared/metadata-representation/metadata-representation-loader.component.spec.ts index 35470b15fae..dab22a5eedc 100644 --- a/src/app/shared/metadata-representation/metadata-representation-loader.component.spec.ts +++ b/src/app/shared/metadata-representation/metadata-representation-loader.component.spec.ts @@ -36,6 +36,10 @@ class TestType implements MetadataRepresentation { getValue(): string { return ''; } + + getLanguage(): string { + return ''; + } } describe('MetadataRepresentationLoaderComponent', () => { diff --git a/src/app/shared/metadata.directive.spec.ts b/src/app/shared/metadata.directive.spec.ts index 43267c96dd8..a5be5d4a528 100644 --- a/src/app/shared/metadata.directive.spec.ts +++ b/src/app/shared/metadata.directive.spec.ts @@ -26,7 +26,14 @@ describe('MetadataDirective', () => { let span: HTMLSpanElement; function createMetadata(value?: string, language?: string): MetadataValue { - return { uuid: '123', value: value as any, language: language as any, place: undefined as any, authority: undefined as any, confidence: undefined as any } as MetadataValue; + return { + uuid: '123', + value: value, + language: language, + place: undefined, + authority: undefined, + confidence: undefined, + } as MetadataValue; } beforeEach(async () => { @@ -71,14 +78,14 @@ describe('MetadataDirective', () => { }); it('removes lang attribute when language missing', () => { - host.mv = createMetadata('Value', undefined as any); + host.mv = createMetadata('Value', undefined); fixture.detectChanges(); expect(span.innerHTML).toBe('Value'); expect(span.hasAttribute('lang')).toBeFalse(); }); it('renders empty string when value is undefined', () => { - host.mv = createMetadata(undefined as any, 'en'); + host.mv = createMetadata(undefined, 'en'); fixture.detectChanges(); expect(span.innerHTML).toBe(''); expect(span.getAttribute('lang')).toBe('en'); diff --git a/src/app/shared/metadata.directive.ts b/src/app/shared/metadata.directive.ts index bf2b1cee8f8..86fda60066e 100644 --- a/src/app/shared/metadata.directive.ts +++ b/src/app/shared/metadata.directive.ts @@ -8,24 +8,54 @@ import { import { MetadataValue } from '../core/shared/metadata.models'; +/** + * A directive that sets the innerHTML and lang attribute of the host element + * based on the provided `MetadataValue`. + * + * - The `innerHTML` is set to the `value` property of the `MetadataValue`. + * - The `lang` attribute is set to the `language` property of the `MetadataValue`. + * - If the `MetadataValue` is null or undefined, the `innerHTML` is cleared and the `lang` attribute is removed. + */ @Directive({ selector: '[dsMetadata]', standalone: true, }) export class MetadataDirective { + /** + * Stores the current `MetadataValue` provided to the directive. + */ private _metadataValue?: MetadataValue; + /** + * Reference to the host DOM element. + */ private el = inject(ElementRef); + + /** + * Angular Renderer2 instance for safely manipulating the DOM. + */ private renderer = inject(Renderer2); /** - * Accepts a MetadataValue. Sets the host element's innerHTML to the metadata value and the lang attribute to the metadata language. + * Input property for the directive. Accepts a `MetadataValue` object. + * When set, it updates the host element's `innerHTML` and `lang` attribute. + * + * @param value - The `MetadataValue` object containing the `value` and `language`. */ @Input() set dsMetadata(value: MetadataValue | null | undefined) { this._metadataValue = value ?? undefined; this.updateHost(); } + /** + * Updates the host element's `innerHTML` and `lang` attribute based on the current `MetadataValue`. + * - If `MetadataValue` is provided: + * - Sets `innerHTML` to `MetadataValue.value` (or an empty string if `value` is null/undefined). + * - Sets the `lang` attribute to `MetadataValue.language` (or removes it if `language` is null/undefined). + * - If `MetadataValue` is null/undefined: + * - Clears the `innerHTML`. + * - Removes the `lang` attribute. + */ private updateHost(): void { if (this._metadataValue) { const val = this._metadataValue.value ?? ''; @@ -41,4 +71,3 @@ export class MetadataDirective { } } } - diff --git a/src/app/shared/object-list/metadata-representation-list-element/browse-link/browse-link-metadata-list-element.component.html b/src/app/shared/object-list/metadata-representation-list-element/browse-link/browse-link-metadata-list-element.component.html index 7777f0d26fd..cf0e3c93e98 100644 --- a/src/app/shared/object-list/metadata-representation-list-element/browse-link/browse-link-metadata-list-element.component.html +++ b/src/app/shared/object-list/metadata-representation-list-element/browse-link/browse-link-metadata-list-element.component.html @@ -3,6 +3,7 @@ {{mdRepresentation.getValue()}} diff --git a/src/app/shared/object-list/metadata-representation-list-element/browse-link/browse-link-metadata-list-element.component.spec.ts b/src/app/shared/object-list/metadata-representation-list-element/browse-link/browse-link-metadata-list-element.component.spec.ts index 89048db936c..95430aaf45b 100644 --- a/src/app/shared/object-list/metadata-representation-list-element/browse-link/browse-link-metadata-list-element.component.spec.ts +++ b/src/app/shared/object-list/metadata-representation-list-element/browse-link/browse-link-metadata-list-element.component.spec.ts @@ -66,6 +66,22 @@ describe('BrowseLinkMetadataListElementComponent', () => { it('should NOT match isLink', () => { expect(comp.isLink()).toBe(false); }); + + it('should set lang attribute when language is provided', () => { + (comp.mdRepresentation as any).language = 'en'; + fixture.detectChanges(); + const anchor: HTMLAnchorElement = fixture.debugElement.nativeElement.querySelector('a'); + expect(anchor.getAttribute('lang')).toBe('en'); + }); + + it('should remove lang attribute when language becomes undefined', () => { + (comp.mdRepresentation as any).language = 'fr'; + fixture.detectChanges(); + (comp.mdRepresentation as any).language = undefined; + fixture.detectChanges(); + const anchor: HTMLAnchorElement = fixture.debugElement.nativeElement.querySelector('a'); + expect(anchor.getAttribute('lang')).toBeNull(); + }); }); describe('with metadata with an url', () => { diff --git a/src/app/shared/object-list/metadata-representation-list-element/plain-text/plain-text-metadata-list-element.component.html b/src/app/shared/object-list/metadata-representation-list-element/plain-text/plain-text-metadata-list-element.component.html index 8c550d0276a..ff76b4b89cb 100644 --- a/src/app/shared/object-list/metadata-representation-list-element/plain-text/plain-text-metadata-list-element.component.html +++ b/src/app/shared/object-list/metadata-representation-list-element/plain-text/plain-text-metadata-list-element.component.html @@ -1,18 +1,19 @@
@if ((mdRepresentation.representationType==='plain_text') && !isLink()) { - + {{mdRepresentation.getValue()}} } @if ((mdRepresentation.representationType==='plain_text') && isLink()) { {{mdRepresentation.getValue()}} } @if ((mdRepresentation.representationType==='authority_controlled')) { - {{mdRepresentation.getValue()}} + {{mdRepresentation.getValue()}} } @if ((mdRepresentation.representationType==='browse_link')) { {{mdRepresentation.getValue()}} diff --git a/src/app/shared/object-list/metadata-representation-list-element/plain-text/plain-text-metadata-list-element.component.spec.ts b/src/app/shared/object-list/metadata-representation-list-element/plain-text/plain-text-metadata-list-element.component.spec.ts index e35f7d67590..a959984d8ea 100644 --- a/src/app/shared/object-list/metadata-representation-list-element/plain-text/plain-text-metadata-list-element.component.spec.ts +++ b/src/app/shared/object-list/metadata-representation-list-element/plain-text/plain-text-metadata-list-element.component.spec.ts @@ -51,4 +51,20 @@ describe('PlainTextMetadataListElementComponent', () => { expect(fixture.debugElement.query(By.css('a.ds-browse-link')).nativeElement.innerHTML).toContain(mockMetadataRepresentation.value); }); + it('should set lang attribute when language is provided', () => { + (comp.mdRepresentation as any).language = 'en'; + fixture.detectChanges(); + const el: HTMLElement = fixture.debugElement.nativeElement.querySelector('.dont-break-out'); + expect(el.getAttribute('lang')).toBe('en'); + }); + + it('should remove lang attribute when language becomes undefined', () => { + (comp.mdRepresentation as any).language = 'fr'; + fixture.detectChanges(); + (comp.mdRepresentation as any).language = undefined; + fixture.detectChanges(); + const el: HTMLElement = fixture.debugElement.nativeElement.querySelector('.dont-break-out'); + expect(el.getAttribute('lang')).toBeNull(); + }); + }); From 6bb321292a634aacbb6ae732d3d82b41c600027f Mon Sep 17 00:00:00 2001 From: Andrea Barbasso <´andrea.barbasso@4science.com´> Date: Fri, 17 Oct 2025 13:31:10 +0200 Subject: [PATCH 4/4] [CST-16756] create dsNormalizeLanguageCode pipe --- .../metadata-values.component.html | 10 +++--- .../metadata-values.component.spec.ts | 4 +-- .../metadata-values.component.ts | 2 ++ src/app/shared/metadata.directive.ts | 4 ++- ...-link-metadata-list-element.component.html | 2 +- ...se-link-metadata-list-element.component.ts | 2 ++ ...-text-metadata-list-element.component.html | 8 ++--- ...in-text-metadata-list-element.component.ts | 2 ++ .../utils/normalize-language-code-utils.ts | 15 +++++++++ .../normalize-language-code.pipe.spec.ts | 25 +++++++++++++++ .../utils/normalize-language-code.pipe.ts | 31 +++++++++++++++++++ 11 files changed, 92 insertions(+), 13 deletions(-) create mode 100644 src/app/shared/utils/normalize-language-code-utils.ts create mode 100644 src/app/shared/utils/normalize-language-code.pipe.spec.ts create mode 100644 src/app/shared/utils/normalize-language-code.pipe.ts diff --git a/src/app/item-page/field-components/metadata-values/metadata-values.component.html b/src/app/item-page/field-components/metadata-values/metadata-values.component.html index 5cc8c4cff92..466c7ecbddd 100644 --- a/src/app/item-page/field-components/metadata-values/metadata-values.component.html +++ b/src/app/item-page/field-components/metadata-values/metadata-values.component.html @@ -14,14 +14,14 @@ - + {{ value }} diff --git a/src/app/item-page/field-components/metadata-values/metadata-values.component.spec.ts b/src/app/item-page/field-components/metadata-values/metadata-values.component.spec.ts index 02f014749c0..b822ce07f4e 100644 --- a/src/app/item-page/field-components/metadata-values/metadata-values.component.spec.ts +++ b/src/app/item-page/field-components/metadata-values/metadata-values.component.spec.ts @@ -99,11 +99,11 @@ describe('MetadataValuesComponent', () => { expect(result.rel).toBe('noopener noreferrer'); }); - it('should set the lang attribute for each rendered metadata value', () => { + it('should set the lang attribute for each rendered metadata value and convert underscores', () => { const valueSpans = fixture.debugElement.queryAll(By.css('span.dont-break-out.preserve-line-breaks')); expect(valueSpans.length).toBe(mockMetadata.length); valueSpans.forEach(spanDebugEl => { - expect(spanDebugEl.attributes.lang).toBe('en_US'); + expect(spanDebugEl.attributes.lang).toBe('en-US'); }); }); diff --git a/src/app/item-page/field-components/metadata-values/metadata-values.component.ts b/src/app/item-page/field-components/metadata-values/metadata-values.component.ts index be58645015f..7caba720642 100644 --- a/src/app/item-page/field-components/metadata-values/metadata-values.component.ts +++ b/src/app/item-page/field-components/metadata-values/metadata-values.component.ts @@ -23,6 +23,7 @@ import { VALUE_LIST_BROWSE_DEFINITION } from '../../../core/shared/value-list-br import { hasValue } from '../../../shared/empty.util'; import { MetadataFieldWrapperComponent } from '../../../shared/metadata-field-wrapper/metadata-field-wrapper.component'; import { MarkdownDirective } from '../../../shared/utils/markdown.directive'; +import { NormalizeLanguageCodePipe } from '../../../shared/utils/normalize-language-code.pipe'; import { ImageField } from '../../simple/field-components/specific-field/image-field'; /** @@ -39,6 +40,7 @@ import { ImageField } from '../../simple/field-components/specific-field/image-f MarkdownDirective, MetadataFieldWrapperComponent, NgTemplateOutlet, + NormalizeLanguageCodePipe, RouterLink, TranslateModule, ], diff --git a/src/app/shared/metadata.directive.ts b/src/app/shared/metadata.directive.ts index 86fda60066e..468033a4543 100644 --- a/src/app/shared/metadata.directive.ts +++ b/src/app/shared/metadata.directive.ts @@ -7,6 +7,7 @@ import { } from '@angular/core'; import { MetadataValue } from '../core/shared/metadata.models'; +import { normalizeLanguageCode } from './utils/normalize-language-code-utils'; /** * A directive that sets the innerHTML and lang attribute of the host element @@ -61,7 +62,8 @@ export class MetadataDirective { const val = this._metadataValue.value ?? ''; this.renderer.setProperty(this.el.nativeElement, 'innerHTML', val); if (this._metadataValue.language) { - this.renderer.setAttribute(this.el.nativeElement, 'lang', this._metadataValue.language); + const normalizedLang = normalizeLanguageCode(this._metadataValue.language); + this.renderer.setAttribute(this.el.nativeElement, 'lang', normalizedLang); } else { this.renderer.removeAttribute(this.el.nativeElement, 'lang'); } diff --git a/src/app/shared/object-list/metadata-representation-list-element/browse-link/browse-link-metadata-list-element.component.html b/src/app/shared/object-list/metadata-representation-list-element/browse-link/browse-link-metadata-list-element.component.html index cf0e3c93e98..96276a3eb0b 100644 --- a/src/app/shared/object-list/metadata-representation-list-element/browse-link/browse-link-metadata-list-element.component.html +++ b/src/app/shared/object-list/metadata-representation-list-element/browse-link/browse-link-metadata-list-element.component.html @@ -3,7 +3,7 @@ {{mdRepresentation.getValue()}} diff --git a/src/app/shared/object-list/metadata-representation-list-element/browse-link/browse-link-metadata-list-element.component.ts b/src/app/shared/object-list/metadata-representation-list-element/browse-link/browse-link-metadata-list-element.component.ts index 167e27f7f33..7246b7dbb4a 100644 --- a/src/app/shared/object-list/metadata-representation-list-element/browse-link/browse-link-metadata-list-element.component.ts +++ b/src/app/shared/object-list/metadata-representation-list-element/browse-link/browse-link-metadata-list-element.component.ts @@ -3,6 +3,7 @@ import { Component } from '@angular/core'; import { RouterLink } from '@angular/router'; import { VALUE_LIST_BROWSE_DEFINITION } from '../../../../core/shared/value-list-browse-definition.resource-type'; +import { NormalizeLanguageCodePipe } from '../../../utils/normalize-language-code.pipe'; import { MetadataRepresentationListElementComponent } from '../metadata-representation-list-element.component'; @Component({ @@ -10,6 +11,7 @@ import { MetadataRepresentationListElementComponent } from '../metadata-represen templateUrl: './browse-link-metadata-list-element.component.html', standalone: true, imports: [ + NormalizeLanguageCodePipe, RouterLink, ], }) diff --git a/src/app/shared/object-list/metadata-representation-list-element/plain-text/plain-text-metadata-list-element.component.html b/src/app/shared/object-list/metadata-representation-list-element/plain-text/plain-text-metadata-list-element.component.html index ff76b4b89cb..13c8769e370 100644 --- a/src/app/shared/object-list/metadata-representation-list-element/plain-text/plain-text-metadata-list-element.component.html +++ b/src/app/shared/object-list/metadata-representation-list-element/plain-text/plain-text-metadata-list-element.component.html @@ -1,19 +1,19 @@
@if ((mdRepresentation.representationType==='plain_text') && !isLink()) { - + {{mdRepresentation.getValue()}} } @if ((mdRepresentation.representationType==='plain_text') && isLink()) { {{mdRepresentation.getValue()}} } @if ((mdRepresentation.representationType==='authority_controlled')) { - {{mdRepresentation.getValue()}} + {{mdRepresentation.getValue()}} } @if ((mdRepresentation.representationType==='browse_link')) { {{mdRepresentation.getValue()}} diff --git a/src/app/shared/object-list/metadata-representation-list-element/plain-text/plain-text-metadata-list-element.component.ts b/src/app/shared/object-list/metadata-representation-list-element/plain-text/plain-text-metadata-list-element.component.ts index 0132f9b05b1..3df3edfe70e 100644 --- a/src/app/shared/object-list/metadata-representation-list-element/plain-text/plain-text-metadata-list-element.component.ts +++ b/src/app/shared/object-list/metadata-representation-list-element/plain-text/plain-text-metadata-list-element.component.ts @@ -3,6 +3,7 @@ import { Component } from '@angular/core'; import { RouterLink } from '@angular/router'; import { VALUE_LIST_BROWSE_DEFINITION } from '../../../../core/shared/value-list-browse-definition.resource-type'; +import { NormalizeLanguageCodePipe } from '../../../utils/normalize-language-code.pipe'; import { MetadataRepresentationListElementComponent } from '../metadata-representation-list-element.component'; @Component({ @@ -10,6 +11,7 @@ import { MetadataRepresentationListElementComponent } from '../metadata-represen templateUrl: './plain-text-metadata-list-element.component.html', standalone: true, imports: [ + NormalizeLanguageCodePipe, RouterLink, ], }) diff --git a/src/app/shared/utils/normalize-language-code-utils.ts b/src/app/shared/utils/normalize-language-code-utils.ts new file mode 100644 index 00000000000..79a6497ba83 --- /dev/null +++ b/src/app/shared/utils/normalize-language-code-utils.ts @@ -0,0 +1,15 @@ +/** + * Normalizes a language code by replacing underscores with hyphens. + * + * This function is useful for transforming POSIX-style language codes (e.g., 'en_US') into + * standard BCP 47 language tags (e.g., 'en-US'). + * + * @param value - The input string to be transformed. Can be null or undefined. + * @returns The transformed string with underscores replaced by dashes, or the original value if null/undefined. + */ +export function normalizeLanguageCode(value: string | null | undefined): string | null | undefined { + if (value === null || value === undefined) { + return value; + } + return value.replace(/_/g, '-'); +} diff --git a/src/app/shared/utils/normalize-language-code.pipe.spec.ts b/src/app/shared/utils/normalize-language-code.pipe.spec.ts new file mode 100644 index 00000000000..b89b9b3ba43 --- /dev/null +++ b/src/app/shared/utils/normalize-language-code.pipe.spec.ts @@ -0,0 +1,25 @@ +import { NormalizeLanguageCodePipe } from './normalize-language-code.pipe'; + +describe('NormalizeLanguageCodePipe', () => { + const pipe = new NormalizeLanguageCodePipe(); + + it('transforms language codes replacing underscores with dashes', () => { + expect(pipe.transform('en_US')).toBe('en-US'); + expect(pipe.transform('pt_BR')).toBe('pt-BR'); + expect(pipe.transform('zh_Hant_TW')).toBe('zh-Hant-TW'); + }); + + it('returns the same value when there are no underscores', () => { + expect(pipe.transform('en')).toBe('en'); + expect(pipe.transform('fr')).toBe('fr'); + }); + + it('preserves null and undefined input', () => { + expect(pipe.transform(null)).toBeNull(); + expect(pipe.transform(undefined)).toBeUndefined(); + }); + + it('handles empty string input', () => { + expect(pipe.transform('')).toBe(''); + }); +}); diff --git a/src/app/shared/utils/normalize-language-code.pipe.ts b/src/app/shared/utils/normalize-language-code.pipe.ts new file mode 100644 index 00000000000..ec3ff4ed730 --- /dev/null +++ b/src/app/shared/utils/normalize-language-code.pipe.ts @@ -0,0 +1,31 @@ +import { + Pipe, + PipeTransform, +} from '@angular/core'; + +import { normalizeLanguageCode } from './normalize-language-code-utils'; + +/** + * A custom Angular pipe that normalizes language codes by replacing underscores with dashes. + * + * This pipe is useful for transforming POSIX-style language codes (e.g., 'en_US') into + * standard BCP 47 language tags (e.g., 'en-US'). + * + * Example usage in a template: + * {{ 'en_US' | dsNormalizeLanguageCode }} -> 'en-US' + */ +@Pipe({ + name: 'dsNormalizeLanguageCode', + standalone: true, +}) +export class NormalizeLanguageCodePipe implements PipeTransform { + /** + * Transforms the input string by replacing all underscores with dashes. + * + * @param value - The input string to be transformed. Can be null or undefined. + * @returns The transformed string with underscores replaced by dashes, or the original value if null/undefined. + */ + transform(value: string | null | undefined): string | null | undefined { + return normalizeLanguageCode(value); + } +}