From 3225693bb2843a8753b17bc527e451cd86b3b4f1 Mon Sep 17 00:00:00 2001 From: Juraj Roka <95219754+jr-rk@users.noreply.github.com> Date: Tue, 10 Mar 2026 16:34:04 +0100 Subject: [PATCH 1/5] feature: based on value of metadata field isUrl - decide whether clickable or not --- src/app/item-page/full/full-item-page.component.html | 8 +++++++- src/app/item-page/full/full-item-page.component.ts | 7 +++++++ 2 files changed, 14 insertions(+), 1 deletion(-) diff --git a/src/app/item-page/full/full-item-page.component.html b/src/app/item-page/full/full-item-page.component.html index 0e0db894b1d..eebd9e45d13 100644 --- a/src/app/item-page/full/full-item-page.component.html +++ b/src/app/item-page/full/full-item-page.component.html @@ -24,7 +24,13 @@ @for (mdValue of mdEntry.value; track mdValue) { {{mdEntry.key}} - {{mdValue.value}} + + @if (isUrl(mdValue.value)) { + {{mdValue.value}} + } @else { + {{mdValue.value}} + } + {{mdValue.language}} } diff --git a/src/app/item-page/full/full-item-page.component.ts b/src/app/item-page/full/full-item-page.component.ts index 87b01f21e97..fd76c400ed0 100644 --- a/src/app/item-page/full/full-item-page.component.ts +++ b/src/app/item-page/full/full-item-page.component.ts @@ -119,6 +119,13 @@ export class FullItemPageComponent extends ItemPageComponent implements OnInit, ); } + /** + * Check if a metadata value is a URL. + */ + isUrl(value: string): boolean { + return !!value && (value.startsWith('http://') || value.startsWith('https://')); + } + /** * Navigate back in browser history. */ From a44142d1c46ed0c32fed301912d16547313156f1 Mon Sep 17 00:00:00 2001 From: Juraj Roka <95219754+jr-rk@users.noreply.github.com> Date: Tue, 10 Mar 2026 16:34:37 +0100 Subject: [PATCH 2/5] test: add unit test for new method --- .../full/full-item-page.component.spec.ts | 70 +++++++++++++++++++ 1 file changed, 70 insertions(+) diff --git a/src/app/item-page/full/full-item-page.component.spec.ts b/src/app/item-page/full/full-item-page.component.spec.ts index ac2b4634a83..ce435259b1a 100644 --- a/src/app/item-page/full/full-item-page.component.spec.ts +++ b/src/app/item-page/full/full-item-page.component.spec.ts @@ -65,6 +65,30 @@ const mockItem: Item = Object.assign(new Item(), { }, }); +const mockItemWithUrl: Item = Object.assign(new Item(), { + bundles: createSuccessfulRemoteDataObject$(createPaginatedList([])), + metadata: { + 'dc.title': [ + { + language: 'en_US', + value: 'test item', + }, + ], + 'dc.identifier.uri': [ + { + language: null, + value: 'https://hdl.handle.net/123456789/1', + }, + ], + 'dc.subject': [ + { + language: 'en_US', + value: 'plain text value', + }, + ], + }, +}); + const mockWithdrawnItem: Item = Object.assign(new Item(), { bundles: createSuccessfulRemoteDataObject$(createPaginatedList([])), metadata: [], @@ -265,4 +289,50 @@ describe('FullItemPageComponent', () => { expect(linkHeadService.addTag).toHaveBeenCalledTimes(3); }); }); + + describe('isUrl', () => { + it('should return true for https URLs', () => { + expect(comp.isUrl('https://example.com')).toBeTrue(); + }); + + it('should return true for http URLs', () => { + expect(comp.isUrl('http://example.com')).toBeTrue(); + }); + + it('should return false for plain text', () => { + expect(comp.isUrl('just some text')).toBeFalse(); + }); + + it('should return false for null', () => { + expect(comp.isUrl(null)).toBeFalse(); + }); + + it('should return false for undefined', () => { + expect(comp.isUrl(undefined)).toBeFalse(); + }); + }); + + describe('metadata URL rendering', () => { + beforeEach(() => { + comp.metadata$ = of(mockItemWithUrl.metadata); + fixture.detectChanges(); + }); + + it('should render URL metadata values as clickable links', () => { + const links = fixture.debugElement.queryAll(By.css('table a')); + const urlLink = links.find(l => l.nativeElement.textContent.includes('https://hdl.handle.net/123456789/1')); + expect(urlLink).toBeTruthy(); + expect(urlLink.nativeElement.getAttribute('href')).toBe('https://hdl.handle.net/123456789/1'); + expect(urlLink.nativeElement.getAttribute('target')).toBe('_blank'); + expect(urlLink.nativeElement.getAttribute('rel')).toBe('noopener noreferrer'); + }); + + it('should render non-URL metadata values as plain text', () => { + const table = fixture.debugElement.query(By.css('table')); + const links = fixture.debugElement.queryAll(By.css('table a')); + expect(table.nativeElement.innerHTML).toContain('plain text value'); + const plainTextLink = links.find(l => l.nativeElement.textContent.includes('plain text value')); + expect(plainTextLink).toBeFalsy(); + }); + }); }); From 63538f0c0a64fae1034fc62beaafa940bf6aee0d Mon Sep 17 00:00:00 2001 From: Juraj Roka <95219754+jr-rk@users.noreply.github.com> Date: Tue, 10 Mar 2026 16:45:10 +0100 Subject: [PATCH 3/5] refactor: rename isUrl to isHttpUrl, accept null/undefined, and normalize input with trim/lowercase --- src/app/item-page/full/full-item-page.component.html | 2 +- .../item-page/full/full-item-page.component.spec.ts | 12 ++++++------ src/app/item-page/full/full-item-page.component.ts | 7 ++++--- 3 files changed, 11 insertions(+), 10 deletions(-) diff --git a/src/app/item-page/full/full-item-page.component.html b/src/app/item-page/full/full-item-page.component.html index eebd9e45d13..d96351648d8 100644 --- a/src/app/item-page/full/full-item-page.component.html +++ b/src/app/item-page/full/full-item-page.component.html @@ -25,7 +25,7 @@ {{mdEntry.key}} - @if (isUrl(mdValue.value)) { + @if (isHttpUrl(mdValue.value)) { {{mdValue.value}} } @else { {{mdValue.value}} diff --git a/src/app/item-page/full/full-item-page.component.spec.ts b/src/app/item-page/full/full-item-page.component.spec.ts index ce435259b1a..e2c505fdfb3 100644 --- a/src/app/item-page/full/full-item-page.component.spec.ts +++ b/src/app/item-page/full/full-item-page.component.spec.ts @@ -290,25 +290,25 @@ describe('FullItemPageComponent', () => { }); }); - describe('isUrl', () => { + describe('isHttpUrl', () => { it('should return true for https URLs', () => { - expect(comp.isUrl('https://example.com')).toBeTrue(); + expect(comp.isHttpUrl('https://example.com')).toBeTrue(); }); it('should return true for http URLs', () => { - expect(comp.isUrl('http://example.com')).toBeTrue(); + expect(comp.isHttpUrl('http://example.com')).toBeTrue(); }); it('should return false for plain text', () => { - expect(comp.isUrl('just some text')).toBeFalse(); + expect(comp.isHttpUrl('just some text')).toBeFalse(); }); it('should return false for null', () => { - expect(comp.isUrl(null)).toBeFalse(); + expect(comp.isHttpUrl(null)).toBeFalse(); }); it('should return false for undefined', () => { - expect(comp.isUrl(undefined)).toBeFalse(); + expect(comp.isHttpUrl(undefined)).toBeFalse(); }); }); diff --git a/src/app/item-page/full/full-item-page.component.ts b/src/app/item-page/full/full-item-page.component.ts index fd76c400ed0..10ccafdc762 100644 --- a/src/app/item-page/full/full-item-page.component.ts +++ b/src/app/item-page/full/full-item-page.component.ts @@ -120,10 +120,11 @@ export class FullItemPageComponent extends ItemPageComponent implements OnInit, } /** - * Check if a metadata value is a URL. + * Check if a metadata value is an HTTP(S) URL. */ - isUrl(value: string): boolean { - return !!value && (value.startsWith('http://') || value.startsWith('https://')); + isHttpUrl(value: string | null | undefined): boolean { + const v = value?.trim().toLowerCase(); + return !!v && (v.startsWith('http://') || v.startsWith('https://')); } /** From 771e5a6d34a03b7f485dcfa009b118a3bb0efd81 Mon Sep 17 00:00:00 2001 From: Juraj Roka <95219754+jr-rk@users.noreply.github.com> Date: Tue, 10 Mar 2026 16:54:25 +0100 Subject: [PATCH 4/5] refactor: trimmed values used in consistent way --- src/app/item-page/full/full-item-page.component.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/app/item-page/full/full-item-page.component.html b/src/app/item-page/full/full-item-page.component.html index d96351648d8..63ff0a2441b 100644 --- a/src/app/item-page/full/full-item-page.component.html +++ b/src/app/item-page/full/full-item-page.component.html @@ -26,7 +26,7 @@ {{mdEntry.key}} @if (isHttpUrl(mdValue.value)) { - {{mdValue.value}} + {{mdValue.value.trim()}} } @else { {{mdValue.value}} } From f4a6bc0a639b5478a11599f4ac7e1fa90f56faf0 Mon Sep 17 00:00:00 2001 From: Juraj Roka <95219754+jr-rk@users.noreply.github.com> Date: Tue, 10 Mar 2026 17:04:02 +0100 Subject: [PATCH 5/5] test: drive metadata URL rendering tests through route data to exercise the real itemRD$ --- src/app/item-page/full/full-item-page.component.spec.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/app/item-page/full/full-item-page.component.spec.ts b/src/app/item-page/full/full-item-page.component.spec.ts index e2c505fdfb3..cab8dd68ff2 100644 --- a/src/app/item-page/full/full-item-page.component.spec.ts +++ b/src/app/item-page/full/full-item-page.component.spec.ts @@ -314,7 +314,8 @@ describe('FullItemPageComponent', () => { describe('metadata URL rendering', () => { beforeEach(() => { - comp.metadata$ = of(mockItemWithUrl.metadata); + routeData.dso = createSuccessfulRemoteDataObject(mockItemWithUrl); + comp.ngOnInit(); fixture.detectChanges(); });