Skip to content
Open
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
<ds-metadata-field-wrapper [label]="label | translate">
@for (mdValue of mdValues; track mdValue; let last = $last) {
<!--
Choose a template. Priority: markdown, link, browse link.
Choose a template. Priority: ORCID authority, markdown, link, browse link.
-->
<ng-container *ngTemplateOutlet="(renderMarkdown ? markdown : (hasLink(mdValue) ? (hasValue(img) ? linkImg : link) : (hasBrowseDefinition() ? browselink : simple)));
context: {value: mdValue.value, img}">
<ng-container *ngTemplateOutlet="(isOrcidAuthority(mdValue) ? orcidTemplate : (renderMarkdown ? markdown : (hasLink(mdValue) ? (hasValue(img) ? linkImg : link) : (hasBrowseDefinition() ? browselink : simple))));
context: {value: mdValue.value, mdValue: mdValue, img}">
</ng-container>
@if (!last) {
<span class="separator" [innerHTML]="separator"></span>
Expand Down Expand Up @@ -50,3 +50,25 @@
[routerLink]="['/browse', browseDefinition.id]"
[queryParams]="getQueryParams(value)" role="link" tabindex="0">{{value}}</a>
</ng-template>

<!-- Render value with ORCID badge and clickable link to ORCID profile -->
<ng-template #orcidTemplate let-value="value" let-mdValue="mdValue">
<span class="orcid-author">
<a class="dont-break-out preserve-line-breaks ds-orcid-link"
[href]="getOrcidUrl(mdValue.authority)"
target="_blank"
rel="noopener noreferrer"
role="link"
tabindex="0">
{{value}}
</a>
<a class="orcid-badge"
[href]="getOrcidUrl(mdValue.authority)"
target="_blank"
rel="noopener noreferrer">
<img src="assets/images/orcid.logo.icon.svg"
alt="ORCID iD icon"
class="orcid-icon">
</a>
</span>
</ng-template>
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
.orcid-author {
display: inline-flex;
align-items: center;
gap: 0.25rem;
}

.ds-orcid-link {
text-decoration: none;
color: inherit;

&:hover {
text-decoration: underline;
}
}

.orcid-badge {
display: inline-flex;
align-items: center;
text-decoration: none;

&:hover {
opacity: 0.8;
}
}

.orcid-icon {
width: 16px;
height: 16px;
vertical-align: middle;
}
Original file line number Diff line number Diff line change
Expand Up @@ -99,4 +99,56 @@ describe('MetadataValuesComponent', () => {
expect(result.rel).toBe('noopener noreferrer');
});

it('should correctly detect ORCID authority pattern', () => {
const orcidMdValue = {
value: 'John Doe',
authority: '0000-0002-1825-0097',
confidence: 600,
} as MetadataValue;
expect(comp.isOrcidAuthority(orcidMdValue)).toBe(true);
});

it('should return false for non-ORCID authority patterns', () => {
const nonOrcidMdValue = {
value: 'Jane Smith',
authority: 'not-an-orcid',
confidence: 600,
} as MetadataValue;
expect(comp.isOrcidAuthority(nonOrcidMdValue)).toBe(false);
});

it('should return false for metadata values without authority', () => {
const noAuthorityMdValue = {
value: 'Anonymous Author',
authority: null,
confidence: -1,
} as MetadataValue;
expect(comp.isOrcidAuthority(noAuthorityMdValue)).toBe(false);
});

it('should generate correct ORCID profile URL', () => {
const orcidId = '0000-0002-1825-0097';
const expectedUrl = 'https://orcid.org/0000-0002-1825-0097';
expect(comp.getOrcidUrl(orcidId)).toBe(expectedUrl);
});

it('should render ORCID link and badge for metadata with ORCID authority', () => {
const orcidMetadata = [
{
language: 'en_US',
value: 'John Doe',
authority: '0000-0002-1825-0097',
confidence: 600,
},
] as MetadataValue[];
comp.mdValues = orcidMetadata;
fixture.detectChanges();
const orcidLink = fixture.debugElement.query(By.css('a.ds-orcid-link'));
expect(orcidLink).toBeTruthy();
expect(orcidLink.nativeElement.getAttribute('href')).toBe('https://orcid.org/0000-0002-1825-0097');
expect(orcidLink.nativeElement.textContent).toContain('John Doe');
const orcidBadge = fixture.debugElement.query(By.css('a.orcid-badge'));
expect(orcidBadge).toBeTruthy();
});

});
Original file line number Diff line number Diff line change
Expand Up @@ -140,6 +140,28 @@ export class MetadataValuesComponent implements OnChanges {
return linkValue.startsWith(environment.ui.baseUrl);
}

/**
* Checks if a metadata value has ORCID authority (matches ORCID pattern)
* @param mdValue - The metadata value to check
* @returns True if the authority field matches ORCID pattern
*/
isOrcidAuthority(mdValue: MetadataValue): boolean {
if (!hasValue(mdValue.authority)) {
return false;
}
const orcidPattern = /^\d{4}-\d{4}-\d{4}-\d{4}$/;
return orcidPattern.test(mdValue.authority);
}

/**
* Generates ORCID profile URL from authority ID
* @param authorityId - The ORCID authority ID
* @returns The full ORCID profile URL
*/
getOrcidUrl(authorityId: string): string {
return `https://orcid.org/${encodeURIComponent(authorityId)}`;
}
Comment on lines +148 to +163

Copilot AI Mar 20, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ORCID iDs can have an "X" as the final check digit; the regex ^\d{4}-\d{4}-\d{4}-\d{4}$ will reject valid ORCIDs ending in X. Update the pattern (and consider making it a shared constant since the same regex/URL is now duplicated in multiple components).

Copilot uses AI. Check for mistakes.

/**
* This method performs a validation and determines the target of the url.
* @returns - Returns the target url.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,19 @@
{{mdRepresentation.getValue()}}
</a>
}
@if ((mdRepresentation.representationType==='authority_controlled')) {
@if ((mdRepresentation.representationType==='authority_controlled') && isOrcidAuthority()) {
<span class="dont-break-out orcid-author">
<a class="orcid-link" [href]="getOrcidUrl()" target="_blank" rel="noopener noreferrer">
{{mdRepresentation.getValue()}}
</a>
<a class="orcid-badge" [href]="getOrcidUrl()" target="_blank" rel="noopener noreferrer">
<img src="assets/images/orcid.logo.icon.svg"
alt="ORCID iD icon"
class="orcid-icon">
</a>
</span>
}
@if ((mdRepresentation.representationType==='authority_controlled') && !isOrcidAuthority()) {
<span class="dont-break-out">{{mdRepresentation.getValue()}}</span>
}
@if ((mdRepresentation.representationType==='browse_link')) {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
.orcid-author {
display: inline-flex;
align-items: center;
gap: 0.25rem;
}

.orcid-link {
text-decoration: none;
color: inherit;

&:hover {
text-decoration: underline;
}
}

.orcid-badge {
display: inline-flex;
align-items: center;
text-decoration: none;

&:hover {
opacity: 0.8;
}
}

.orcid-icon {
width: 16px;
height: 16px;
vertical-align: middle;
}
Original file line number Diff line number Diff line change
Expand Up @@ -51,4 +51,68 @@ describe('PlainTextMetadataListElementComponent', () => {
expect(fixture.debugElement.query(By.css('a.ds-browse-link')).nativeElement.innerHTML).toContain(mockMetadataRepresentation.value);
});

it('should correctly detect ORCID authority', () => {
const orcidRepresentation = Object.assign(new MetadatumRepresentation('type'), {
key: 'dc.contributor.author',
value: 'John Doe',
authority: '0000-0002-1825-0097',
confidence: 600,
});
comp.mdRepresentation = orcidRepresentation;
expect(comp.isOrcidAuthority()).toBe(true);
});

it('should return false for non-ORCID authority', () => {
const nonOrcidRepresentation = Object.assign(new MetadatumRepresentation('type'), {
key: 'dc.contributor.author',
value: 'Jane Smith',
authority: 'not-an-orcid',
confidence: 600,
});
comp.mdRepresentation = nonOrcidRepresentation;
expect(comp.isOrcidAuthority()).toBe(false);
});

it('should generate correct ORCID profile URL', () => {
const orcidRepresentation = Object.assign(new MetadatumRepresentation('type'), {
key: 'dc.contributor.author',
value: 'John Doe',
authority: '0000-0002-1825-0097',
confidence: 600,
});
comp.mdRepresentation = orcidRepresentation;
expect(comp.getOrcidUrl()).toBe('https://orcid.org/0000-0002-1825-0097');
});

it('should render ORCID link and badge for authority controlled metadata with ORCID', () => {
const orcidRepresentation = Object.assign(new MetadatumRepresentation('type'), {
key: 'dc.contributor.author',
value: 'John Doe',
authority: '0000-0002-1825-0097',
confidence: 600,
});
comp.mdRepresentation = orcidRepresentation;
fixture.detectChanges();
const orcidLink = fixture.debugElement.query(By.css('a.orcid-link'));
expect(orcidLink).toBeTruthy();
expect(orcidLink.nativeElement.getAttribute('href')).toBe('https://orcid.org/0000-0002-1825-0097');
expect(orcidLink.nativeElement.textContent).toContain('John Doe');
const orcidBadge = fixture.debugElement.query(By.css('a.orcid-badge'));
expect(orcidBadge).toBeTruthy();
});

it('should render plain text for authority controlled metadata without ORCID', () => {
const nonOrcidRepresentation = Object.assign(new MetadatumRepresentation('type'), {
key: 'dc.contributor.author',
value: 'Jane Smith',
authority: 'some-other-authority',
confidence: 600,
});
comp.mdRepresentation = nonOrcidRepresentation;
fixture.detectChanges();
const orcidLink = fixture.debugElement.query(By.css('a.orcid-link'));
expect(orcidLink).toBeFalsy();
expect(fixture.nativeElement.textContent).toContain('Jane Smith');
});

});
Original file line number Diff line number Diff line change
@@ -1,13 +1,16 @@

import { Component } from '@angular/core';

Check failure on line 2 in src/app/shared/object-list/metadata-representation-list-element/plain-text/plain-text-metadata-list-element.component.ts

View workflow job for this annotation

GitHub Actions / tests (20.x)

Run autofix to sort these imports!

Check failure on line 2 in src/app/shared/object-list/metadata-representation-list-element/plain-text/plain-text-metadata-list-element.component.ts

View workflow job for this annotation

GitHub Actions / tests (18.x)

Run autofix to sort these imports!
import { RouterLink } from '@angular/router';

import { VALUE_LIST_BROWSE_DEFINITION } from '../../../../core/shared/value-list-browse-definition.resource-type';
import { MetadatumRepresentation } from '../../../../core/shared/metadata-representation/metadatum/metadatum-representation.model';
import { hasValue } from '../../../empty.util';
import { MetadataRepresentationListElementComponent } from '../metadata-representation-list-element.component';

@Component({
selector: 'ds-plain-text-metadata-list-element',
templateUrl: './plain-text-metadata-list-element.component.html',
styleUrls: ['./plain-text-metadata-list-element.component.scss'],
standalone: true,
imports: [
RouterLink,
Expand All @@ -31,4 +34,24 @@
}
return queryParams;
}

/**
* Check if this metadata representation has an ORCID authority
*/
isOrcidAuthority(): boolean {
const metadatum = this.mdRepresentation as MetadatumRepresentation;
if (!hasValue(metadatum.authority)) {
return false;
}
const orcidPattern = /^\d{4}-\d{4}-\d{4}-\d{4}$/;
return orcidPattern.test(metadatum.authority);
Comment on lines +42 to +47

Copilot AI Mar 20, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ORCID iDs can have an "X" as the final check digit (e.g. 0000-0002-1694-233X). The current regex ^\d{4}-\d{4}-\d{4}-\d{4}$ will incorrectly return false for valid ORCID values; consider updating the pattern to allow an X in the last position (and ideally hoist the regex/base URL into shared constants to avoid duplication).

Copilot uses AI. Check for mistakes.
}

/**
* Get the ORCID profile URL
*/
getOrcidUrl(): string {
const metadatum = this.mdRepresentation as MetadatumRepresentation;
return `https://orcid.org/${encodeURIComponent(metadatum.authority)}`;
}
}
Loading