Skip to content

Commit 3b041c7

Browse files
FrancescoMolinaroAndrea Barbasso
authored andcommitted
Merged in task/dspace-cris-2025_02_x/DSC-2544 (pull request DSpace#4272)
[DSC-2544] port link handling in valuepair and correct label parsing in link rendering Approved-by: Andrea Barbasso
2 parents e8fb769 + 7b7ba38 commit 3b041c7

5 files changed

Lines changed: 168 additions & 5 deletions

File tree

src/app/cris-layout/cris-layout-matrix/cris-layout-box-container/boxes/metadata/rendering-types/link/link.component.spec.ts

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -109,6 +109,10 @@ describe('LinkComponent', () => {
109109
describe('with sub-type label', () => {
110110
beforeEach(() => {
111111
component.renderingSubType = 'LABEL';
112+
component.metadataValueProvider = Object.assign(new MetadataValue(), metadataValue, {
113+
value: '[Default Label](http://rest.api/item/link/id)',
114+
});
115+
component.metadataValue = component.metadataValueProvider;
112116
spyOn(translateService, 'instant').and.returnValue(i18nLabel);
113117
fixture.detectChanges();
114118
});
@@ -239,4 +243,42 @@ describe('LinkComponent', () => {
239243
});
240244
});
241245
});
246+
247+
describe('parseLabelValue', () => {
248+
249+
beforeEach(() => {
250+
fixture.detectChanges();
251+
});
252+
253+
it('should correctly extract label and URL from [Label](URL) format', () => {
254+
const input = '[My Label](https://example.com/path)';
255+
const result = component.parseLabelValue(input);
256+
257+
expect(result).toEqual({
258+
label: 'My Label',
259+
value: 'https://example.com/path',
260+
});
261+
});
262+
263+
it('should return the same value if input does not match [Label](URL) format', () => {
264+
const input = 'Just a plain URL';
265+
const result = component.parseLabelValue(input);
266+
267+
expect(result).toEqual({
268+
label: input,
269+
value: input,
270+
});
271+
});
272+
273+
it('should handle empty string gracefully', () => {
274+
const input = '';
275+
const result = component.parseLabelValue(input);
276+
277+
expect(result).toEqual({
278+
label: '',
279+
value: '',
280+
});
281+
});
282+
283+
});
242284
});

src/app/cris-layout/cris-layout-matrix/cris-layout-box-container/boxes/metadata/rendering-types/link/link.component.ts

Lines changed: 40 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -64,8 +64,13 @@ export class LinkComponent extends RenderingTypeValueModelComponent implements O
6464
* Check if the metadata value is a valid URL
6565
*/
6666
isValidUrl(): boolean {
67-
const value = this.metadataValue.value.trim();
67+
let value = this.metadataValue.value.trim();
6868

69+
// If the subtype is LABEL, the value is in [Label](URL) format — extract the URL part
70+
if (hasValue(this.renderingSubType) && this.renderingSubType.toUpperCase() === TYPES.LABEL.toString()) {
71+
const parsed = this.parseLabelValue(value);
72+
value = parsed.value;
73+
}
6974
// Comprehensive URL regex that matches:
7075
// - URLs with protocols (http, https, ftp, mailto, etc.)
7176
// - URLs without protocols (www.example.com, example.com)
@@ -88,16 +93,46 @@ export class LinkComponent extends RenderingTypeValueModelComponent implements O
8893
metadataValue = 'mailto:' + this.metadataValue.value;
8994
linkText = (hasValue(this.renderingSubType) &&
9095
this.renderingSubType.toUpperCase() === TYPES.EMAIL.toString()) ? this.metadataValue.value : this.translateService.instant(this.field.label);
96+
} else if ((hasValue(this.renderingSubType) && this.renderingSubType.toUpperCase() === TYPES.LABEL.toString())) {
97+
// Parse value in format [Label](URL)
98+
const parsedValue = this.parseLabelValue(this.metadataValue.value);
99+
100+
metadataValue = this.getLinkWithProtocol(parsedValue.value);
101+
linkText = parsedValue.label;
91102
} else {
92-
const startsWithProtocol = [/^https?:\/\//, /^ftp:\/\//];
93-
metadataValue = startsWithProtocol.some(rx => rx.test(this.metadataValue.value)) ? this.metadataValue.value : 'http://' + this.metadataValue.value;
94-
linkText = (hasValue(this.renderingSubType) &&
95-
this.renderingSubType.toUpperCase() === TYPES.LABEL.toString()) ? this.translateService.instant(this.field.label) : this.metadataValue.value;
103+
// Use same value for link and label, correcting the protocol for link if needed
104+
metadataValue = this.getLinkWithProtocol(this.metadataValue.value);
105+
linkText = this.metadataValue.value;
96106
}
97107

98108
return {
99109
href: metadataValue,
100110
text: linkText,
101111
};
102112
}
113+
114+
/**
115+
* Exctract label and values for TYPES.LABEL
116+
* @param value
117+
*/
118+
parseLabelValue(input: string): { label: string; value: string } {
119+
const match = input.match(/^\[([^\]]+)\]\(([^)]+)\)$/);
120+
121+
if (!match) {
122+
return {
123+
label: input,
124+
value: input,
125+
};
126+
}
127+
128+
return {
129+
label: match[1],
130+
value: match[2],
131+
};
132+
}
133+
134+
getLinkWithProtocol(link: string): string {
135+
const startsWithProtocol = [/^https?:\/\//, /^ftp:\/\//];
136+
return startsWithProtocol.some(rx => rx.test(link)) ? link : 'http://' + link;
137+
}
103138
}
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,11 @@
11
<div [class]="field.styleValue" [attr.lang]="metadataValue.language">
2+
@if (isMetadataLink !== true) {
23
<span class="text-value">
34
{{ value$ | async }}
45
</span>
6+
} @else {
7+
<a [href]="metadataValue.value" class="text-value">
8+
{{ value$ | async }}
9+
</a>
10+
}
511
</div>

src/app/cris-layout/cris-layout-matrix/cris-layout-box-container/boxes/metadata/rendering-types/valuepair/valuepair.component.spec.ts

Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ import {
1515
import { AuthService } from '../../../../../../../core/auth/auth.service';
1616
import { LayoutField } from '../../../../../../../core/layout/models/box.model';
1717
import { Item } from '../../../../../../../core/shared/item.model';
18+
import { MetadataValue } from '../../../../../../../core/shared/metadata.models';
1819
import { VocabularyService } from '../../../../../../../core/submission/vocabularies/vocabulary.service';
1920
import { TranslateLoaderMock } from '../../../../../../../shared/mocks/translate-loader.mock';
2021
import { AuthServiceStub } from '../../../../../../../shared/testing/auth-service.stub';
@@ -194,4 +195,70 @@ describe('ValuepairComponent', () => {
194195

195196
});
196197

198+
describe('when the metadata value is a link', () => {
199+
const linkValue = 'https://example.com';
200+
const testFieldLink: LayoutField = {
201+
metadata: 'dc.identifier',
202+
label: 'Identifier',
203+
rendering: 'valuepair.' + VOCABULARY_NAME_2,
204+
fieldType: 'METADATA',
205+
metadataGroup: null,
206+
labelAsHeading: false,
207+
valuesInline: false,
208+
};
209+
210+
const testItemLink = Object.assign(new Item(), {
211+
allMetadata: () => [{ value: linkValue, authority: null }],
212+
});
213+
214+
beforeEach(waitForAsync(() => {
215+
TestBed.configureTestingModule({
216+
imports: [
217+
TranslateModule.forRoot({
218+
loader: {
219+
provide: TranslateLoader,
220+
useClass: TranslateLoaderMock,
221+
},
222+
}),
223+
ValuepairComponent,
224+
DsDatePipe,
225+
],
226+
providers: [
227+
{ provide: VocabularyService, useValue: vocabularyServiceSpy },
228+
{ provide: AuthService, useValue: authService },
229+
{ provide: 'fieldProvider', useValue: testFieldLink },
230+
{ provide: 'itemProvider', useValue: testItemLink },
231+
{ provide: 'metadataValueProvider', useValue: { value: linkValue, authority: null } },
232+
{ provide: 'renderingSubTypeProvider', useValue: '' }, // leave empty
233+
{ provide: 'tabNameProvider', useValue: '' },
234+
],
235+
}).compileComponents();
236+
}));
237+
238+
beforeEach(() => {
239+
fixture = TestBed.createComponent(ValuepairComponent);
240+
component = fixture.componentInstance;
241+
component.metadataValue = Object.assign(new MetadataValue(), {
242+
value: linkValue,
243+
});
244+
component.value$.next(linkValue);
245+
246+
fixture.detectChanges();
247+
});
248+
249+
it('should detect that the value is a link', () => {
250+
expect(component.isMetadataLink).toBeTrue();
251+
});
252+
253+
it('should render the value as an <a> tag', () => {
254+
const compiled = fixture.nativeElement as HTMLElement;
255+
const linkEl = compiled.querySelector('a.text-value') as HTMLAnchorElement;
256+
257+
expect(linkEl).toBeTruthy();
258+
expect(linkEl.href).toBe(linkValue + '/');
259+
expect(linkEl.textContent.trim()).toBe(linkValue);
260+
});
261+
});
262+
263+
197264
});

src/app/cris-layout/cris-layout-matrix/cris-layout-box-container/boxes/metadata/rendering-types/valuepair/valuepair.component.ts

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,12 @@ export class ValuepairComponent extends RenderingTypeValueModelComponent impleme
4242
*/
4343
value$: BehaviorSubject<string> = new BehaviorSubject<string>(null);
4444

45+
/**
46+
* Whether the value is a link
47+
*/
48+
49+
isMetadataLink: boolean;
50+
4551
constructor(
4652
@Inject('fieldProvider') public fieldProvider: LayoutField,
4753
@Inject('itemProvider') public itemProvider: Item,
@@ -74,6 +80,13 @@ export class ValuepairComponent extends RenderingTypeValueModelComponent impleme
7480
take(1),
7581
).subscribe(value => this.value$.next(value));
7682

83+
this.isMetadataLink = this.isLink(this.metadataValue.value);
84+
}
85+
86+
87+
isLink(input: string): boolean {
88+
// check only values with protocol, if missing fix value in value-pair list
89+
return input && (input.startsWith('http://') || input.startsWith('https://'));
7790
}
7891

7992
}

0 commit comments

Comments
 (0)