Skip to content

Commit 43562d8

Browse files
KasinhouMatus Kasak
andauthored
ZCU-PUB/Update author redirect based on configuration (#1298)
* Author redirect based on configuration - browse/orcid * Changed name of property * Update spec --------- Co-authored-by: Matus Kasak <matus.kasak@dataquest.sk>
1 parent a6b90a3 commit 43562d8

7 files changed

Lines changed: 239 additions & 65 deletions

File tree

src/app/shared/object-list/metadata-representation-list-element/plain-text/plain-text-metadata-list-element.component.html

Lines changed: 33 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -8,20 +8,41 @@
88
{{mdRepresentation.getValue()}}
99
</a>
1010
<ng-container *ngIf="(mdRepresentation.representationType=='authority_controlled')">
11-
<ng-container *ngIf="orcidDomainUrl$ | async as orcidDomainUrl; else noOrcidDomain">
12-
<a *ngIf="isOrcidAuthority(orcidDomainUrl); else plainAuthority"
13-
class="dont-break-out orcid-author-link"
14-
[href]="getOrcidUrl(orcidDomainUrl)"
15-
target="_blank"
16-
rel="noopener noreferrer">
17-
{{mdRepresentation.getValue()}}
18-
<i class="fa-brands fa-orcid orcid-icon" aria-hidden="true"></i>
19-
</a>
20-
<ng-template #plainAuthority>
21-
<span class="dont-break-out">{{mdRepresentation.getValue()}}</span>
11+
<ng-container *ngIf="isOrcidAuthority(); else plainAuthority">
12+
<ng-container *ngIf="(authorOrcidLinkTarget$ | async) === 'orcid'; else searchModeOrcid">
13+
<!-- linkTarget === 'orcid': the whole author name + icon link to the ORCID profile. -->
14+
<a class="dont-break-out orcid-author-link"
15+
[href]="getOrcidUrl()"
16+
target="_blank"
17+
rel="noopener noreferrer"
18+
[title]="'item.view.box.author.preview.orcid-link.title' | translate"
19+
[attr.aria-label]="('item.view.box.author.preview.orcid-link.title' | translate) + ' ' + mdRepresentation.getValue()">
20+
{{mdRepresentation.getValue()}}
21+
<i class="fa-brands fa-orcid orcid-icon" aria-hidden="true"></i>
22+
</a>
23+
</ng-container>
24+
<ng-template #searchModeOrcid>
25+
<!-- Default linkTarget === 'browse': author name navigates to the browse-by-author page;
26+
the ORCID icon is rendered as a separate link to the ORCID profile. -->
27+
<ng-container *ngIf="hasBrowseDefinition(); else searchModeOrcidNoBrowse">
28+
<a class="dont-break-out ds-browse-link"
29+
[routerLink]="['/browse/', mdRepresentation.browseDefinition.id]"
30+
[queryParams]="getAuthorityBrowseQueryParams()">
31+
{{mdRepresentation.getValue()}}
32+
</a>
33+
</ng-container>
34+
<ng-template #searchModeOrcidNoBrowse>
35+
<span class="dont-break-out">{{mdRepresentation.getValue()}}</span>
36+
</ng-template>
37+
<a class="orcid-icon-link"
38+
[href]="getOrcidUrl()"
39+
target="_blank"
40+
rel="noopener noreferrer"
41+
[title]="'item.view.box.author.preview.orcid-link.title' | translate"><i
42+
class="fa-brands fa-orcid orcid-icon" aria-hidden="true"></i></a>
2243
</ng-template>
2344
</ng-container>
24-
<ng-template #noOrcidDomain>
45+
<ng-template #plainAuthority>
2546
<span class="dont-break-out">{{mdRepresentation.getValue()}}</span>
2647
</ng-template>
2748
</ng-container>

src/app/shared/object-list/metadata-representation-list-element/plain-text/plain-text-metadata-list-element.component.scss

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,14 @@
66
}
77
}
88

9+
.orcid-icon-link {
10+
text-decoration: none;
11+
12+
&:hover {
13+
opacity: 0.8;
14+
}
15+
}
16+
917
.orcid-icon {
1018
color: #a6ce39; // Official ORCID green color
1119
font-size: 1em;

src/app/shared/object-list/metadata-representation-list-element/plain-text/plain-text-metadata-list-element.component.spec.ts

Lines changed: 20 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import { ChangeDetectionStrategy, NO_ERRORS_SCHEMA } from '@angular/core';
22
import { By } from '@angular/platform-browser';
33
import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing';
4+
import { TranslateModule } from '@ngx-translate/core';
45

56
import { ConfigurationDataService } from '../../../../core/data/configuration-data.service';
67
import { MetadatumRepresentation } from '../../../../core/shared/metadata-representation/metadatum/metadatum-representation.model';
@@ -34,9 +35,12 @@ const mockOrcidWithWhitespaceRepresentation = Object.assign(new MetadatumReprese
3435
});
3536

3637
const mockConfigurationDataService = {
37-
findByPropertyName: jasmine.createSpy('findByPropertyName').and.returnValue(
38-
createSuccessfulRemoteDataObject$({ values: ['https://orcid.org'] }),
39-
),
38+
findByPropertyName: jasmine.createSpy('findByPropertyName').and.callFake((property: string) => {
39+
if (property === 'orcid.author.link-target') {
40+
return createSuccessfulRemoteDataObject$({ values: ['browse'] });
41+
}
42+
return createSuccessfulRemoteDataObject$({ values: ['https://orcid.org'] });
43+
}),
4044
};
4145

4246
describe('PlainTextMetadataListElementComponent', () => {
@@ -45,7 +49,7 @@ describe('PlainTextMetadataListElementComponent', () => {
4549

4650
beforeEach(waitForAsync(() => {
4751
TestBed.configureTestingModule({
48-
imports: [],
52+
imports: [TranslateModule.forRoot()],
4953
declarations: [PlainTextMetadataListElementComponent],
5054
providers: [
5155
{ provide: ConfigurationDataService, useValue: mockConfigurationDataService },
@@ -74,6 +78,7 @@ describe('PlainTextMetadataListElementComponent', () => {
7478
describe('when metadata has ORCID authority', () => {
7579
beforeEach(() => {
7680
comp.mdRepresentation = mockOrcidRepresentation;
81+
comp.authorOrcidLinkTarget$.next('orcid');
7782
fixture.detectChanges();
7883
});
7984

@@ -90,11 +95,11 @@ describe('PlainTextMetadataListElementComponent', () => {
9095
});
9196

9297
it('isOrcidAuthority should return true', () => {
93-
expect(comp.isOrcidAuthority(comp.orcidDomainUrl$.value)).toBeTrue();
98+
expect(comp.isOrcidAuthority()).toBeTrue();
9499
});
95100

96101
it('getOrcidUrl should return full ORCID URL', () => {
97-
expect(comp.getOrcidUrl(comp.orcidDomainUrl$.value)).toBe('https://orcid.org/1234-5678-9012-3456');
102+
expect(comp.getOrcidUrl()).toBe('https://orcid.org/1234-5678-9012-3456');
98103
});
99104
});
100105

@@ -116,21 +121,21 @@ describe('PlainTextMetadataListElementComponent', () => {
116121
});
117122

118123
it('isOrcidAuthority should return false', () => {
119-
expect(comp.isOrcidAuthority(comp.orcidDomainUrl$.value)).toBeFalse();
124+
expect(comp.isOrcidAuthority()).toBeFalse();
120125
});
121126
});
122127

123128
describe('getOrcidUrl with trailing slash handling', () => {
124129
it('should not double-slash when domain URL ends with /', () => {
125130
comp.orcidDomainUrl$.next('https://orcid.org/');
126131
comp.mdRepresentation = mockOrcidRepresentation;
127-
expect(comp.getOrcidUrl('https://orcid.org/')).toBe('https://orcid.org/1234-5678-9012-3456');
132+
expect(comp.getOrcidUrl()).toBe('https://orcid.org/1234-5678-9012-3456');
128133
});
129134

130135
it('should add slash when domain URL does not end with /', () => {
131136
comp.orcidDomainUrl$.next('https://sandbox.orcid.org');
132137
comp.mdRepresentation = mockOrcidRepresentation;
133-
expect(comp.getOrcidUrl('https://sandbox.orcid.org')).toBe('https://sandbox.orcid.org/1234-5678-9012-3456');
138+
expect(comp.getOrcidUrl()).toBe('https://sandbox.orcid.org/1234-5678-9012-3456');
134139
});
135140
});
136141

@@ -147,25 +152,25 @@ describe('PlainTextMetadataListElementComponent', () => {
147152
});
148153

149154
it('isOrcidAuthority should return false', () => {
150-
expect(comp.isOrcidAuthority(comp.orcidDomainUrl$.value)).toBeFalse();
155+
expect(comp.isOrcidAuthority()).toBeFalse();
151156
});
152157

153158
it('getOrcidUrl should return empty string', () => {
154-
expect(comp.getOrcidUrl(comp.orcidDomainUrl$.value)).toBe('');
159+
expect(comp.getOrcidUrl()).toBe('');
155160
});
156161
});
157162

158163
describe('getOrcidUrl defensive behavior', () => {
159164
it('should return empty string when orcidDomainUrl is null', () => {
160165
comp.orcidDomainUrl$.next(null);
161166
comp.mdRepresentation = mockOrcidRepresentation;
162-
expect(comp.getOrcidUrl(null)).toBe('');
167+
expect(comp.getOrcidUrl()).toBe('');
163168
});
164169

165170
it('should return empty string when mdRepresentation has no authority', () => {
166171
comp.orcidDomainUrl$.next('https://orcid.org');
167172
comp.mdRepresentation = mockMetadataRepresentation;
168-
expect(comp.getOrcidUrl('https://orcid.org')).toBe('');
173+
expect(comp.getOrcidUrl()).toBe('');
169174
});
170175
});
171176

@@ -177,11 +182,11 @@ describe('PlainTextMetadataListElementComponent', () => {
177182
});
178183

179184
it('isOrcidAuthority should return true after trimming', () => {
180-
expect(comp.isOrcidAuthority(comp.orcidDomainUrl$.value)).toBeTrue();
185+
expect(comp.isOrcidAuthority()).toBeTrue();
181186
});
182187

183188
it('getOrcidUrl should return trimmed ORCID URL', () => {
184-
expect(comp.getOrcidUrl(comp.orcidDomainUrl$.value)).toBe('https://orcid.org/1234-5678-9012-3456');
189+
expect(comp.getOrcidUrl()).toBe('https://orcid.org/1234-5678-9012-3456');
185190
});
186191
});
187192
});

src/app/shared/object-list/metadata-representation-list-element/plain-text/plain-text-metadata-list-element.component.ts

Lines changed: 54 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -4,9 +4,16 @@ import { BehaviorSubject } from 'rxjs';
44
import { ConfigurationDataService } from '../../../../core/data/configuration-data.service';
55
import { MetadataRepresentationType } from '../../../../core/shared/metadata-representation/metadata-representation.model';
66
import { MetadatumRepresentation } from '../../../../core/shared/metadata-representation/metadatum/metadatum-representation.model';
7-
import { getFirstCompletedRemoteData } from '../../../../core/shared/operators';
87
import { VALUE_LIST_BROWSE_DEFINITION } from '../../../../core/shared/value-list-browse-definition.resource-type';
98
import { metadataRepresentationComponent } from '../../../metadata-representation/metadata-representation.decorator';
9+
import {
10+
AuthorOrcidLinkTarget,
11+
buildOrcidProfileUrl,
12+
DEFAULT_AUTHOR_ORCID_LINK_TARGET,
13+
isOrcidAuthorityValue,
14+
loadAuthorOrcidLinkTarget,
15+
loadOrcidDomainUrl,
16+
} from '../../../utils/orcid-author.util';
1017
import { MetadataRepresentationListElementComponent } from '../metadata-representation-list-element.component';
1118

1219
@metadataRepresentationComponent('Publication', MetadataRepresentationType.PlainText)
@@ -24,30 +31,23 @@ import { MetadataRepresentationListElementComponent } from '../metadata-represen
2431
export class PlainTextMetadataListElementComponent extends MetadataRepresentationListElementComponent implements OnInit {
2532

2633
/**
27-
* Regex pattern for ORCID identifiers: four groups of four digits separated by hyphens.
28-
* The last group may end with an X (checksum digit).
34+
* ORCID domain URL loaded from the backend.
2935
*/
30-
private static readonly ORCID_PATTERN = /^\d{4}-\d{4}-\d{4}-(\d{3}X|\d{4})$/;
31-
3236
orcidDomainUrl$ = new BehaviorSubject<string | null>(null);
3337

38+
/**
39+
* Target of the link rendered on the author name for an ORCID author. Loaded from the
40+
* backend property `orcid.author.link-target`.
41+
*/
42+
authorOrcidLinkTarget$ = new BehaviorSubject<AuthorOrcidLinkTarget>(DEFAULT_AUTHOR_ORCID_LINK_TARGET);
43+
3444
constructor(private configurationService: ConfigurationDataService) {
3545
super();
3646
}
3747

3848
ngOnInit(): void {
39-
this.configurationService.findByPropertyName('orcid.domain-url').pipe(
40-
getFirstCompletedRemoteData(),
41-
).subscribe((rd) => {
42-
if (rd.hasFailed || !rd.hasSucceeded || !rd.payload?.values?.length) {
43-
return;
44-
}
45-
46-
const url = rd.payload.values[0]?.trim();
47-
if (url && /^https?:\/\//i.test(url)) {
48-
this.orcidDomainUrl$.next(url);
49-
}
50-
});
49+
loadOrcidDomainUrl(this.configurationService).then((url) => this.orcidDomainUrl$.next(url));
50+
loadAuthorOrcidLinkTarget(this.configurationService).then((t) => this.authorOrcidLinkTarget$.next(t));
5151
}
5252

5353
/**
@@ -62,30 +62,46 @@ export class PlainTextMetadataListElementComponent extends MetadataRepresentatio
6262
return queryParams;
6363
}
6464

65-
isOrcidAuthority(orcidDomainUrl: string | null): boolean {
66-
if (orcidDomainUrl === null) {
67-
return false;
68-
}
69-
if (this.mdRepresentation instanceof MetadatumRepresentation) {
70-
const authority = this.mdRepresentation.authority?.trim();
71-
return !!authority && PlainTextMetadataListElementComponent.ORCID_PATTERN.test(authority);
72-
}
73-
return false;
65+
/**
66+
* Query parameters for the browse link of an authority-controlled value (e.g. ORCID author).
67+
* Passes both `value` and `authority` so the browse page can call the REST endpoint with
68+
* `filterValue` + `filterAuthority` and resolve items whose metadata is indexed by authority key.
69+
*/
70+
getAuthorityBrowseQueryParams() {
71+
return {
72+
value: this.mdRepresentation.getValue(),
73+
authority: this.getAuthority(),
74+
};
7475
}
7576

76-
getOrcidUrl(orcidDomainUrl: string | null): string {
77-
if (orcidDomainUrl === null) {
78-
return '';
79-
}
80-
const authority = this.mdRepresentation instanceof MetadatumRepresentation
81-
? this.mdRepresentation.authority?.trim()
82-
: undefined;
77+
/**
78+
* True when the current metadatum carries a browse definition (e.g. `dc.contributor.author`),
79+
* which means the value can be rendered as a clickable browse link.
80+
*/
81+
hasBrowseDefinition(): boolean {
82+
return !!this.mdRepresentation?.browseDefinition;
83+
}
8384

84-
if (!authority || !PlainTextMetadataListElementComponent.ORCID_PATTERN.test(authority)) {
85-
return '';
86-
}
85+
/**
86+
* Check whether the authority value of this metadata is an ORCID identifier.
87+
* Accepts either a bare ORCID iD or a full ORCID URL.
88+
*/
89+
isOrcidAuthority(): boolean {
90+
return isOrcidAuthorityValue(this.getAuthority(), this.orcidDomainUrl$.value);
91+
}
8792

88-
const base = orcidDomainUrl.endsWith('/') ? orcidDomainUrl : orcidDomainUrl + '/';
89-
return `${base}${authority}`;
93+
/**
94+
* Build the full ORCID profile URL for the current author. Returns an empty string when
95+
* the authority is not an ORCID value or when the ORCID domain URL is required but missing.
96+
*/
97+
getOrcidUrl(): string {
98+
return buildOrcidProfileUrl(this.getAuthority(), this.orcidDomainUrl$.value);
99+
}
100+
101+
private getAuthority(): string | undefined {
102+
if (this.mdRepresentation instanceof MetadatumRepresentation) {
103+
return this.mdRepresentation.authority?.trim();
104+
}
105+
return undefined;
90106
}
91107
}

0 commit comments

Comments
 (0)