Skip to content

Commit e5e4b61

Browse files
committed
updates orcid icon and link displays according to their guidelines
1 parent 74e6d57 commit e5e4b61

16 files changed

Lines changed: 457 additions & 16 deletions

File tree

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
@@ -49,6 +49,9 @@
4949
[fields]="['dc.title']"
5050
[label]="'person.page.name'">
5151
</ds-generic-item-page-field>
52+
<ds-item-page-orcid-field
53+
[item]="object">
54+
</ds-item-page-orcid-field>
5255
<div>
5356
<a class="btn btn-outline-primary" [routerLink]="[itemPageRoute + '/full']" role="link" tabindex="0">
5457
{{"item.page.link.full" | translate}}

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

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import { TranslateModule } from '@ngx-translate/core';
88

99
import { ViewMode } from '../../../../core/shared/view-mode.model';
1010
import { GenericItemPageFieldComponent } from '../../../../item-page/simple/field-components/specific-field/generic/generic-item-page-field.component';
11+
import { ItemPageOrcidFieldComponent } from '../../../../item-page/simple/field-components/specific-field/orcid/item-page-orcid-field.component';
1112
import { ThemedItemPageTitleFieldComponent } from '../../../../item-page/simple/field-components/specific-field/title/themed-item-page-field.component';
1213
import { ItemComponent } from '../../../../item-page/simple/item-types/shared/item.component';
1314
import { TabbedRelatedEntitiesSearchComponent } from '../../../../item-page/simple/related-entities/tabbed-related-entities-search/tabbed-related-entities-search.component';
@@ -24,7 +25,7 @@ import { ThemedThumbnailComponent } from '../../../../thumbnail/themed-thumbnail
2425
styleUrls: ['./person.component.scss'],
2526
templateUrl: './person.component.html',
2627
standalone: true,
27-
imports: [NgIf, ThemedResultsBackButtonComponent, ThemedItemPageTitleFieldComponent, DsoEditMenuComponent, MetadataFieldWrapperComponent, ThemedThumbnailComponent, GenericItemPageFieldComponent, RelatedItemsComponent, RouterLink, TabbedRelatedEntitiesSearchComponent, AsyncPipe, TranslateModule],
28+
imports: [NgIf, ThemedResultsBackButtonComponent, ThemedItemPageTitleFieldComponent, DsoEditMenuComponent, MetadataFieldWrapperComponent, ThemedThumbnailComponent, GenericItemPageFieldComponent, ItemPageOrcidFieldComponent, RelatedItemsComponent, RouterLink, TabbedRelatedEntitiesSearchComponent, AsyncPipe, TranslateModule],
2829
})
2930
/**
3031
* The component for displaying metadata and relations of an item of the type Person
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/shared/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: 170 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,170 @@
1+
import { AsyncPipe } from '@angular/common';
2+
import {
3+
Component,
4+
Input,
5+
OnInit,
6+
} from '@angular/core';
7+
import { TranslateModule } 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+
standalone: true,
29+
imports: [
30+
AsyncPipe,
31+
TranslateModule,
32+
],
33+
})
34+
/**
35+
* This component is used for displaying ORCID identifier as a clickable link
36+
*/
37+
export class ItemPageOrcidFieldComponent extends ItemPageFieldComponent implements OnInit {
38+
39+
/**
40+
* The item to display metadata for
41+
*/
42+
@Input() item: Item;
43+
44+
/**
45+
* Separator string between multiple values of the metadata fields defined
46+
* @type {string}
47+
*/
48+
separator: string;
49+
50+
/**
51+
* Fields (schema.element.qualifier) used to render their values.
52+
* In this component, we want to display values for metadata 'person.identifier.orcid'
53+
*/
54+
fields: string[] = [
55+
'person.identifier.orcid',
56+
];
57+
58+
/**
59+
* Label i18n key for the rendered metadata
60+
*/
61+
label = 'item.page.orcid-profile';
62+
63+
/**
64+
* Observable for the ORCID URL from configuration
65+
*/
66+
baseUrl$: Observable<string>;
67+
68+
/**
69+
* ORCID ID (without full URL)
70+
*/
71+
orcidId: string | null;
72+
73+
/**
74+
* Observable for the full ORCID URL
75+
*/
76+
orcidUrl$: Observable<string>;
77+
78+
/**
79+
* Whether the item has ORCID metadata
80+
*/
81+
hasOrcidMetadata: boolean;
82+
83+
/**
84+
* ORCID icon configuration
85+
*/
86+
img: ImageField = {
87+
URI: 'assets/images/orcid.logo.icon.svg',
88+
alt: 'item.page.orcid-icon',
89+
heightVar: '--ds-orcid-icon-height',
90+
};
91+
92+
/**
93+
* Creates an instance of ItemPageOrcidFieldComponent.
94+
*
95+
* @param {BrowseDefinitionDataService} browseDefinitionDataService - Service for managing browse definitions
96+
* @param {BrowseService} browseService - Service for browse functionality
97+
* @param {ConfigurationDataService} configurationService - Service for accessing configuration properties
98+
*/
99+
constructor(
100+
protected browseDefinitionDataService: BrowseDefinitionDataService,
101+
protected browseService: BrowseService,
102+
protected configurationService: ConfigurationDataService,
103+
) {
104+
super(browseDefinitionDataService, browseService);
105+
}
106+
107+
/**
108+
* Initializes the component and sets up observables for ORCID URL.
109+
* Separates the display value (ORCID ID) from the link URL.
110+
*
111+
* @returns {void}
112+
*/
113+
ngOnInit(): void {
114+
115+
this.hasOrcidMetadata = this.hasOrcid();
116+
117+
this.baseUrl$ = this.configurationService
118+
.findByPropertyName('orcid.domain-url')
119+
.pipe(
120+
getFirstSucceededRemoteDataPayload(),
121+
map((property: ConfigurationProperty) =>
122+
property?.values?.length > 0 ? property.values[0] : null,
123+
),
124+
);
125+
126+
const metadata = this.getOrcidMetadata();
127+
128+
this.orcidId = metadata?.value.replace(/^\//, '') || null;
129+
130+
this.orcidUrl$ = combineLatest([
131+
this.baseUrl$,
132+
]).pipe(
133+
map(([baseUrl]) => {
134+
if (!baseUrl || !this.orcidId) {
135+
return null;
136+
}
137+
138+
const cleanBaseUrl = baseUrl.replace(/\/$/, '');
139+
return `${cleanBaseUrl}/${this.orcidId}`;
140+
}),
141+
);
142+
}
143+
144+
/**
145+
* Retrieves the ORCID metadata value from the item.
146+
* Extracts the first ORCID identifier from the item's metadata fields,
147+
* ensuring the value is not empty or whitespace only.
148+
*
149+
* @private
150+
* @returns {MetadataValue | null} The ORCID metadata value if found and valid, null otherwise
151+
*/
152+
private getOrcidMetadata(): MetadataValue | null {
153+
if (!this.item || !this.hasOrcid()) {
154+
return null;
155+
}
156+
157+
const metadata = this.item.findMetadataSortedByPlace('person.identifier.orcid');
158+
return metadata.length > 0 && metadata[0].value?.trim() ? metadata[0] : null;
159+
}
160+
161+
/**
162+
* Checks whether the item has ORCID metadata associated with it.
163+
*
164+
* @public
165+
* @returns {boolean} True if the item has 'person.identifier.orcid' metadata, false otherwise
166+
*/
167+
public hasOrcid(): boolean {
168+
return this.item?.hasMetadata('person.identifier.orcid');
169+
}
170+
}

0 commit comments

Comments
 (0)