From d9db2e4d2b08fac64c2f768e10122349da33eca6 Mon Sep 17 00:00:00 2001 From: guillermo2519 Date: Fri, 10 Apr 2026 18:40:03 -0600 Subject: [PATCH 1/4] Fix #5314: The keyword now performs a search and returns the correct item; it no longer sends to browse/srsc. --- .../metadata-values.component.html | 21 ++++++++++++--- .../metadata-values.component.spec.ts | 26 +++++++++++++++++++ .../metadata-values.component.ts | 25 ++++++++++++++++++ .../item-page-field.component.html | 7 ++--- .../item-page-field.component.ts | 9 ++++++- 5 files changed, 81 insertions(+), 7 deletions(-) diff --git a/src/app/item-page/field-components/metadata-values/metadata-values.component.html b/src/app/item-page/field-components/metadata-values/metadata-values.component.html index 7ed177f5b6d..9038a2b023c 100644 --- a/src/app/item-page/field-components/metadata-values/metadata-values.component.html +++ b/src/app/item-page/field-components/metadata-values/metadata-values.component.html @@ -4,15 +4,30 @@ - - + + @if (!last) { } } + + + {{ value }} + + + diff --git a/src/app/item-page/field-components/metadata-values/metadata-values.component.spec.ts b/src/app/item-page/field-components/metadata-values/metadata-values.component.spec.ts index f3581f8cc8c..eb090db8b76 100644 --- a/src/app/item-page/field-components/metadata-values/metadata-values.component.spec.ts +++ b/src/app/item-page/field-components/metadata-values/metadata-values.component.spec.ts @@ -141,5 +141,31 @@ describe('MetadataValuesComponent', () => { }); }); + it('should render search link when searchFilter is present', () => { + comp.searchFilter = 'subject'; + fixture.detectChanges(); + + const value = mockMetadata[0].value; + + const link = fixture.debugElement.query(By.css('a.ds-search-link')); + + expect(link).not.toBeNull(); + expect(link.nativeElement.textContent).toContain(value); + }); + + it('should render browse link when browseDefinition is provided', () => { + comp.browseDefinition = { + id: 'subject', + metadataKeys: [], + order: 0, + getRenderType: () => 'metadata', + } as any; + + fixture.detectChanges(); + + const link = fixture.debugElement.query(By.css('a.ds-browse-link')); + + expect(link).not.toBeNull(); + }); }); diff --git a/src/app/item-page/field-components/metadata-values/metadata-values.component.ts b/src/app/item-page/field-components/metadata-values/metadata-values.component.ts index c37f1f975bf..04ed9473a24 100644 --- a/src/app/item-page/field-components/metadata-values/metadata-values.component.ts +++ b/src/app/item-page/field-components/metadata-values/metadata-values.component.ts @@ -103,10 +103,24 @@ export class MetadataValuesComponent implements OnChanges { hasValue = hasValue; + /** + * Optional metadata field used to build search links for values. + * If defined, values will link to the search page using this field as filter. + */ + @Input() searchFilter?: string; + ngOnChanges(changes: SimpleChanges): void { this.renderMarkdown = !!this.appConfig.markdown.enabled && this.enableMarkdown; } + /** + * Determines whether a search filter has been configured for this metadata field. + * Used to decide if values should be rendered as search links. + */ + hasSearchFilter(): boolean { + return !!this.searchFilter; + } + /** * Does this metadata value have a configured link to a browse definition? */ @@ -126,6 +140,17 @@ export class MetadataValuesComponent implements OnChanges { return false; } + /** + * Builds query parameters for the search page based on the configured search filter. + * The metadata value is used as the search term for the specified field. + * + * @param value The metadata value to search for. + * @returns Query parameters object for Angular router navigation. + */ + getSearchQueryParams(value: string): any { + return { [`f.${this.searchFilter}`]: `${value},equals` }; + } + /** * Whether the metadata is a controlled vocabulary * @param value A MetadataValue being displayed diff --git a/src/app/item-page/simple/field-components/specific-field/item-page-field.component.html b/src/app/item-page/simple/field-components/specific-field/item-page-field.component.html index f45d4657a46..576fbf43d4e 100644 --- a/src/app/item-page/simple/field-components/specific-field/item-page-field.component.html +++ b/src/app/item-page/simple/field-components/specific-field/item-page-field.component.html @@ -5,7 +5,8 @@ [label]="label" [enableMarkdown]="enableMarkdown" [urlRegex]="urlRegex" - [browseDefinition]="browseDefinition|async" - [img]="img" - > + [browseDefinition]="browseDefinition | async" + [searchFilter]="searchFilter" + [img]="img"> + diff --git a/src/app/item-page/simple/field-components/specific-field/item-page-field.component.ts b/src/app/item-page/simple/field-components/specific-field/item-page-field.component.ts index 69c9acd03af..bcf188f36e1 100644 --- a/src/app/item-page/simple/field-components/specific-field/item-page-field.component.ts +++ b/src/app/item-page/simple/field-components/specific-field/item-page-field.component.ts @@ -55,7 +55,7 @@ export class ItemPageFieldComponent { /** * Fields (schema.element.qualifier) used to render their values. */ - fields: string[]; + @Input() fields: string[]; /** * Label i18n key for the rendered metadata @@ -78,6 +78,13 @@ export class ItemPageFieldComponent { */ img: ImageField; + /** + * Search filter used when rendering subject metadata as search links. + */ + get searchFilter(): string | undefined { + return this.fields?.includes('dc.subject') ? 'subject' : undefined; + } + /** * Return browse definition that matches any field used in this component if it is configured as a browse * link in dspace.cfg (webui.browse.link.) From d52051f54565265bf658fd352b0aec930a776aa7 Mon Sep 17 00:00:00 2001 From: guillermo2519 Date: Sat, 11 Apr 2026 01:53:09 -0600 Subject: [PATCH 2/4] Fix #5314: fix the spec --- .../metadata-values.component.spec.ts | 82 ++++++++----------- 1 file changed, 34 insertions(+), 48 deletions(-) diff --git a/src/app/item-page/field-components/metadata-values/metadata-values.component.spec.ts b/src/app/item-page/field-components/metadata-values/metadata-values.component.spec.ts index eb090db8b76..7c23933f7a3 100644 --- a/src/app/item-page/field-components/metadata-values/metadata-values.component.spec.ts +++ b/src/app/item-page/field-components/metadata-values/metadata-values.component.spec.ts @@ -8,6 +8,8 @@ import { waitForAsync, } from '@angular/core/testing'; import { By } from '@angular/platform-browser'; +import { ActivatedRoute } from '@angular/router'; +import { RouterTestingModule } from '@angular/router/testing'; import { APP_CONFIG } from '@dspace/config/app-config.interface'; import { buildPaginatedList } from '@dspace/core/data/paginated-list.model'; import { MetadataValue } from '@dspace/core/shared/metadata.models'; @@ -28,18 +30,10 @@ let comp: MetadataValuesComponent; let fixture: ComponentFixture; const mockMetadata = [ - { - language: 'en_US', - value: '1234', - }, - { - language: 'en_US', - value: 'a publisher', - }, - { - language: 'en_US', - value: 'desc', - }] as MetadataValue[]; + { language: 'en_US', value: '1234' }, + { language: 'en_US', value: 'a publisher' }, + { language: 'en_US', value: 'desc' }, +] as MetadataValue[]; const mockSeperator = '
'; const mockLabel = 'fake.message'; const vocabularyServiceMock = { @@ -58,15 +52,28 @@ const controlledMetadata = { describe('MetadataValuesComponent', () => { beforeEach(waitForAsync(() => { TestBed.configureTestingModule({ - imports: [TranslateModule.forRoot({ - loader: { - provide: TranslateLoader, - useClass: TranslateLoaderMock, - }, - }), MetadataValuesComponent], + imports: [ + RouterTestingModule.withRoutes([]), + TranslateModule.forRoot({ + loader: { + provide: TranslateLoader, + useClass: TranslateLoaderMock, + }, + }), + MetadataValuesComponent, + ], providers: [ { provide: APP_CONFIG, useValue: environment }, { provide: VocabularyService, useValue: vocabularyServiceMock }, + { + provide: ActivatedRoute, + useValue: { + snapshot: {}, + params: of({}), + queryParams: of({}), + data: of({}), + }, + }, ], schemas: [NO_ERRORS_SCHEMA], }).overrideComponent(MetadataValuesComponent, { @@ -103,38 +110,28 @@ describe('MetadataValuesComponent', () => { it('should return correct target and rel for internal links', () => { spyOn(comp, 'hasInternalLink').and.returnValue(true); - const urlValue = '/internal-link'; - const result = comp.getLinkAttributes(urlValue); + const result = comp.getLinkAttributes('/internal-link'); expect(result.target).toBe('_self'); expect(result.rel).toBe(''); }); it('should return correct target and rel for external links', () => { spyOn(comp, 'hasInternalLink').and.returnValue(false); - const urlValue = 'https://www.dspace.org'; - const result = comp.getLinkAttributes(urlValue); + const result = comp.getLinkAttributes('https://www.dspace.org'); expect(result.target).toBe('_blank'); expect(result.rel).toBe('noopener noreferrer'); }); it('should detect controlled vocabulary metadata', () => { - const result = comp.isControlledVocabulary(controlledMetadata); - expect(result).toBeTrue(); + expect(comp.isControlledVocabulary(controlledMetadata)).toBeTrue(); }); it('should return translated vocabulary value when available', (done) => { - const vocabEntry = { - display: 'Translated Value', - }; - vocabularyServiceMock.getPublicVocabularyEntryByID.and.returnValue( - of( - createSuccessfulRemoteDataObject( - buildPaginatedList(new PageInfo(), [vocabEntry]), - ), - ), + of(createSuccessfulRemoteDataObject( + buildPaginatedList(new PageInfo(), [{ display: 'Translated Value' }]), + )), ); - comp.getVocabularyValue(controlledMetadata).subscribe((value) => { expect(value).toBe('Translated Value'); done(); @@ -143,14 +140,8 @@ describe('MetadataValuesComponent', () => { it('should render search link when searchFilter is present', () => { comp.searchFilter = 'subject'; - fixture.detectChanges(); - - const value = mockMetadata[0].value; - - const link = fixture.debugElement.query(By.css('a.ds-search-link')); - - expect(link).not.toBeNull(); - expect(link.nativeElement.textContent).toContain(value); + expect(comp.hasSearchFilter()).toBeTrue(); + expect(comp.getSearchQueryParams('test')).toEqual({ 'f.subject': 'test,equals' }); }); it('should render browse link when browseDefinition is provided', () => { @@ -160,12 +151,7 @@ describe('MetadataValuesComponent', () => { order: 0, getRenderType: () => 'metadata', } as any; - - fixture.detectChanges(); - - const link = fixture.debugElement.query(By.css('a.ds-browse-link')); - - expect(link).not.toBeNull(); + expect(comp.hasBrowseDefinition()).toBeTrue(); }); }); From fe717c63d39c590eec31f6029ca4960c0e348dc1 Mon Sep 17 00:00:00 2001 From: guillermo2519 Date: Sun, 12 Apr 2026 22:18:36 -0600 Subject: [PATCH 3/4] chore: re-run CI From 10f631aefcafbaff3c16a244577b7b22a99fb2c2 Mon Sep 17 00:00:00 2001 From: guillermo2519 Date: Sun, 12 Apr 2026 22:58:04 -0600 Subject: [PATCH 4/4] chore: re-run CI