Skip to content

Commit 87bc813

Browse files
authored
Merge pull request DSpace#5489 from TexasDigitalLibrary/DS-4766-replacement
DS-4766 replacement: updates ORCID icon and link displays according to their guidelines
2 parents 51a37cc + 8dcf4e1 commit 87bc813

File tree

16 files changed

+460
-19
lines changed

16 files changed

+460
-19
lines changed

src/app/entity-groups/research-entities/item-pages/person/person.component.html

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,9 @@
5151
[fields]="['dc.title']"
5252
[label]="'person.page.name'">
5353
</ds-generic-item-page-field>
54+
<ds-item-page-orcid-field
55+
[item]="object">
56+
</ds-item-page-orcid-field>
5457
<div>
5558
<a class="btn btn-outline-primary" [routerLink]="[itemPageRoute + '/full']" role="button" tabindex="0">
5659
{{"item.page.link.full" | translate}}

src/app/entity-groups/research-entities/item-pages/person/person.component.ts

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

77
import { GenericItemPageFieldComponent } from '../../../../item-page/simple/field-components/specific-field/generic/generic-item-page-field.component';
8+
import { ItemPageOrcidFieldComponent } from '../../../../item-page/simple/field-components/specific-field/orcid/item-page-orcid-field.component';
89
import { ThemedItemPageTitleFieldComponent } from '../../../../item-page/simple/field-components/specific-field/title/themed-item-page-field.component';
910
import { ItemComponent } from '../../../../item-page/simple/item-types/shared/item.component';
1011
import { AuthorityRelatedEntitiesSearchComponent } from '../../../../item-page/simple/related-entities/authority-related-entities-search/authority-related-entities-search.component';
@@ -26,6 +27,7 @@ import { ThemedThumbnailComponent } from '../../../../thumbnail/themed-thumbnail
2627
AuthorityRelatedEntitiesSearchComponent,
2728
DsoEditMenuComponent,
2829
GenericItemPageFieldComponent,
30+
ItemPageOrcidFieldComponent,
2931
MetadataFieldWrapperComponent,
3032
RelatedItemsComponent,
3133
RouterLink,
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
@if (hasOrcidMetadata) {
2+
<div class="simple-view-element">
3+
<h2 class="simple-view-element-header">{{ label | translate }}</h2>
4+
<div #content class="simple-view-element-body d-flex flex-column">
5+
<span class="d-flex align-items-center">
6+
<a [href]="orcidUrl$ | async" target="_blank" rel="noopener noreferrer">
7+
<img [src]="img.URI" [alt]="img.alt | translate" class="orcid-icon" />
8+
<span>{{ orcidId }}</span>
9+
</a>
10+
</span>
11+
</div>
12+
</div>
13+
}
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
:host {
2+
.simple-view-element {
3+
margin-bottom: 15px;
4+
}
5+
.simple-view-element-header {
6+
font-size: 1.25rem;
7+
}
8+
.orcid-icon {
9+
height: var(--ds-orcid-icon-height, 16px);
10+
margin-right: 8px;
11+
}
12+
}
Lines changed: 168 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,168 @@
1+
import { NO_ERRORS_SCHEMA } from '@angular/core';
2+
import {
3+
ComponentFixture,
4+
TestBed,
5+
} from '@angular/core/testing';
6+
import { TranslateModule } from '@ngx-translate/core';
7+
import { of } from 'rxjs';
8+
import { APP_CONFIG } from 'src/config/app-config.interface';
9+
10+
import { createSuccessfulRemoteDataObject$ } from '../../../../../../app/core/utilities/remote-data.utils';
11+
import { BrowseService } from '../../../../../core/browse/browse.service';
12+
import { BrowseDefinitionDataService } from '../../../../../core/browse/browse-definition-data.service';
13+
import { ConfigurationDataService } from '../../../../../core/data/configuration-data.service';
14+
import { ConfigurationProperty } from '../../../../../core/shared/configuration-property.model';
15+
import { Item } from '../../../../../core/shared/item.model';
16+
import { ItemPageOrcidFieldComponent } from './item-page-orcid-field.component';
17+
18+
describe('ItemPageOrcidFieldComponent', () => {
19+
let component: ItemPageOrcidFieldComponent;
20+
let fixture: ComponentFixture<ItemPageOrcidFieldComponent>;
21+
let configurationService: jasmine.SpyObj<ConfigurationDataService>;
22+
23+
const mockItem = Object.assign(new Item(), {
24+
metadata: {
25+
'person.identifier.orcid': [
26+
{
27+
value: '0000-0002-1825-0097',
28+
language: null,
29+
authority: null,
30+
confidence: -1,
31+
place: 0,
32+
},
33+
],
34+
},
35+
});
36+
37+
const mockConfigProperty = Object.assign(new ConfigurationProperty(), {
38+
name: 'orcid.domain-url',
39+
values: ['https://sandbox.orcid.org'],
40+
});
41+
42+
const mockAppConfig = {
43+
ui: {
44+
ssl: false,
45+
host: 'localhost',
46+
port: 4000,
47+
nameSpace: '/',
48+
},
49+
markdown: {
50+
enabled: false,
51+
mathjax: false,
52+
},
53+
};
54+
55+
beforeEach(async () => {
56+
configurationService = jasmine.createSpyObj('ConfigurationDataService', ['findByPropertyName']);
57+
configurationService.findByPropertyName.and.returnValue(
58+
createSuccessfulRemoteDataObject$(mockConfigProperty),
59+
);
60+
61+
const browseDefinitionDataServiceStub = {
62+
findAll: jasmine.createSpy('findAll').and.returnValue(of({})),
63+
getBrowseDefinitions: jasmine.createSpy('getBrowseDefinitions').and.returnValue(of([])),
64+
};
65+
66+
const browseServiceStub = {
67+
getBrowseEntriesFor: jasmine.createSpy('getBrowseEntriesFor').and.returnValue(of({})),
68+
getBrowseDefinitions: jasmine.createSpy('getBrowseDefinitions').and.returnValue(of([])),
69+
};
70+
71+
await TestBed.configureTestingModule({
72+
imports: [
73+
ItemPageOrcidFieldComponent,
74+
TranslateModule.forRoot(),
75+
],
76+
providers: [
77+
{ provide: ConfigurationDataService, useValue: configurationService },
78+
{ provide: BrowseDefinitionDataService, useValue: browseDefinitionDataServiceStub },
79+
{ provide: BrowseService, useValue: browseServiceStub },
80+
{ provide: APP_CONFIG, useValue: mockAppConfig },
81+
],
82+
schemas: [NO_ERRORS_SCHEMA],
83+
})
84+
.compileComponents();
85+
86+
fixture = TestBed.createComponent(ItemPageOrcidFieldComponent);
87+
component = fixture.componentInstance;
88+
component.item = mockItem;
89+
});
90+
91+
it('should create', () => {
92+
fixture.detectChanges();
93+
expect(component).toBeTruthy();
94+
});
95+
96+
it('should check if item has ORCID', () => {
97+
expect(component.hasOrcid()).toBe(true);
98+
});
99+
100+
it('should return false when item has no ORCID', () => {
101+
component.item = Object.assign(new Item(), { metadata: {} });
102+
expect(component.hasOrcid()).toBe(false);
103+
});
104+
105+
it('should set hasOrcidMetadata property on init', () => {
106+
fixture.detectChanges();
107+
expect(component.hasOrcidMetadata).toBe(true);
108+
});
109+
110+
it('should set hasOrcidMetadata to false when item has no ORCID', () => {
111+
component.item = Object.assign(new Item(), { metadata: {} });
112+
component.ngOnInit();
113+
expect(component.hasOrcidMetadata).toBe(false);
114+
});
115+
116+
it('should construct ORCID URL on init', (done) => {
117+
fixture.detectChanges();
118+
119+
component.orcidUrl$.subscribe(url => {
120+
expect(url).toBe('https://sandbox.orcid.org/0000-0002-1825-0097');
121+
done();
122+
});
123+
});
124+
125+
it('should extract ORCID ID on init', () => {
126+
fixture.detectChanges();
127+
expect(component.orcidId).toBe('0000-0002-1825-0097');
128+
});
129+
130+
it('should handle ORCID with leading slash', (done) => {
131+
component.item = Object.assign(new Item(), {
132+
metadata: {
133+
'person.identifier.orcid': [
134+
{
135+
value: '/0000-0002-1825-0097',
136+
language: null,
137+
authority: null,
138+
confidence: -1,
139+
place: 0,
140+
},
141+
],
142+
},
143+
});
144+
145+
component.ngOnInit();
146+
fixture.detectChanges();
147+
148+
expect(component.orcidId).toBe('0000-0002-1825-0097');
149+
150+
component.orcidUrl$.subscribe(url => {
151+
expect(url).toBe('https://sandbox.orcid.org/0000-0002-1825-0097');
152+
done();
153+
});
154+
});
155+
156+
it('should return null when item has no ORCID metadata', (done) => {
157+
component.item = Object.assign(new Item(), { metadata: {} });
158+
component.ngOnInit();
159+
fixture.detectChanges();
160+
161+
expect(component.orcidId).toBeNull();
162+
163+
component.orcidUrl$.subscribe(url => {
164+
expect(url).toBeNull();
165+
done();
166+
});
167+
});
168+
});
Lines changed: 169 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,169 @@
1+
import { AsyncPipe } from '@angular/common';
2+
import {
3+
Component,
4+
Input,
5+
OnInit,
6+
} from '@angular/core';
7+
import { TranslatePipe } from '@ngx-translate/core';
8+
import {
9+
combineLatest,
10+
map,
11+
Observable,
12+
} from 'rxjs';
13+
14+
import { BrowseService } from '../../../../../core/browse/browse.service';
15+
import { BrowseDefinitionDataService } from '../../../../../core/browse/browse-definition-data.service';
16+
import { ConfigurationDataService } from '../../../../../core/data/configuration-data.service';
17+
import { ConfigurationProperty } from '../../../../../core/shared/configuration-property.model';
18+
import { Item } from '../../../../../core/shared/item.model';
19+
import { MetadataValue } from '../../../../../core/shared/metadata.models';
20+
import { getFirstSucceededRemoteDataPayload } from '../../../../../core/shared/operators';
21+
import { ImageField } from '../image-field';
22+
import { ItemPageFieldComponent } from '../item-page-field.component';
23+
24+
@Component({
25+
selector: 'ds-item-page-orcid-field',
26+
templateUrl: './item-page-orcid-field.component.html',
27+
styleUrls: ['./item-page-orcid-field.component.scss'],
28+
imports: [
29+
AsyncPipe,
30+
TranslatePipe,
31+
],
32+
})
33+
/**
34+
* This component is used for displaying ORCID identifier as a clickable link
35+
*/
36+
export class ItemPageOrcidFieldComponent extends ItemPageFieldComponent implements OnInit {
37+
38+
/**
39+
* The item to display metadata for
40+
*/
41+
@Input() item: Item;
42+
43+
/**
44+
* Separator string between multiple values of the metadata fields defined
45+
* @type {string}
46+
*/
47+
separator: string;
48+
49+
/**
50+
* Fields (schema.element.qualifier) used to render their values.
51+
* In this component, we want to display values for metadata 'person.identifier.orcid'
52+
*/
53+
fields: string[] = [
54+
'person.identifier.orcid',
55+
];
56+
57+
/**
58+
* Label i18n key for the rendered metadata
59+
*/
60+
label = 'item.page.orcid-profile';
61+
62+
/**
63+
* Observable for the ORCID URL from configuration
64+
*/
65+
baseUrl$: Observable<string>;
66+
67+
/**
68+
* ORCID ID (without full URL)
69+
*/
70+
orcidId: string | null;
71+
72+
/**
73+
* Observable for the full ORCID URL
74+
*/
75+
orcidUrl$: Observable<string>;
76+
77+
/**
78+
* Whether the item has ORCID metadata
79+
*/
80+
hasOrcidMetadata: boolean;
81+
82+
/**
83+
* ORCID icon configuration
84+
*/
85+
img: ImageField = {
86+
URI: 'assets/images/orcid.logo.icon.svg',
87+
alt: 'item.page.orcid-icon',
88+
heightVar: '--ds-orcid-icon-height',
89+
};
90+
91+
/**
92+
* Creates an instance of ItemPageOrcidFieldComponent.
93+
*
94+
* @param {BrowseDefinitionDataService} browseDefinitionDataService - Service for managing browse definitions
95+
* @param {BrowseService} browseService - Service for browse functionality
96+
* @param {ConfigurationDataService} configurationService - Service for accessing configuration properties
97+
*/
98+
constructor(
99+
protected browseDefinitionDataService: BrowseDefinitionDataService,
100+
protected browseService: BrowseService,
101+
protected configurationService: ConfigurationDataService,
102+
) {
103+
super(browseDefinitionDataService, browseService);
104+
}
105+
106+
/**
107+
* Initializes the component and sets up observables for ORCID URL.
108+
* Separates the display value (ORCID ID) from the link URL.
109+
*
110+
* @returns {void}
111+
*/
112+
ngOnInit(): void {
113+
114+
this.hasOrcidMetadata = this.hasOrcid();
115+
116+
this.baseUrl$ = this.configurationService
117+
.findByPropertyName('orcid.domain-url')
118+
.pipe(
119+
getFirstSucceededRemoteDataPayload(),
120+
map((property: ConfigurationProperty) =>
121+
property?.values?.length > 0 ? property.values[0] : null,
122+
),
123+
);
124+
125+
const metadata = this.getOrcidMetadata();
126+
127+
this.orcidId = metadata?.value.replace(/^\//, '') || null;
128+
129+
this.orcidUrl$ = combineLatest([
130+
this.baseUrl$,
131+
]).pipe(
132+
map(([baseUrl]) => {
133+
if (!baseUrl || !this.orcidId) {
134+
return null;
135+
}
136+
137+
const cleanBaseUrl = baseUrl.replace(/\/$/, '');
138+
return `${cleanBaseUrl}/${this.orcidId}`;
139+
}),
140+
);
141+
}
142+
143+
/**
144+
* Retrieves the ORCID metadata value from the item.
145+
* Extracts the first ORCID identifier from the item's metadata fields,
146+
* ensuring the value is not empty or whitespace only.
147+
*
148+
* @private
149+
* @returns {MetadataValue | null} The ORCID metadata value if found and valid, null otherwise
150+
*/
151+
private getOrcidMetadata(): MetadataValue | null {
152+
if (!this.item || !this.hasOrcid()) {
153+
return null;
154+
}
155+
156+
const metadata = this.item.findMetadataSortedByPlace('person.identifier.orcid');
157+
return metadata.length > 0 && metadata[0].value?.trim() ? metadata[0] : null;
158+
}
159+
160+
/**
161+
* Checks whether the item has ORCID metadata associated with it.
162+
*
163+
* @public
164+
* @returns {boolean} True if the item has 'person.identifier.orcid' metadata, false otherwise
165+
*/
166+
public hasOrcid(): boolean {
167+
return this.item?.hasMetadata('person.identifier.orcid');
168+
}
169+
}

0 commit comments

Comments
 (0)