Skip to content

Commit b1046fb

Browse files
FrancescoMolinaroAndrea Barbasso
authored andcommitted
Merged in task/dspace-cris-2024_02_x/DSC-2544 (pull request DSpace#4271)
Task/dspace cris 2024 02 x/DSC-2544 Approved-by: Andrea Barbasso
2 parents 0510acb + b7ae91e commit b1046fb

File tree

5 files changed

+178
-8
lines changed

5 files changed

+178
-8
lines changed

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: 41 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -65,7 +65,13 @@ export class LinkComponent extends RenderingTypeValueModelComponent implements O
6565
* Check if the metadata value is a valid URL
6666
*/
6767
isValidUrl(): boolean {
68-
const value = this.metadataValue.value.trim();
68+
let value = this.metadataValue.value.trim();
69+
70+
// If the subtype is LABEL, the value is in [Label](URL) format — extract the URL part
71+
if (hasValue(this.renderingSubType) && this.renderingSubType.toUpperCase() === TYPES.LABEL.toString()) {
72+
const parsed = this.parseLabelValue(value);
73+
value = parsed.value;
74+
}
6975

7076
// Comprehensive URL regex that matches:
7177
// - URLs with protocols (http, https, ftp, mailto, etc.)
@@ -89,16 +95,46 @@ export class LinkComponent extends RenderingTypeValueModelComponent implements O
8995
metadataValue = 'mailto:' + this.metadataValue.value;
9096
linkText = (hasValue(this.renderingSubType) &&
9197
this.renderingSubType.toUpperCase() === TYPES.EMAIL.toString()) ? this.metadataValue.value : this.translateService.instant(this.field.label);
98+
} else if ((hasValue(this.renderingSubType) && this.renderingSubType.toUpperCase() === TYPES.LABEL.toString())) {
99+
// Parse value in format [Label](URL)
100+
const parsedValue = this.parseLabelValue(this.metadataValue.value);
101+
102+
metadataValue = this.getLinkWithProtocol(parsedValue.value);
103+
linkText = parsedValue.label;
92104
} else {
93-
const startsWithProtocol = [/^https?:\/\//, /^ftp:\/\//];
94-
metadataValue = startsWithProtocol.some(rx => rx.test(this.metadataValue.value)) ? this.metadataValue.value : 'http://' + this.metadataValue.value;
95-
linkText = (hasValue(this.renderingSubType) &&
96-
this.renderingSubType.toUpperCase() === TYPES.LABEL.toString()) ? this.translateService.instant(this.field.label) : this.metadataValue.value;
105+
// Use same value for link and label, correcting the protocol for link if needed
106+
metadataValue = this.getLinkWithProtocol(this.metadataValue.value);
107+
linkText = this.metadataValue.value;
97108
}
98109

99110
return {
100111
href: metadataValue,
101112
text: linkText,
102113
};
103114
}
115+
116+
/**
117+
* Exctract label and values for TYPES.LABEL
118+
* @param value
119+
*/
120+
parseLabelValue(input: string): { label: string; value: string } {
121+
const match = input.match(/^\[([^\]]+)\]\(([^)]+)\)$/);
122+
123+
if (!match) {
124+
return {
125+
label: input,
126+
value: input,
127+
};
128+
}
129+
130+
return {
131+
label: match[1],
132+
value: match[2],
133+
};
134+
}
135+
136+
getLinkWithProtocol(link: string): string {
137+
const startsWithProtocol = [/^https?:\/\//, /^ftp:\/\//];
138+
return startsWithProtocol.some(rx => rx.test(link)) ? link : 'http://' + link;
139+
}
104140
}
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,13 @@
11
<div [class]="field.styleValue" [attr.lang]="metadataValue.language">
2+
<ng-container *ngIf="isMetadataLink !== true; else linkTemplate">
23
<span class="text-value">
34
{{ value$ | async }}
45
</span>
6+
</ng-container>
7+
8+
<ng-template #linkTemplate>
9+
<a [href]="metadataValue.value" class="text-value">
10+
{{ value$ | async }}
11+
</a>
12+
</ng-template>
513
</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: 20 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,7 @@
1-
import { AsyncPipe } from '@angular/common';
1+
import {
2+
AsyncPipe,
3+
NgIf,
4+
} from '@angular/common';
25
import {
36
Component,
47
Inject,
@@ -32,7 +35,7 @@ import { RenderingTypeValueModelComponent } from '../rendering-type-value.model'
3235
templateUrl: './valuepair.component.html',
3336
styleUrls: ['./valuepair.component.scss'],
3437
standalone: true,
35-
imports: [AsyncPipe],
38+
imports: [AsyncPipe, NgIf],
3639
})
3740
export class ValuepairComponent extends RenderingTypeValueModelComponent implements OnInit {
3841

@@ -41,6 +44,12 @@ export class ValuepairComponent extends RenderingTypeValueModelComponent impleme
4144
*/
4245
value$: BehaviorSubject<string> = new BehaviorSubject<string>(null);
4346

47+
/**
48+
* Whether the value is a link
49+
*/
50+
51+
isMetadataLink: boolean;
52+
4453
constructor(
4554
@Inject('fieldProvider') public fieldProvider: LayoutField,
4655
@Inject('itemProvider') public itemProvider: Item,
@@ -68,11 +77,19 @@ export class ValuepairComponent extends RenderingTypeValueModelComponent impleme
6877
getFirstCompletedRemoteData(),
6978
getRemoteDataPayload(),
7079
getPaginatedListPayload(),
71-
map((res) => res?.length > 0 ? res[0] : null),
80+
map((res) => {
81+
return res?.length > 0 ? res[0] : null;
82+
}),
7283
map((res) => res?.display ?? this.metadataValue.value),
7384
take(1),
7485
).subscribe(value => this.value$.next(value));
7586

87+
this.isMetadataLink = this.isLink(this.metadataValue.value);
88+
}
89+
90+
isLink(input: string): boolean {
91+
// check only values with protocol, if missing fix value in value-pair list
92+
return input && (input.startsWith('http://') || input.startsWith('https://'));
7693
}
7794

7895
}

0 commit comments

Comments
 (0)