Skip to content

Commit 731311b

Browse files
UFAL/ROR identifiers on CLARIN display surfaces + ROR authority i18n labels (#1339, #1337) (#1400)
ADAPT port of the dtq-dev ROR display cluster (FE side of the ROR feature): - a9fc7f5 (#1339): authority-based publisher search links + ROR icon on the CLARIN item page untyped field and the search-result card. - 0ac3e53 (#1337): form.other-information.ror-id / .location i18n labels. Runtime: - clarin-shared-util.ts: new buildAuthoritySearchFilter(searchType, mdValue) centralises the authority-vs-equals operator logic; loadItemAuthors refactored onto it (behaviour-preserving). isEmpty added to the empty.util import. - clarin-generic-item-field: getLinkToSearch rewritten to resolve the full MetadataValue via allMetadata()[index] (so a ROR authority is used); getMetadataValue removed. HTML: ROR <img> added inside the type==='search' anchor, guarded by mdValue.authority && fields includes dc.publisher/creativework.publisher. - clarin-item-box-view: publisherMd via allMetadata(['dc.publisher','creativework.publisher']); hasPublisherRorAuthority flag; publisherRedirectLink via the helper. HTML: [href]->[attr.href] (no transient broken link before assignBaseUrl resolves) + @if(hasPublisherRorAuthority) ROR icon. v9 adaptations: - Templates rewritten to @if (v9 is @if-migrated; fork used *ngIf); BS4 ml-1 -> BS5 ms-1. - item-box-view: did NOT drag in the fork pre-image's metadataLangToBcp47 line (that comes from the not-yet-ported a11y commit d154682). - ror-icon.svg: kept the existing vanilla blob (24735df), NOT the fork blob (add/add). - i18n: location BEFORE orcid, ror-id AFTER orcid (en) / AFTER other-names (cs), commented- English + Czech pair convention. Hand-merged into the CLARIN-modified json5. - Specs (all 3 dropped by the v9 squash) restored & adapted to standalone TestBed: clarin-shared-util.spec.ts (7 tests), clarin-generic-item-field.component.spec.ts (7 tests incl. the AC5 negative-icon rendering test: no ROR icon on dc.subject), clarin-item-box-view.component.spec.ts (10 tests, +provideRouter for the standalone routerLink). Karma: 24/24 green. lint:nobuild: 0 errors. Cluster: BE 00501a2db0 (SimpleRORAuthority) is already deployed (merged BE-3 #1381), so the feature is live-verifiable. Without an authority the FE degrades to the equals fallback. Fulfils CLARIN_V9_POST_SNAPSHOT_SYNC_ACCEPTANCE.md §5 / a9fc7f5 + 0ac3e53 (FE-3, Vlna 3).
1 parent 10a4e27 commit 731311b

10 files changed

Lines changed: 464 additions & 43 deletions

src/app/item-page/simple/field-components/clarin-generic-item-field/clarin-generic-item-field.component.html

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -42,7 +42,7 @@
4242
}
4343
@if (type === 'search') {
4444
<span class="d-inline-flex pe-1">
45-
<a [href]="getLinkToSearch(i)">{{mdValue.value | dsReplace: this.replaceCharacter}}</a>
45+
<a [href]="getLinkToSearch(i)">{{mdValue.value | dsReplace: this.replaceCharacter}} @if (mdValue.authority && (fields?.includes('dc.publisher') || fields?.includes('creativework.publisher'))) {<img src="assets/images/ror-icon.svg" alt="ROR ID" height="16" class="ror-icon ms-1 align-middle">}</a>
4646
@if (!last) {
4747
<span [innerHTML]="separator"></span>
4848
}
Lines changed: 125 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,125 @@
1+
import { NO_ERRORS_SCHEMA } from '@angular/core';
2+
import {
3+
ComponentFixture,
4+
TestBed,
5+
} from '@angular/core/testing';
6+
import { By } from '@angular/platform-browser';
7+
import { TranslateModule } from '@ngx-translate/core';
8+
import { of } from 'rxjs';
9+
10+
import { DSONameService } from '../../../../core/breadcrumbs/dso-name.service';
11+
import { ConfigurationDataService } from '../../../../core/data/configuration-data.service';
12+
import { Item } from '../../../../core/shared/item.model';
13+
import { ClarinGenericItemFieldComponent } from './clarin-generic-item-field.component';
14+
15+
describe('ClarinGenericItemFieldComponent', () => {
16+
let component: ClarinGenericItemFieldComponent;
17+
let fixture: ComponentFixture<ClarinGenericItemFieldComponent>;
18+
19+
const configurationServiceSpy = jasmine.createSpyObj('configurationService', {
20+
findByPropertyName: of(true),
21+
});
22+
const dsoNameServiceSpy = jasmine.createSpyObj('dsoNameService', ['getName']);
23+
24+
/** Build an Item carrying a single metadata value (dc.publisher by default) with the given authority. */
25+
function itemWith(authority: string | null, field = 'dc.publisher', value = 'ACME Press'): Item {
26+
const item = new Item();
27+
item.metadata = {
28+
[field]: [
29+
{
30+
value,
31+
authority,
32+
confidence: authority ? 600 : -1,
33+
place: 0,
34+
language: null,
35+
uuid: 'mock-uuid',
36+
isVirtual: false,
37+
virtualValue: null,
38+
} as any,
39+
],
40+
};
41+
return item;
42+
}
43+
44+
beforeEach(async () => {
45+
await TestBed.configureTestingModule({
46+
imports: [
47+
TranslateModule.forRoot(),
48+
ClarinGenericItemFieldComponent,
49+
],
50+
providers: [
51+
{ provide: ConfigurationDataService, useValue: configurationServiceSpy },
52+
{ provide: DSONameService, useValue: dsoNameServiceSpy },
53+
],
54+
schemas: [NO_ERRORS_SCHEMA],
55+
}).compileComponents();
56+
});
57+
58+
beforeEach(() => {
59+
fixture = TestBed.createComponent(ClarinGenericItemFieldComponent);
60+
component = fixture.componentInstance;
61+
// Avoid ngOnInit (detectChanges) for the isolation tests so we can exercise getLinkToSearch directly.
62+
component.baseUrl = 'http://localhost:4000';
63+
});
64+
65+
it('should create', () => {
66+
expect(component).toBeTruthy();
67+
});
68+
69+
describe('getLinkToSearch', () => {
70+
it('uses the authority operator and key when the metadata value has an authority', () => {
71+
component.item = itemWith('02mhbdp94');
72+
component.fields = ['dc.publisher'];
73+
expect(component.getLinkToSearch(0))
74+
.toBe('http://localhost:4000/search?f.publisher=02mhbdp94,authority');
75+
});
76+
77+
it('uses the equals operator and plain value when the metadata value has no authority', () => {
78+
component.item = itemWith(null);
79+
component.fields = ['dc.publisher'];
80+
expect(component.getLinkToSearch(0))
81+
.toBe('http://localhost:4000/search?f.publisher=ACME%20Press,equals');
82+
});
83+
84+
it('uses the explicitly provided value (e.g. a split subject) with the equals operator', () => {
85+
const item = new Item();
86+
item.metadata = {
87+
'dc.subject': [
88+
{ value: 'history;art', authority: null, confidence: -1, place: 0, language: null } as any,
89+
],
90+
};
91+
component.item = item;
92+
component.fields = ['dc.subject'];
93+
expect(component.getLinkToSearch(-1, 'history'))
94+
.toBe('http://localhost:4000/search?f.subject=history,equals');
95+
});
96+
97+
it('falls back to the bare search endpoint when the index is out of range', () => {
98+
component.item = itemWith(null);
99+
component.fields = ['dc.publisher'];
100+
expect(component.getLinkToSearch(5)).toBe('http://localhost:4000/search');
101+
});
102+
});
103+
104+
describe('ROR icon rendering (guard scope)', () => {
105+
it('renders the ROR icon for an authority-bearing dc.publisher search field', () => {
106+
component.item = itemWith('02mhbdp94', 'dc.publisher');
107+
component.fields = ['dc.publisher'];
108+
component.type = 'search';
109+
fixture.detectChanges();
110+
const img = fixture.debugElement.query(By.css('img.ror-icon'));
111+
expect(img).not.toBeNull();
112+
expect(img.nativeElement.getAttribute('src')).toContain('ror-icon.svg');
113+
});
114+
115+
it('does NOT render the ROR icon for an authority-bearing NON-publisher field (dc.subject)', () => {
116+
component.item = itemWith('some-authority', 'dc.subject', 'History');
117+
component.fields = ['dc.subject'];
118+
component.type = 'search';
119+
fixture.detectChanges();
120+
const imgs = fixture.debugElement.queryAll(By.css('img'))
121+
.filter((de) => (de.nativeElement.getAttribute('src') || '').includes('ror-icon.svg'));
122+
expect(imgs.length).toBe(0);
123+
});
124+
});
125+
});

src/app/item-page/simple/field-components/clarin-generic-item-field/clarin-generic-item-field.component.ts

Lines changed: 9 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ import { Item } from '../../../../core/shared/item.model';
1818
import { getFirstSucceededRemoteDataPayload } from '../../../../core/shared/operators';
1919
import { ClarinItemAuthorPreviewComponent } from '../../../../shared/clarin-item-author-preview/clarin-item-author-preview.component';
2020
import {
21+
buildAuthoritySearchFilter,
2122
convertMetadataFieldIntoSearchType,
2223
getBaseUrl,
2324
} from '../../../../shared/clarin-shared-util';
@@ -172,38 +173,17 @@ export class ClarinGenericItemFieldComponent implements OnInit {
172173
* @param index
173174
*/
174175
public getLinkToSearch(index, value = '') {
175-
let metadataValue = 'Error: value is empty';
176-
if (isEmpty(value)) {
177-
// Get metadata value from the Item's metadata field
178-
metadataValue = this.getMetadataValue(index);
179-
} else {
180-
// The metadata value is passed from the parameter.
181-
metadataValue = value;
182-
}
183-
184176
const searchType = convertMetadataFieldIntoSearchType(this.fields);
185-
return this.baseUrl + '/search?f.' + encodeURIComponent(searchType) + '=' +
186-
encodeURIComponent(metadataValue) + ',equals';
187-
}
188177

189-
/**
190-
* If the metadata field has more than 1 value return the value based on the index.
191-
* @param index of the metadata value
192-
*/
193-
public getMetadataValue(index) {
194-
let metadataValue = '';
195-
if (index === 0) {
196-
// Return first metadata value.
197-
return this.item.firstMetadataValue(this.fields);
178+
// If a value is explicitly provided (e.g. a single subject from a split list), search by that plain value.
179+
// Otherwise resolve the full MetadataValue for this index so an authority (e.g. ROR) can be used.
180+
const mdValue = !isEmpty(value) ? { value } : this.item.allMetadata(this.fields)?.[index];
181+
if (!mdValue) {
182+
// ultimate fallback (should not happen)
183+
return this.baseUrl + '/search';
198184
}
199-
// The metadata field has more metadata values - get the actual one
200-
this.item.allMetadataValues(this.fields)?.forEach((metadataValueArray, arrayIndex) => {
201-
if (index !== arrayIndex) {
202-
return metadataValue;
203-
}
204-
metadataValue = metadataValueArray;
205-
});
206-
return metadataValue;
185+
186+
return this.baseUrl + '/search?' + buildAuthoritySearchFilter(searchType, mdValue);
207187
}
208188

209189
/**

src/app/shared/clarin-item-box-view/clarin-item-box-view.component.html

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@
2222
<!-- truthiness check: a missing dc.publisher is undefined, and an empty <a>
2323
has no accessible name (axe link-name) -->
2424
<span>(@if (itemPublisher) {
25-
<span><a [href]="publisherRedirectLink">{{itemPublisher}}</a> / </span>
25+
<span><a [attr.href]="publisherRedirectLink">{{itemPublisher}} @if (hasPublisherRorAuthority) {<img src="assets/images/ror-icon.svg" alt="ROR ID" height="16" class="ror-icon ms-1 align-middle">}</a> / </span>
2626
}{{itemDate}})</span>
2727
</div>
2828
}

0 commit comments

Comments
 (0)