MENDELU/Convert full item metadata to hyperlinks#1246
MENDELU/Convert full item metadata to hyperlinks#1246jr-rk wants to merge 10 commits intocustomer/mendelufrom
Conversation
There was a problem hiding this comment.
Pull request overview
Updates the full item view to render URL-like metadata values as clickable hyperlinks, improving usability when metadata contains resolvable links (e.g., handle URLs).
Changes:
- Added an
isUrlhelper onFullItemPageComponentto detect http(s) URLs. - Updated the full item metadata table to render detected URLs as
<a>links (new tab +noopener noreferrer). - Added unit tests covering URL detection and link rendering behavior in the metadata table.
Reviewed changes
Copilot reviewed 3 out of 3 changed files in this pull request and generated 1 comment.
| File | Description |
|---|---|
| src/app/item-page/full/full-item-page.component.ts | Introduces isUrl helper used by the template to decide link vs. plain text rendering. |
| src/app/item-page/full/full-item-page.component.html | Renders metadata values as <a> when isUrl(...) returns true. |
| src/app/item-page/full/full-item-page.component.spec.ts | Adds tests for isUrl and verifies URL values render as clickable links in the metadata table. |
…lize input with trim/lowercase
| <a [routerLink]="link.routerLink" [queryParams]="link.queryParams">{{mdValue.value}}</a> | ||
| } | ||
| } @else { | ||
| <span [innerHTML]="makeLinks(mdValue.value)"></span> |
There was a problem hiding this comment.
[innerHTML]="makeLinks(mdValue.value)" changes escaping semantics compared to {{mdValue.value}}: any HTML present in metadata values will now be rendered (after Angular sanitization) instead of displayed as plain text. If metadata is not meant to support HTML markup, consider avoiding innerHTML here and rendering links via bindings (or escaping the non-URL parts before inserting <a> tags) so the rest of the value remains text.
| <span [innerHTML]="makeLinks(mdValue.value)"></span> | |
| <span>{{mdValue.value}}</span> |
src/app/shared/utils/make-links.ts
Outdated
| export function makeLinks(text: string): string { | ||
| const regex = /(?:https?|ftp):\/\/[^\s)]+|www\.[^\s)]+/g; | ||
| return text?.replace(regex, (url) => { | ||
| const href = url.startsWith('www.') ? `https://${url}` : url; | ||
| return `<a href="${href}" target="_blank" rel="noopener noreferrer">${url}</a>`; | ||
| }); |
There was a problem hiding this comment.
makeLinks is typed as (text: string) => string, but it uses optional chaining and can return undefined (and callers/tests pass null/undefined). This makes the util’s contract misleading for other consumers. Update the signature to accept string | null | undefined and return string | undefined (or normalize to an empty string) so the type reflects actual behavior.
| // --- Internal search configuration --- | ||
| const SEARCH_PATH = '/search'; | ||
| const SEARCH_FILTER_SUFFIX = ',equals'; | ||
|
|
||
| /** Maps metadata keys to their corresponding search filter parameter. */ | ||
| const SEARCH_FIELD_FILTERS: Record<string, string> = { | ||
| 'dc.subject': 'f.subject', | ||
| 'dc.contributor.author': 'f.author', | ||
| }; |
There was a problem hiding this comment.
For internal search links, the filter value is built by manually appending the operator suffix (,equals). The repo already has addOperatorToFilterValue in shared/search/search.utils.ts, which centralizes operator handling and avoids duplicating the operator list/regex behavior. Consider using that helper here instead of SEARCH_FILTER_SUFFIX + string concatenation.
| it('should convert URLs embedded in text', () => { | ||
| const result = makeLinks('Visit https://example.com for details'); | ||
| expect(result).toContain('<a href="https://example.com"'); | ||
| expect(result).toContain('Visit'); | ||
| expect(result).toContain('for details'); | ||
| }); | ||
|
|
||
| it('should handle DOI / handle redirect URLs', () => { | ||
| const result = makeLinks('https://hdl.handle.net/123456789/1'); | ||
| expect(result).toContain('<a href="https://hdl.handle.net/123456789/1"'); | ||
| expect(result).toContain('rel="noopener noreferrer"'); | ||
| }); | ||
|
|
||
| it('should handle DOI resolver URLs', () => { | ||
| const result = makeLinks('https://doi.org/10.1234/test'); | ||
| expect(result).toContain('<a href="https://doi.org/10.1234/test"'); | ||
| }); | ||
|
|
||
| it('should not create links for javascript: URIs', () => { | ||
| const result = makeLinks('javascript:alert(1)'); | ||
| expect(result).not.toContain('<a'); | ||
| expect(result).toBe('javascript:alert(1)'); | ||
| }); | ||
|
|
||
| it('should not create links for data: URIs', () => { | ||
| const result = makeLinks('data:text/html,<script>alert(1)</script>'); | ||
| expect(result).not.toContain('<a'); | ||
| }); | ||
|
|
||
| it('should handle multiple URLs in one string', () => { | ||
| const result = makeLinks('See https://a.com and https://b.com'); | ||
| expect(result).toContain('<a href="https://a.com"'); | ||
| expect(result).toContain('<a href="https://b.com"'); | ||
| }); | ||
|
|
||
| it('should handle URLs with query parameters', () => { | ||
| const result = makeLinks('https://example.com/search?q=test&page=1'); | ||
| expect(result).toContain('<a href="https://example.com/search?q=test&page=1"'); | ||
| }); | ||
|
|
||
| it('should handle URLs with fragments', () => { | ||
| const result = makeLinks('https://example.com/page#section'); | ||
| expect(result).toContain('<a href="https://example.com/page#section"'); | ||
| }); | ||
|
|
There was a problem hiding this comment.
The new makeLinks/getMetadataLink unit tests are added to full-item-page.component.spec.ts, which significantly increases the size/scope of a component spec for logic that lives in shared/utils/make-links.ts. To keep tests cohesive (and reduce future maintenance when the component changes), consider moving these util-focused tests into a dedicated make-links.spec.ts and leaving only the DOM rendering assertions in this component spec.
| it('should convert URLs embedded in text', () => { | |
| const result = makeLinks('Visit https://example.com for details'); | |
| expect(result).toContain('<a href="https://example.com"'); | |
| expect(result).toContain('Visit'); | |
| expect(result).toContain('for details'); | |
| }); | |
| it('should handle DOI / handle redirect URLs', () => { | |
| const result = makeLinks('https://hdl.handle.net/123456789/1'); | |
| expect(result).toContain('<a href="https://hdl.handle.net/123456789/1"'); | |
| expect(result).toContain('rel="noopener noreferrer"'); | |
| }); | |
| it('should handle DOI resolver URLs', () => { | |
| const result = makeLinks('https://doi.org/10.1234/test'); | |
| expect(result).toContain('<a href="https://doi.org/10.1234/test"'); | |
| }); | |
| it('should not create links for javascript: URIs', () => { | |
| const result = makeLinks('javascript:alert(1)'); | |
| expect(result).not.toContain('<a'); | |
| expect(result).toBe('javascript:alert(1)'); | |
| }); | |
| it('should not create links for data: URIs', () => { | |
| const result = makeLinks('data:text/html,<script>alert(1)</script>'); | |
| expect(result).not.toContain('<a'); | |
| }); | |
| it('should handle multiple URLs in one string', () => { | |
| const result = makeLinks('See https://a.com and https://b.com'); | |
| expect(result).toContain('<a href="https://a.com"'); | |
| expect(result).toContain('<a href="https://b.com"'); | |
| }); | |
| it('should handle URLs with query parameters', () => { | |
| const result = makeLinks('https://example.com/search?q=test&page=1'); | |
| expect(result).toContain('<a href="https://example.com/search?q=test&page=1"'); | |
| }); | |
| it('should handle URLs with fragments', () => { | |
| const result = makeLinks('https://example.com/page#section'); | |
| expect(result).toContain('<a href="https://example.com/page#section"'); | |
| }); |
Problem description
Copilot review