Skip to content

Commit 54417ab

Browse files
authored
Merge pull request #5470 from guillermo-escire/feature/5314
Fix #5314: The keyword now performs a search and returns the correct …
2 parents d3a5aa8 + 10f631a commit 54417ab

5 files changed

Lines changed: 101 additions & 41 deletions

File tree

src/app/item-page/field-components/metadata-values/metadata-values.component.html

Lines changed: 18 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -4,15 +4,30 @@
44
<!--
55
Choose a template. Priority: vocabulary, markdown, link, browse link.
66
-->
7-
<ng-container *ngTemplateOutlet="(isVocabulary ? controlledVocabulary : (renderMarkdown ? markdown : (hasLink(mdValue) ? (hasValue(img) ? linkImg : link) : (hasBrowseDefinition() ? browselink : simple))));
8-
context: {value: (isVocabulary ? mdValue : mdValue.value), img}">
9-
</ng-container>
7+
<ng-container
8+
*ngTemplateOutlet="
9+
isVocabulary ? controlledVocabulary :
10+
renderMarkdown ? markdown :
11+
hasLink(mdValue) ? (hasValue(img) ? linkImg : link) :
12+
hasSearchFilter() ? searchlink :
13+
hasBrowseDefinition() ? browselink : simple;
14+
context: { value: (isVocabulary ? mdValue : mdValue.value), img: img }
15+
">
16+
</ng-container>
1017
@if (!last) {
1118
<span class="separator" [innerHTML]="separator"></span>
1219
}
1320
}
1421
</ds-metadata-field-wrapper>
1522

23+
<ng-template #searchlink let-value="value">
24+
<a class="ds-search-link"
25+
[routerLink]="['/search']"
26+
[queryParams]="getSearchQueryParams(value)">
27+
{{ value }}
28+
</a>
29+
</ng-template>
30+
1631
<!-- Render value as markdown -->
1732
<ng-template #markdown let-value="value">
1833
<span class="dont-break-out" [dsMarkdown]="value">

src/app/item-page/field-components/metadata-values/metadata-values.component.spec.ts

Lines changed: 46 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,8 @@ import {
88
waitForAsync,
99
} from '@angular/core/testing';
1010
import { By } from '@angular/platform-browser';
11+
import { ActivatedRoute } from '@angular/router';
12+
import { RouterTestingModule } from '@angular/router/testing';
1113
import { APP_CONFIG } from '@dspace/config/app-config.interface';
1214
import { buildPaginatedList } from '@dspace/core/data/paginated-list.model';
1315
import { MetadataValue } from '@dspace/core/shared/metadata.models';
@@ -28,18 +30,10 @@ let comp: MetadataValuesComponent;
2830
let fixture: ComponentFixture<MetadataValuesComponent>;
2931

3032
const mockMetadata = [
31-
{
32-
language: 'en_US',
33-
value: '1234',
34-
},
35-
{
36-
language: 'en_US',
37-
value: 'a publisher',
38-
},
39-
{
40-
language: 'en_US',
41-
value: 'desc',
42-
}] as MetadataValue[];
33+
{ language: 'en_US', value: '1234' },
34+
{ language: 'en_US', value: 'a publisher' },
35+
{ language: 'en_US', value: 'desc' },
36+
] as MetadataValue[];
4337
const mockSeperator = '<br/>';
4438
const mockLabel = 'fake.message';
4539
const vocabularyServiceMock = {
@@ -58,15 +52,28 @@ const controlledMetadata = {
5852
describe('MetadataValuesComponent', () => {
5953
beforeEach(waitForAsync(() => {
6054
TestBed.configureTestingModule({
61-
imports: [TranslateModule.forRoot({
62-
loader: {
63-
provide: TranslateLoader,
64-
useClass: TranslateLoaderMock,
65-
},
66-
}), MetadataValuesComponent],
55+
imports: [
56+
RouterTestingModule.withRoutes([]),
57+
TranslateModule.forRoot({
58+
loader: {
59+
provide: TranslateLoader,
60+
useClass: TranslateLoaderMock,
61+
},
62+
}),
63+
MetadataValuesComponent,
64+
],
6765
providers: [
6866
{ provide: APP_CONFIG, useValue: environment },
6967
{ provide: VocabularyService, useValue: vocabularyServiceMock },
68+
{
69+
provide: ActivatedRoute,
70+
useValue: {
71+
snapshot: {},
72+
params: of({}),
73+
queryParams: of({}),
74+
data: of({}),
75+
},
76+
},
7077
],
7178
schemas: [NO_ERRORS_SCHEMA],
7279
}).overrideComponent(MetadataValuesComponent, {
@@ -103,43 +110,48 @@ describe('MetadataValuesComponent', () => {
103110

104111
it('should return correct target and rel for internal links', () => {
105112
spyOn(comp, 'hasInternalLink').and.returnValue(true);
106-
const urlValue = '/internal-link';
107-
const result = comp.getLinkAttributes(urlValue);
113+
const result = comp.getLinkAttributes('/internal-link');
108114
expect(result.target).toBe('_self');
109115
expect(result.rel).toBe('');
110116
});
111117

112118
it('should return correct target and rel for external links', () => {
113119
spyOn(comp, 'hasInternalLink').and.returnValue(false);
114-
const urlValue = 'https://www.dspace.org';
115-
const result = comp.getLinkAttributes(urlValue);
120+
const result = comp.getLinkAttributes('https://www.dspace.org');
116121
expect(result.target).toBe('_blank');
117122
expect(result.rel).toBe('noopener noreferrer');
118123
});
119124

120125
it('should detect controlled vocabulary metadata', () => {
121-
const result = comp.isControlledVocabulary(controlledMetadata);
122-
expect(result).toBeTrue();
126+
expect(comp.isControlledVocabulary(controlledMetadata)).toBeTrue();
123127
});
124128

125129
it('should return translated vocabulary value when available', (done) => {
126-
const vocabEntry = {
127-
display: 'Translated Value',
128-
};
129-
130130
vocabularyServiceMock.getPublicVocabularyEntryByID.and.returnValue(
131-
of(
132-
createSuccessfulRemoteDataObject(
133-
buildPaginatedList(new PageInfo(), [vocabEntry]),
134-
),
135-
),
131+
of(createSuccessfulRemoteDataObject(
132+
buildPaginatedList(new PageInfo(), [{ display: 'Translated Value' }]),
133+
)),
136134
);
137-
138135
comp.getVocabularyValue(controlledMetadata).subscribe((value) => {
139136
expect(value).toBe('Translated Value');
140137
done();
141138
});
142139
});
143140

141+
it('should render search link when searchFilter is present', () => {
142+
comp.searchFilter = 'subject';
143+
expect(comp.hasSearchFilter()).toBeTrue();
144+
expect(comp.getSearchQueryParams('test')).toEqual({ 'f.subject': 'test,equals' });
145+
});
146+
147+
it('should render browse link when browseDefinition is provided', () => {
148+
comp.browseDefinition = {
149+
id: 'subject',
150+
metadataKeys: [],
151+
order: 0,
152+
getRenderType: () => 'metadata',
153+
} as any;
154+
expect(comp.hasBrowseDefinition()).toBeTrue();
155+
});
144156

145157
});

src/app/item-page/field-components/metadata-values/metadata-values.component.ts

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -103,10 +103,24 @@ export class MetadataValuesComponent implements OnChanges {
103103

104104
hasValue = hasValue;
105105

106+
/**
107+
* Optional metadata field used to build search links for values.
108+
* If defined, values will link to the search page using this field as filter.
109+
*/
110+
@Input() searchFilter?: string;
111+
106112
ngOnChanges(changes: SimpleChanges): void {
107113
this.renderMarkdown = !!this.appConfig.markdown.enabled && this.enableMarkdown;
108114
}
109115

116+
/**
117+
* Determines whether a search filter has been configured for this metadata field.
118+
* Used to decide if values should be rendered as search links.
119+
*/
120+
hasSearchFilter(): boolean {
121+
return !!this.searchFilter;
122+
}
123+
110124
/**
111125
* Does this metadata value have a configured link to a browse definition?
112126
*/
@@ -126,6 +140,17 @@ export class MetadataValuesComponent implements OnChanges {
126140
return false;
127141
}
128142

143+
/**
144+
* Builds query parameters for the search page based on the configured search filter.
145+
* The metadata value is used as the search term for the specified field.
146+
*
147+
* @param value The metadata value to search for.
148+
* @returns Query parameters object for Angular router navigation.
149+
*/
150+
getSearchQueryParams(value: string): any {
151+
return { [`f.${this.searchFilter}`]: `${value},equals` };
152+
}
153+
129154
/**
130155
* Whether the metadata is a controlled vocabulary
131156
* @param value A MetadataValue being displayed

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

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,8 @@
55
[label]="label"
66
[enableMarkdown]="enableMarkdown"
77
[urlRegex]="urlRegex"
8-
[browseDefinition]="browseDefinition|async"
9-
[img]="img"
10-
></ds-metadata-values>
8+
[browseDefinition]="browseDefinition | async"
9+
[searchFilter]="searchFilter"
10+
[img]="img">
11+
</ds-metadata-values>
1112
</div>

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

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -55,7 +55,7 @@ export class ItemPageFieldComponent {
5555
/**
5656
* Fields (schema.element.qualifier) used to render their values.
5757
*/
58-
fields: string[];
58+
@Input() fields: string[];
5959

6060
/**
6161
* Label i18n key for the rendered metadata
@@ -78,6 +78,13 @@ export class ItemPageFieldComponent {
7878
*/
7979
img: ImageField;
8080

81+
/**
82+
* Search filter used when rendering subject metadata as search links.
83+
*/
84+
get searchFilter(): string | undefined {
85+
return this.fields?.includes('dc.subject') ? 'subject' : undefined;
86+
}
87+
8188
/**
8289
* Return browse definition that matches any field used in this component if it is configured as a browse
8390
* link in dspace.cfg (webui.browse.link.<n>)

0 commit comments

Comments
 (0)