diff --git a/src/app/core/browse/browse-definition-data.service.spec.ts b/src/app/core/browse/browse-definition-data.service.spec.ts index 9377dc715f..f321c2551c 100644 --- a/src/app/core/browse/browse-definition-data.service.spec.ts +++ b/src/app/core/browse/browse-definition-data.service.spec.ts @@ -2,27 +2,66 @@ import { BrowseDefinitionDataService } from './browse-definition-data.service'; import { followLink } from '../../shared/utils/follow-link-config.model'; import { EMPTY } from 'rxjs'; import { FindListOptions } from '../data/find-list-options.model'; +import { getMockRemoteDataBuildService } from '../../shared/mocks/remote-data-build.service.mock'; +import { RequestService } from '../data/request.service'; +import { HALEndpointServiceStub } from '../../shared/testing/hal-endpoint-service.stub'; +import { getMockObjectCacheService } from '../../shared/mocks/object-cache.service.mock'; describe(`BrowseDefinitionDataService`, () => { + let requestService: RequestService; let service: BrowseDefinitionDataService; - const findAllDataSpy = jasmine.createSpyObj('findAllData', { - findAll: EMPTY, - }); + let findAllDataSpy; + let searchDataSpy; + const browsesEndpointURL = 'https://rest.api/browses'; + const halService: any = new HALEndpointServiceStub(browsesEndpointURL); + const options = new FindListOptions(); const linksToFollow = [ followLink('entries'), followLink('items') ]; + function initTestService() { + return new BrowseDefinitionDataService( + requestService, + getMockRemoteDataBuildService(), + getMockObjectCacheService(), + halService, + ); + } + beforeEach(() => { - service = new BrowseDefinitionDataService(null, null, null, null); + service = initTestService(); + findAllDataSpy = jasmine.createSpyObj('findAllData', { + findAll: EMPTY, + }); + searchDataSpy = jasmine.createSpyObj('searchData', { + searchBy: EMPTY, + getSearchByHref: EMPTY, + }); (service as any).findAllData = findAllDataSpy; + (service as any).searchData = searchDataSpy; }); + describe('findByFields', () => { + it(`should call searchByHref on searchData`, () => { + service.findByFields(['test'], true, false, ...linksToFollow); + expect(searchDataSpy.getSearchByHref).toHaveBeenCalled(); + }); + }); + describe('searchBy', () => { + it(`should call searchBy on searchData`, () => { + service.searchBy('test', options, true, false, ...linksToFollow); + expect(searchDataSpy.searchBy).toHaveBeenCalledWith('test', options, true, false, ...linksToFollow); + }); + }); describe(`findAll`, () => { it(`should call findAll on findAllData`, () => { service.findAll(options, true, false, ...linksToFollow); expect(findAllDataSpy.findAll).toHaveBeenCalledWith(options, true, false, ...linksToFollow); }); }); + + + }); diff --git a/src/app/core/browse/browse-definition-data.service.ts b/src/app/core/browse/browse-definition-data.service.ts index 32c3b44e14..88d070000e 100644 --- a/src/app/core/browse/browse-definition-data.service.ts +++ b/src/app/core/browse/browse-definition-data.service.ts @@ -13,6 +13,8 @@ import { FindListOptions } from '../data/find-list-options.model'; import { IdentifiableDataService } from '../data/base/identifiable-data.service'; import { FindAllData, FindAllDataImpl } from '../data/base/find-all-data'; import { dataService } from '../data/base/data-service.decorator'; +import { RequestParam } from '../cache/models/request-param.model'; +import { SearchData, SearchDataImpl } from '../data/base/search-data'; /** * Data service responsible for retrieving browse definitions from the REST server @@ -21,8 +23,9 @@ import { dataService } from '../data/base/data-service.decorator'; providedIn: 'root', }) @dataService(BROWSE_DEFINITION) -export class BrowseDefinitionDataService extends IdentifiableDataService implements FindAllData { +export class BrowseDefinitionDataService extends IdentifiableDataService implements FindAllData, SearchData { private findAllData: FindAllDataImpl; + private searchData: SearchDataImpl; constructor( protected requestService: RequestService, @@ -31,7 +34,7 @@ export class BrowseDefinitionDataService extends IdentifiableDataService[]): Observable>> { return this.findAllData.findAll(options, useCachedVersionIfAvailable, reRequestOnStale, ...linksToFollow); } + + /** + * Make a new FindListRequest with given search method + * + * @param searchMethod The search method for the object + * @param options The [[FindListOptions]] object + * @param useCachedVersionIfAvailable If this is true, the request will only be sent if there's + * no valid cached version. Defaults to true + * @param reRequestOnStale Whether or not the request should automatically be re- + * requested after the response becomes stale + * @param linksToFollow List of {@link FollowLinkConfig} that indicate which + * {@link HALLink}s should be automatically resolved + * @return {Observable>} + * Return an observable that emits response from the server + */ + public searchBy(searchMethod: string, options?: FindListOptions, useCachedVersionIfAvailable?: boolean, reRequestOnStale?: boolean, ...linksToFollow: FollowLinkConfig[]): Observable>> { + return this.searchData.searchBy(searchMethod, options, useCachedVersionIfAvailable, reRequestOnStale, ...linksToFollow); + } + + /** + * Create the HREF for a specific object's search method with given options object + * + * @param searchMethod The search method for the object + * @param options The [[FindListOptions]] object + * @return {Observable} + * Return an observable that emits created HREF + * @param linksToFollow List of {@link FollowLinkConfig} that indicate which {@link HALLink}s should be automatically resolved + */ + public getSearchByHref(searchMethod: string, options?: FindListOptions, ...linksToFollow: FollowLinkConfig[]): Observable { + return this.searchData.getSearchByHref(searchMethod, options, ...linksToFollow); + } + + /** + * Get the browse URL by providing a list of metadata keys. The first matching browse index definition + * for any of the fields is returned. This is used in eg. item page field component, which can be configured + * with several fields for a component like 'Author', and needs to know if and how to link the values + * to configured browse indices. + * + * @param fields an array of field strings, eg. ['dc.contributor.author', 'dc.creator'] + * @param useCachedVersionIfAvailable Override the data service useCachedVersionIfAvailable parameter (default: true) + * @param reRequestOnStale Override the data service reRequestOnStale parameter (default: true) + * @param linksToFollow Override the data service linksToFollow parameter (default: empty array) + */ + findByFields( + fields: string[], + useCachedVersionIfAvailable = true, + reRequestOnStale = true, + ...linksToFollow: FollowLinkConfig[] + ): Observable> { + const searchParams = []; + searchParams.push(new RequestParam('fields', fields)); + + const hrefObs = this.getSearchByHref( + 'byFields', + { searchParams }, + ...linksToFollow + ); + + return this.findByHref( + hrefObs, + useCachedVersionIfAvailable, + reRequestOnStale, + ...linksToFollow, + ); + } + } diff --git a/src/app/core/browse/browse.service.ts b/src/app/core/browse/browse.service.ts index 2fab189254..be28015069 100644 --- a/src/app/core/browse/browse.service.ts +++ b/src/app/core/browse/browse.service.ts @@ -19,9 +19,9 @@ import { } from '../shared/operators'; import { URLCombiner } from '../url-combiner/url-combiner'; import { BrowseEntrySearchOptions } from './browse-entry-search-options.model'; -import { BrowseDefinitionDataService } from './browse-definition-data.service'; import { HrefOnlyDataService } from '../data/href-only-data.service'; import { followLink, FollowLinkConfig } from '../../shared/utils/follow-link-config.model'; +import { BrowseDefinitionDataService } from './browse-definition-data.service'; export const BROWSE_LINKS_TO_FOLLOW: FollowLinkConfig[] = [ @@ -35,7 +35,7 @@ export const BROWSE_LINKS_TO_FOLLOW: FollowLinkConfig[] = [ export class BrowseService { protected linkPath = 'browses'; - private static toSearchKeyArray(metadataKey: string): string[] { + public static toSearchKeyArray(metadataKey: string): string[] { const keyParts = metadataKey.split('.'); const searchFor = []; searchFor.push('*'); diff --git a/src/app/core/shared/metadata-representation/metadata-representation.model.ts b/src/app/core/shared/metadata-representation/metadata-representation.model.ts index 06387966f7..379a3d1be8 100644 --- a/src/app/core/shared/metadata-representation/metadata-representation.model.ts +++ b/src/app/core/shared/metadata-representation/metadata-representation.model.ts @@ -1,11 +1,14 @@ /** * An Enum defining the representation type of metadata */ +import { BrowseDefinition } from '../browse-definition.model'; + export enum MetadataRepresentationType { None = 'none', Item = 'item', AuthorityControlled = 'authority_controlled', - PlainText = 'plain_text' + PlainText = 'plain_text', + BrowseLink = 'browse_link' } /** @@ -24,8 +27,14 @@ export interface MetadataRepresentation { */ representationType: MetadataRepresentationType; + /** + * The browse definition (optional) + */ + browseDefinition?: BrowseDefinition; + /** * Fetches the value to be displayed */ getValue(): string; + } diff --git a/src/app/core/shared/metadata-representation/metadatum/metadatum-representation.model.ts b/src/app/core/shared/metadata-representation/metadatum/metadatum-representation.model.ts index 595147f3e6..a09de12ae4 100644 --- a/src/app/core/shared/metadata-representation/metadatum/metadatum-representation.model.ts +++ b/src/app/core/shared/metadata-representation/metadatum/metadatum-representation.model.ts @@ -1,6 +1,7 @@ import { MetadataRepresentation, MetadataRepresentationType } from '../metadata-representation.model'; import { hasValue } from '../../../../shared/empty.util'; import { MetadataValue } from '../../metadata.models'; +import { BrowseDefinition } from '../../browse-definition.model'; /** * This class defines the way the metadatum it extends should be represented @@ -12,9 +13,15 @@ export class MetadatumRepresentation extends MetadataValue implements MetadataRe */ itemType: string; - constructor(itemType: string) { + /** + * The browse definition ID passed in with the metadatum, if any + */ + browseDefinition?: BrowseDefinition; + + constructor(itemType: string, browseDefinition?: BrowseDefinition) { super(); this.itemType = itemType; + this.browseDefinition = browseDefinition; } /** @@ -23,6 +30,8 @@ export class MetadatumRepresentation extends MetadataValue implements MetadataRe get representationType(): MetadataRepresentationType { if (hasValue(this.authority)) { return MetadataRepresentationType.AuthorityControlled; + } else if (hasValue(this.browseDefinition)) { + return MetadataRepresentationType.BrowseLink; } else { return MetadataRepresentationType.PlainText; } diff --git a/src/app/entity-groups/journal-entities/item-pages/journal/journal.component.spec.ts b/src/app/entity-groups/journal-entities/item-pages/journal/journal.component.spec.ts index 7e20edca6b..6e2ded334b 100644 --- a/src/app/entity-groups/journal-entities/item-pages/journal/journal.component.spec.ts +++ b/src/app/entity-groups/journal-entities/item-pages/journal/journal.component.spec.ts @@ -35,6 +35,10 @@ import { VersionDataService } from '../../../../core/data/version-data.service'; import { WorkspaceitemDataService } from '../../../../core/submission/workspaceitem-data.service'; import { SearchService } from '../../../../core/shared/search/search.service'; import { mockRouteService } from '../../../../item-page/simple/item-types/shared/item.component.spec'; +import { + BrowseDefinitionDataServiceStub +} from '../../../../shared/testing/browse-definition-data-service.stub'; +import { BrowseDefinitionDataService } from '../../../../core/browse/browse-definition-data.service'; let comp: JournalComponent; let fixture: ComponentFixture; @@ -100,7 +104,8 @@ describe('JournalComponent', () => { { provide: BitstreamDataService, useValue: mockBitstreamDataService }, { provide: WorkspaceitemDataService, useValue: {} }, { provide: SearchService, useValue: {} }, - { provide: RouteService, useValue: mockRouteService } + { provide: RouteService, useValue: mockRouteService }, + { provide: BrowseDefinitionDataService, useValue: BrowseDefinitionDataServiceStub } ], schemas: [NO_ERRORS_SCHEMA] 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 becb35e4bd..61088edd16 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 @@ -1,16 +1,38 @@ - + + + + + + + + + {{value}} + + + + + {{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 9e599b1294..23f8098207 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 @@ -52,6 +52,7 @@ describe('MetadataValuesComponent', () => { comp.mdValues = mockMetadata; comp.separator = mockSeperator; comp.label = mockLabel; + comp.urlRegex = /^.*test.*$/; fixture.detectChanges(); })); @@ -67,4 +68,9 @@ describe('MetadataValuesComponent', () => { expect(separators.length).toBe(mockMetadata.length - 1); }); + it('should correctly detect a pattern on string containing "test"', () => { + const mdValue = {value: 'This is a test value'} as MetadataValue; + expect(comp.hasLink(mdValue)).toBe(true); + }); + }); 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 3f0c918796..49ce453fbe 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 @@ -1,6 +1,8 @@ import { Component, Inject, Input, OnChanges, SimpleChanges } from '@angular/core'; import { MetadataValue } from '../../../core/shared/metadata.models'; import { APP_CONFIG, AppConfig } from '../../../../config/app-config.interface'; +import { BrowseDefinition } from '../../../core/shared/browse-definition.model'; +import { hasValue } from '../../../shared/empty.util'; /** * This component renders the configured 'values' into the ds-metadata-field-wrapper component. @@ -40,12 +42,51 @@ export class MetadataValuesComponent implements OnChanges { */ @Input() enableMarkdown = false; + /** + * Whether any valid HTTP(S) URL should be rendered as a link + */ + @Input() urlRegex?; + /** * This variable will be true if both {@link environment.markdown.enabled} and {@link enableMarkdown} are true. */ renderMarkdown; + @Input() browseDefinition?: BrowseDefinition; + ngOnChanges(changes: SimpleChanges): void { this.renderMarkdown = !!this.appConfig.markdown.enabled && this.enableMarkdown; } + + /** + * Does this metadata value have a configured link to a browse definition? + */ + hasBrowseDefinition(): boolean { + return hasValue(this.browseDefinition); + } + + /** + * Does this metadata value have a valid URL that should be rendered as a link? + * @param value A MetadataValue being displayed + */ + hasLink(value: MetadataValue): boolean { + if (hasValue(this.urlRegex)) { + const pattern = new RegExp(this.urlRegex); + return pattern.test(value.value); + } + return false; + } + + /** + * Return a queryparams object for use in a link, with the key dependent on whether this browse + * definition is metadata browse, or item browse + * @param value the specific metadata value being linked + */ + getQueryParams(value) { + let queryParams = {startsWith: value}; + if (this.browseDefinition.metadataBrowse) { + return {value: value}; + } + return queryParams; + } } diff --git a/src/app/item-page/simple/field-components/specific-field/abstract/item-page-abstract-field.component.spec.ts b/src/app/item-page/simple/field-components/specific-field/abstract/item-page-abstract-field.component.spec.ts index 53f0522f39..bfed3847c5 100644 --- a/src/app/item-page/simple/field-components/specific-field/abstract/item-page-abstract-field.component.spec.ts +++ b/src/app/item-page/simple/field-components/specific-field/abstract/item-page-abstract-field.component.spec.ts @@ -7,6 +7,8 @@ import { SharedModule } from '../../../../../shared/shared.module'; import { APP_CONFIG } from '../../../../../../config/app-config.interface'; import { environment } from '../../../../../../environments/environment'; import { By } from '@angular/platform-browser'; +import { BrowseDefinitionDataService } from '../../../../../core/browse/browse-definition-data.service'; +import { BrowseDefinitionDataServiceStub } from '../../../../../shared/testing/browse-definition-data-service.stub'; let comp: ItemPageAbstractFieldComponent; let fixture: ComponentFixture; @@ -25,6 +27,7 @@ describe('ItemPageAbstractFieldComponent', () => { ], providers: [ { provide: APP_CONFIG, useValue: environment }, + { provide: BrowseDefinitionDataService, useValue: BrowseDefinitionDataServiceStub } ], declarations: [ItemPageAbstractFieldComponent], schemas: [NO_ERRORS_SCHEMA] diff --git a/src/app/item-page/simple/field-components/specific-field/author/item-page-author-field.component.spec.ts b/src/app/item-page/simple/field-components/specific-field/author/item-page-author-field.component.spec.ts index 7f8d6fb812..855a995142 100644 --- a/src/app/item-page/simple/field-components/specific-field/author/item-page-author-field.component.spec.ts +++ b/src/app/item-page/simple/field-components/specific-field/author/item-page-author-field.component.spec.ts @@ -3,10 +3,12 @@ import { ChangeDetectionStrategy, NO_ERRORS_SCHEMA } from '@angular/core'; import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing'; import { TranslateLoaderMock } from '../../../../../shared/testing/translate-loader.mock'; import { MetadataValuesComponent } from '../../../../field-components/metadata-values/metadata-values.component'; -import { mockItemWithMetadataFieldAndValue } from '../item-page-field.component.spec'; +import { mockItemWithMetadataFieldsAndValue } from '../item-page-field.component.spec'; import { ItemPageAuthorFieldComponent } from './item-page-author-field.component'; import { APP_CONFIG } from '../../../../../../config/app-config.interface'; import { environment } from '../../../../../../environments/environment'; +import { BrowseDefinitionDataService } from '../../../../../core/browse/browse-definition-data.service'; +import { BrowseDefinitionDataServiceStub } from '../../../../../shared/testing/browse-definition-data-service.stub'; let comp: ItemPageAuthorFieldComponent; let fixture: ComponentFixture; @@ -25,6 +27,7 @@ describe('ItemPageAuthorFieldComponent', () => { })], providers: [ { provide: APP_CONFIG, useValue: environment }, + { provide: BrowseDefinitionDataService, useValue: BrowseDefinitionDataServiceStub } ], declarations: [ItemPageAuthorFieldComponent, MetadataValuesComponent], schemas: [NO_ERRORS_SCHEMA] @@ -37,7 +40,7 @@ describe('ItemPageAuthorFieldComponent', () => { beforeEach(waitForAsync(() => { fixture = TestBed.createComponent(ItemPageAuthorFieldComponent); comp = fixture.componentInstance; - comp.item = mockItemWithMetadataFieldAndValue(field, mockValue); + comp.item = mockItemWithMetadataFieldsAndValue([field], mockValue); fixture.detectChanges(); })); diff --git a/src/app/item-page/simple/field-components/specific-field/date/item-page-date-field.component.spec.ts b/src/app/item-page/simple/field-components/specific-field/date/item-page-date-field.component.spec.ts index ebd37e8b8a..be124dab82 100644 --- a/src/app/item-page/simple/field-components/specific-field/date/item-page-date-field.component.spec.ts +++ b/src/app/item-page/simple/field-components/specific-field/date/item-page-date-field.component.spec.ts @@ -3,10 +3,12 @@ import { ChangeDetectionStrategy, NO_ERRORS_SCHEMA } from '@angular/core'; import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing'; import { TranslateLoaderMock } from '../../../../../shared/testing/translate-loader.mock'; import { MetadataValuesComponent } from '../../../../field-components/metadata-values/metadata-values.component'; -import { mockItemWithMetadataFieldAndValue } from '../item-page-field.component.spec'; +import { mockItemWithMetadataFieldsAndValue } from '../item-page-field.component.spec'; import { ItemPageDateFieldComponent } from './item-page-date-field.component'; import { APP_CONFIG } from '../../../../../../config/app-config.interface'; import { environment } from '../../../../../../environments/environment'; +import { BrowseDefinitionDataService } from '../../../../../core/browse/browse-definition-data.service'; +import { BrowseDefinitionDataServiceStub } from '../../../../../shared/testing/browse-definition-data-service.stub'; let comp: ItemPageDateFieldComponent; let fixture: ComponentFixture; @@ -25,6 +27,7 @@ describe('ItemPageDateFieldComponent', () => { })], providers: [ { provide: APP_CONFIG, useValue: environment }, + { provide: BrowseDefinitionDataService, useValue: BrowseDefinitionDataServiceStub } ], declarations: [ItemPageDateFieldComponent, MetadataValuesComponent], schemas: [NO_ERRORS_SCHEMA] @@ -36,7 +39,7 @@ describe('ItemPageDateFieldComponent', () => { beforeEach(waitForAsync(() => { fixture = TestBed.createComponent(ItemPageDateFieldComponent); comp = fixture.componentInstance; - comp.item = mockItemWithMetadataFieldAndValue(mockField, mockValue); + comp.item = mockItemWithMetadataFieldsAndValue([mockField], mockValue); fixture.detectChanges(); })); diff --git a/src/app/item-page/simple/field-components/specific-field/generic/generic-item-page-field.component.spec.ts b/src/app/item-page/simple/field-components/specific-field/generic/generic-item-page-field.component.spec.ts index f055101f2a..fdf5ac1bb5 100644 --- a/src/app/item-page/simple/field-components/specific-field/generic/generic-item-page-field.component.spec.ts +++ b/src/app/item-page/simple/field-components/specific-field/generic/generic-item-page-field.component.spec.ts @@ -3,10 +3,12 @@ import { ChangeDetectionStrategy, NO_ERRORS_SCHEMA } from '@angular/core'; import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing'; import { TranslateLoaderMock } from '../../../../../shared/testing/translate-loader.mock'; import { MetadataValuesComponent } from '../../../../field-components/metadata-values/metadata-values.component'; -import { mockItemWithMetadataFieldAndValue } from '../item-page-field.component.spec'; +import { mockItemWithMetadataFieldsAndValue } from '../item-page-field.component.spec'; import { GenericItemPageFieldComponent } from './generic-item-page-field.component'; import { APP_CONFIG } from '../../../../../../config/app-config.interface'; import { environment } from '../../../../../../environments/environment'; +import { BrowseDefinitionDataService } from '../../../../../core/browse/browse-definition-data.service'; +import { BrowseDefinitionDataServiceStub } from '../../../../../shared/testing/browse-definition-data-service.stub'; let comp: GenericItemPageFieldComponent; let fixture: ComponentFixture; @@ -27,6 +29,7 @@ describe('GenericItemPageFieldComponent', () => { })], providers: [ { provide: APP_CONFIG, useValue: environment }, + { provide: BrowseDefinitionDataService, useValue: BrowseDefinitionDataServiceStub } ], declarations: [GenericItemPageFieldComponent, MetadataValuesComponent], schemas: [NO_ERRORS_SCHEMA] @@ -38,7 +41,7 @@ describe('GenericItemPageFieldComponent', () => { beforeEach(waitForAsync(() => { fixture = TestBed.createComponent(GenericItemPageFieldComponent); comp = fixture.componentInstance; - comp.item = mockItemWithMetadataFieldAndValue(mockField, mockValue); + comp.item = mockItemWithMetadataFieldsAndValue([mockField], mockValue); comp.fields = mockFields; comp.label = mockLabel; fixture.detectChanges(); diff --git a/src/app/item-page/simple/field-components/specific-field/generic/generic-item-page-field.component.ts b/src/app/item-page/simple/field-components/specific-field/generic/generic-item-page-field.component.ts index 9d818e2f7d..53d2f6aa20 100644 --- a/src/app/item-page/simple/field-components/specific-field/generic/generic-item-page-field.component.ts +++ b/src/app/item-page/simple/field-components/specific-field/generic/generic-item-page-field.component.ts @@ -40,5 +40,10 @@ export class GenericItemPageFieldComponent extends ItemPageFieldComponent { */ @Input() enableMarkdown = false; + /** + * Whether any valid HTTP(S) URL should be rendered as a link + */ + @Input() urlRegex?: string; + } 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 8b017c77f5..91d40b0ad7 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 @@ -4,5 +4,7 @@ [separator]="separator" [label]="label" [enableMarkdown]="enableMarkdown" + [urlRegex]="urlRegex" + [browseDefinition]="browseDefinition|async" > diff --git a/src/app/item-page/simple/field-components/specific-field/item-page-field.component.spec.ts b/src/app/item-page/simple/field-components/specific-field/item-page-field.component.spec.ts index e41fd1b8a7..15b7a9df21 100644 --- a/src/app/item-page/simple/field-components/specific-field/item-page-field.component.spec.ts +++ b/src/app/item-page/simple/field-components/specific-field/item-page-field.component.spec.ts @@ -12,6 +12,10 @@ import { environment } from '../../../../../environments/environment'; import { MarkdownPipe } from '../../../../shared/utils/markdown.pipe'; import { SharedModule } from '../../../../shared/shared.module'; import { APP_CONFIG } from '../../../../../config/app-config.interface'; +import { By } from '@angular/platform-browser'; +import { BrowseDefinitionDataService } from '../../../../core/browse/browse-definition-data.service'; +import { BrowseDefinitionDataServiceStub } from '../../../../shared/testing/browse-definition-data-service.stub'; +import { RouterTestingModule } from '@angular/router/testing'; let comp: ItemPageFieldComponent; let fixture: ComponentFixture; @@ -20,7 +24,9 @@ let markdownSpy; const mockValue = 'test value'; const mockField = 'dc.test'; const mockLabel = 'test label'; -const mockFields = [mockField]; +const mockAuthorField = 'dc.contributor.author'; +const mockDateIssuedField = 'dc.date.issued'; +const mockFields = [mockField, mockAuthorField, mockDateIssuedField]; describe('ItemPageFieldComponent', () => { @@ -34,6 +40,7 @@ describe('ItemPageFieldComponent', () => { const buildTestEnvironment = async () => { await TestBed.configureTestingModule({ imports: [ + RouterTestingModule.withRoutes([]), TranslateModule.forRoot({ loader: { provide: TranslateLoader, @@ -44,6 +51,7 @@ describe('ItemPageFieldComponent', () => { ], providers: [ { provide: APP_CONFIG, useValue: appConfig }, + { provide: BrowseDefinitionDataService, useValue: BrowseDefinitionDataServiceStub } ], declarations: [ItemPageFieldComponent, MetadataValuesComponent], schemas: [NO_ERRORS_SCHEMA] @@ -53,7 +61,7 @@ describe('ItemPageFieldComponent', () => { markdownSpy = spyOn(MarkdownPipe.prototype, 'transform'); fixture = TestBed.createComponent(ItemPageFieldComponent); comp = fixture.componentInstance; - comp.item = mockItemWithMetadataFieldAndValue(mockField, mockValue); + comp.item = mockItemWithMetadataFieldsAndValue(mockFields, mockValue); comp.fields = mockFields; comp.label = mockLabel; fixture.detectChanges(); @@ -126,17 +134,57 @@ describe('ItemPageFieldComponent', () => { expect(markdownSpy).toHaveBeenCalled(); }); }); + }); + + describe('test rendering of configured browse links', () => { + beforeEach(() => { + fixture.detectChanges(); + }); + waitForAsync(() => { + it('should have a browse link', () => { + expect(fixture.debugElement.query(By.css('a.ds-browse-link')).nativeElement.innerHTML).toContain(mockValue); + }); + }); + }); + + describe('test rendering of configured regex-based links', () => { + beforeEach(() => { + comp.urlRegex = '^test'; + fixture.detectChanges(); + }); + waitForAsync(() => { + it('should have a rendered (non-browse) link since the value matches ^test', () => { + expect(fixture.debugElement.query(By.css('a.ds-simple-metadata-link')).nativeElement.innerHTML).toContain(mockValue); + }); + }); + }); + + describe('test skipping of configured links that do NOT match regex', () => { + beforeEach(() => { + comp.urlRegex = '^nope'; + fixture.detectChanges(); + }); + beforeEach(waitForAsync(() => { + it('should NOT have a rendered (non-browse) link since the value matches ^test', () => { + expect(fixture.debugElement.query(By.css('a.ds-simple-metadata-link'))).toBeNull(); + }); + })); + }); + + }); -export function mockItemWithMetadataFieldAndValue(field: string, value: string): Item { +export function mockItemWithMetadataFieldsAndValue(fields: string[], value: string): Item { const item = Object.assign(new Item(), { bundles: createSuccessfulRemoteDataObject$(createPaginatedList([])), metadata: new MetadataMap() }); - item.metadata[field] = [{ - language: 'en_US', - value: value - }] as MetadataValue[]; + fields.forEach((field: string) => { + item.metadata[field] = [{ + language: 'en_US', + value: value + }] as MetadataValue[]; + }); return item; } 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 681c5e16bc..fc526dabcc 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 @@ -1,5 +1,10 @@ import { Component, Input } from '@angular/core'; import { Item } from '../../../../core/shared/item.model'; +import { map } from 'rxjs/operators'; +import { Observable } from 'rxjs'; +import { BrowseDefinition } from '../../../../core/shared/browse-definition.model'; +import { BrowseDefinitionDataService } from '../../../../core/browse/browse-definition-data.service'; +import { getRemoteDataPayload } from '../../../../core/shared/operators'; /** * This component can be used to represent metadata on a simple item page. @@ -12,6 +17,9 @@ import { Item } from '../../../../core/shared/item.model'; }) export class ItemPageFieldComponent { + constructor(protected browseDefinitionDataService: BrowseDefinitionDataService) { + } + /** * The item to display metadata for */ @@ -38,4 +46,19 @@ export class ItemPageFieldComponent { */ separator = '
'; + /** + * Whether any valid HTTP(S) URL should be rendered as a link + */ + urlRegex?: string; + + /** + * 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.) + */ + get browseDefinition(): Observable { + return this.browseDefinitionDataService.findByFields(this.fields).pipe( + getRemoteDataPayload(), + map((def) => def) + ); + } } diff --git a/src/app/item-page/simple/field-components/specific-field/title/item-page-title-field.component.spec.ts b/src/app/item-page/simple/field-components/specific-field/title/item-page-title-field.component.spec.ts index bc661e81c9..316e08e564 100644 --- a/src/app/item-page/simple/field-components/specific-field/title/item-page-title-field.component.spec.ts +++ b/src/app/item-page/simple/field-components/specific-field/title/item-page-title-field.component.spec.ts @@ -3,7 +3,7 @@ import { ChangeDetectionStrategy, NO_ERRORS_SCHEMA } from '@angular/core'; import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing'; import { TranslateLoaderMock } from '../../../../../shared/testing/translate-loader.mock'; import { MetadataValuesComponent } from '../../../../field-components/metadata-values/metadata-values.component'; -import { mockItemWithMetadataFieldAndValue } from '../item-page-field.component.spec'; +import { mockItemWithMetadataFieldsAndValue } from '../item-page-field.component.spec'; import { ItemPageTitleFieldComponent } from './item-page-title-field.component'; let comp: ItemPageTitleFieldComponent; @@ -31,7 +31,7 @@ describe('ItemPageTitleFieldComponent', () => { beforeEach(waitForAsync(() => { fixture = TestBed.createComponent(ItemPageTitleFieldComponent); comp = fixture.componentInstance; - comp.item = mockItemWithMetadataFieldAndValue(mockField, mockValue); + comp.item = mockItemWithMetadataFieldsAndValue([mockField], mockValue); fixture.detectChanges(); })); diff --git a/src/app/item-page/simple/field-components/specific-field/uri/item-page-uri-field.component.spec.ts b/src/app/item-page/simple/field-components/specific-field/uri/item-page-uri-field.component.spec.ts index 7c766252a3..cc55b76e3e 100644 --- a/src/app/item-page/simple/field-components/specific-field/uri/item-page-uri-field.component.spec.ts +++ b/src/app/item-page/simple/field-components/specific-field/uri/item-page-uri-field.component.spec.ts @@ -2,11 +2,13 @@ import { TranslateLoader, TranslateModule } from '@ngx-translate/core'; import { ChangeDetectionStrategy, NO_ERRORS_SCHEMA } from '@angular/core'; import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing'; import { TranslateLoaderMock } from '../../../../../shared/testing/translate-loader.mock'; -import { mockItemWithMetadataFieldAndValue } from '../item-page-field.component.spec'; +import { mockItemWithMetadataFieldsAndValue } from '../item-page-field.component.spec'; import { ItemPageUriFieldComponent } from './item-page-uri-field.component'; import { MetadataUriValuesComponent } from '../../../../field-components/metadata-uri-values/metadata-uri-values.component'; import { environment } from '../../../../../../environments/environment'; import { APP_CONFIG } from '../../../../../../config/app-config.interface'; +import { BrowseDefinitionDataService } from '../../../../../core/browse/browse-definition-data.service'; +import { BrowseDefinitionDataServiceStub } from '../../../../../shared/testing/browse-definition-data-service.stub'; let comp: ItemPageUriFieldComponent; let fixture: ComponentFixture; @@ -26,6 +28,7 @@ describe('ItemPageUriFieldComponent', () => { })], providers: [ { provide: APP_CONFIG, useValue: environment }, + { provide: BrowseDefinitionDataService, useValue: BrowseDefinitionDataServiceStub } ], declarations: [ItemPageUriFieldComponent, MetadataUriValuesComponent], schemas: [NO_ERRORS_SCHEMA] @@ -37,7 +40,7 @@ describe('ItemPageUriFieldComponent', () => { beforeEach(waitForAsync(() => { fixture = TestBed.createComponent(ItemPageUriFieldComponent); comp = fixture.componentInstance; - comp.item = mockItemWithMetadataFieldAndValue(mockField, mockValue); + comp.item = mockItemWithMetadataFieldsAndValue([mockField], mockValue); comp.fields = [mockField]; comp.label = mockLabel; fixture.detectChanges(); diff --git a/src/app/item-page/simple/item-types/publication/publication.component.spec.ts b/src/app/item-page/simple/item-types/publication/publication.component.spec.ts index 3c2ff4f844..211ec102bc 100644 --- a/src/app/item-page/simple/item-types/publication/publication.component.spec.ts +++ b/src/app/item-page/simple/item-types/publication/publication.component.spec.ts @@ -36,6 +36,10 @@ import { VersionDataService } from '../../../../core/data/version-data.service'; import { RouterTestingModule } from '@angular/router/testing'; import { WorkspaceitemDataService } from '../../../../core/submission/workspaceitem-data.service'; import { SearchService } from '../../../../core/shared/search/search.service'; +import { BrowseDefinitionDataService } from '../../../../core/browse/browse-definition-data.service'; +import { + BrowseDefinitionDataServiceStub +} from '../../../../shared/testing/browse-definition-data-service.stub'; const noMetadata = new MetadataMap(); @@ -87,7 +91,8 @@ describe('PublicationComponent', () => { { provide: BitstreamDataService, useValue: mockBitstreamDataService }, { provide: WorkspaceitemDataService, useValue: {} }, { provide: SearchService, useValue: {} }, - { provide: RouteService, useValue: mockRouteService } + { provide: RouteService, useValue: mockRouteService }, + { provide: BrowseDefinitionDataService, useValue: BrowseDefinitionDataServiceStub }, ], schemas: [NO_ERRORS_SCHEMA] diff --git a/src/app/item-page/simple/item-types/shared/item.component.spec.ts b/src/app/item-page/simple/item-types/shared/item.component.spec.ts index cb91a31b06..5bf08fc004 100644 --- a/src/app/item-page/simple/item-types/shared/item.component.spec.ts +++ b/src/app/item-page/simple/item-types/shared/item.component.spec.ts @@ -23,7 +23,9 @@ import { UUIDService } from '../../../../core/shared/uuid.service'; import { isNotEmpty } from '../../../../shared/empty.util'; import { TranslateLoaderMock } from '../../../../shared/mocks/translate-loader.mock'; import { NotificationsService } from '../../../../shared/notifications/notifications.service'; -import { createSuccessfulRemoteDataObject$ } from '../../../../shared/remote-data.utils'; +import { + createSuccessfulRemoteDataObject$ +} from '../../../../shared/remote-data.utils'; import { TruncatableService } from '../../../../shared/truncatable/truncatable.service'; import { TruncatePipe } from '../../../../shared/utils/truncate.pipe'; import { GenericItemPageFieldComponent } from '../../field-components/specific-field/generic/generic-item-page-field.component'; @@ -38,6 +40,10 @@ import { VersionHistoryDataService } from '../../../../core/data/version-history import { RouterTestingModule } from '@angular/router/testing'; import { AuthorizationDataService } from '../../../../core/data/feature-authorization/authorization-data.service'; import { ResearcherProfileDataService } from '../../../../core/profile/researcher-profile-data.service'; +import { BrowseDefinitionDataService } from '../../../../core/browse/browse-definition-data.service'; +import { + BrowseDefinitionDataServiceStub +} from '../../../../shared/testing/browse-definition-data-service.stub'; import { buildPaginatedList } from '../../../../core/data/paginated-list.model'; import { PageInfo } from '../../../../core/shared/page-info.model'; @@ -125,7 +131,8 @@ export function getItemPageFieldsTest(mockItem: Item, component) { { provide: SearchService, useValue: {} }, { provide: RouteService, useValue: mockRouteService }, { provide: AuthorizationDataService, useValue: authorizationService }, - { provide: ResearcherProfileDataService, useValue: {} } + { provide: ResearcherProfileDataService, useValue: {} }, + { provide: BrowseDefinitionDataService, useValue: BrowseDefinitionDataServiceStub }, ], schemas: [NO_ERRORS_SCHEMA] @@ -444,7 +451,7 @@ describe('ItemComponent', () => { { provide: SearchService, useValue: {} }, { provide: RouteService, useValue: mockRouteService }, { provide: AuthorizationDataService, useValue: {} }, - { provide: ResearcherProfileDataService, useValue: {} } + { provide: ResearcherProfileDataService, useValue: {} }, ], schemas: [NO_ERRORS_SCHEMA] }).overrideComponent(ItemComponent, { diff --git a/src/app/item-page/simple/item-types/untyped-item/untyped-item.component.spec.ts b/src/app/item-page/simple/item-types/untyped-item/untyped-item.component.spec.ts index 3581694a5e..4b7da40abe 100644 --- a/src/app/item-page/simple/item-types/untyped-item/untyped-item.component.spec.ts +++ b/src/app/item-page/simple/item-types/untyped-item/untyped-item.component.spec.ts @@ -37,6 +37,10 @@ import { RouterTestingModule } from '@angular/router/testing'; import { WorkspaceitemDataService } from '../../../../core/submission/workspaceitem-data.service'; import { SearchService } from '../../../../core/shared/search/search.service'; import { ItemVersionsSharedService } from '../../../versions/item-versions-shared.service'; +import { BrowseDefinitionDataService } from '../../../../core/browse/browse-definition-data.service'; +import { + BrowseDefinitionDataServiceStub +} from '../../../../shared/testing/browse-definition-data-service.stub'; const noMetadata = new MetadataMap(); @@ -90,7 +94,8 @@ describe('UntypedItemComponent', () => { { provide: SearchService, useValue: {} }, { provide: ItemDataService, useValue: {} }, { provide: ItemVersionsSharedService, useValue: {} }, - { provide: RouteService, useValue: mockRouteService } + { provide: RouteService, useValue: mockRouteService }, + { provide: BrowseDefinitionDataService, useValue: BrowseDefinitionDataServiceStub }, ], schemas: [NO_ERRORS_SCHEMA] }).overrideComponent(UntypedItemComponent, { diff --git a/src/app/item-page/simple/item-types/versioned-item/versioned-item.component.spec.ts b/src/app/item-page/simple/item-types/versioned-item/versioned-item.component.spec.ts index fcd82ce678..b29c7e58f3 100644 --- a/src/app/item-page/simple/item-types/versioned-item/versioned-item.component.spec.ts +++ b/src/app/item-page/simple/item-types/versioned-item/versioned-item.component.spec.ts @@ -77,7 +77,7 @@ describe('VersionedItemComponent', () => { { provide: WorkspaceitemDataService, useValue: {} }, { provide: SearchService, useValue: {} }, { provide: ItemDataService, useValue: {} }, - { provide: RouteService, useValue: mockRouteService } + { provide: RouteService, useValue: mockRouteService }, ] }).compileComponents(); versionService = TestBed.inject(VersionDataService); diff --git a/src/app/item-page/simple/item-types/versioned-item/versioned-item.component.ts b/src/app/item-page/simple/item-types/versioned-item/versioned-item.component.ts index a1f4cebd77..6855d9c4dc 100644 --- a/src/app/item-page/simple/item-types/versioned-item/versioned-item.component.ts +++ b/src/app/item-page/simple/item-types/versioned-item/versioned-item.component.ts @@ -35,7 +35,7 @@ export class VersionedItemComponent extends ItemComponent { private workspaceItemDataService: WorkspaceitemDataService, private searchService: SearchService, private itemService: ItemDataService, - protected routeService: RouteService, + protected routeService: RouteService ) { super(routeService, router); } diff --git a/src/app/item-page/simple/metadata-representation-list/metadata-representation-list.component.spec.ts b/src/app/item-page/simple/metadata-representation-list/metadata-representation-list.component.spec.ts index 54420721b8..180eaaa2be 100644 --- a/src/app/item-page/simple/metadata-representation-list/metadata-representation-list.component.spec.ts +++ b/src/app/item-page/simple/metadata-representation-list/metadata-representation-list.component.spec.ts @@ -11,6 +11,8 @@ import { MetadataValue } from '../../../core/shared/metadata.models'; import { DSpaceObject } from '../../../core/shared/dspace-object.model'; import { ItemMetadataRepresentation } from '../../../core/shared/metadata-representation/item/item-metadata-representation.model'; import { MetadatumRepresentation } from '../../../core/shared/metadata-representation/metadatum/metadatum-representation.model'; +import { BrowseDefinitionDataService } from '../../../core/browse/browse-definition-data.service'; +import { BrowseDefinitionDataServiceStub } from '../../../shared/testing/browse-definition-data-service.stub'; const itemType = 'Person'; const metadataFields = ['dc.contributor.author', 'dc.creator']; @@ -104,7 +106,8 @@ describe('MetadataRepresentationListComponent', () => { imports: [TranslateModule.forRoot()], declarations: [MetadataRepresentationListComponent, VarDirective], providers: [ - { provide: RelationshipDataService, useValue: relationshipService } + { provide: RelationshipDataService, useValue: relationshipService }, + { provide: BrowseDefinitionDataService, useValue: BrowseDefinitionDataServiceStub } ], schemas: [NO_ERRORS_SCHEMA] }).overrideComponent(MetadataRepresentationListComponent, { diff --git a/src/app/item-page/simple/metadata-representation-list/metadata-representation-list.component.ts b/src/app/item-page/simple/metadata-representation-list/metadata-representation-list.component.ts index 16dcf72cd4..d5e6547778 100644 --- a/src/app/item-page/simple/metadata-representation-list/metadata-representation-list.component.ts +++ b/src/app/item-page/simple/metadata-representation-list/metadata-representation-list.component.ts @@ -8,6 +8,13 @@ import { RelationshipDataService } from '../../../core/data/relationship-data.se import { MetadataValue } from '../../../core/shared/metadata.models'; import { Item } from '../../../core/shared/item.model'; import { AbstractIncrementalListComponent } from '../abstract-incremental-list/abstract-incremental-list.component'; +import { map } from 'rxjs/operators'; +import { getRemoteDataPayload } from '../../../core/shared/operators'; +import { + MetadatumRepresentation +} from '../../../core/shared/metadata-representation/metadatum/metadatum-representation.model'; +import { BrowseService } from '../../../core/browse/browse.service'; +import { BrowseDefinitionDataService } from '../../../core/browse/browse-definition-data.service'; @Component({ selector: 'ds-metadata-representation-list', @@ -52,7 +59,8 @@ export class MetadataRepresentationListComponent extends AbstractIncrementalList */ total: number; - constructor(public relationshipService: RelationshipDataService) { + constructor(public relationshipService: RelationshipDataService, + private browseDefinitionDataService: BrowseDefinitionDataService) { super(); } @@ -76,7 +84,21 @@ export class MetadataRepresentationListComponent extends AbstractIncrementalList ...metadata .slice((this.objects.length * this.incrementBy), (this.objects.length * this.incrementBy) + this.incrementBy) .map((metadatum: any) => Object.assign(new MetadataValue(), metadatum)) - .map((metadatum: MetadataValue) => this.relationshipService.resolveMetadataRepresentation(metadatum, this.parentItem, this.itemType)), + .map((metadatum: MetadataValue) => { + if (metadatum.isVirtual) { + return this.relationshipService.resolveMetadataRepresentation(metadatum, this.parentItem, this.itemType); + } else { + // Check for a configured browse link and return a standard metadata representation + let searchKeyArray: string[] = []; + this.metadataFields.forEach((field: string) => { + searchKeyArray = searchKeyArray.concat(BrowseService.toSearchKeyArray(field)); + }); + return this.browseDefinitionDataService.findByFields(this.metadataFields).pipe( + getRemoteDataPayload(), + map((def) => Object.assign(new MetadatumRepresentation(this.itemType, def), metadatum)) + ); + } + }), ); } } diff --git a/src/app/shared/object-list/metadata-representation-list-element/browse-link/browse-link-metadata-list-element.component.html b/src/app/shared/object-list/metadata-representation-list-element/browse-link/browse-link-metadata-list-element.component.html new file mode 100644 index 0000000000..8d3afea273 --- /dev/null +++ b/src/app/shared/object-list/metadata-representation-list-element/browse-link/browse-link-metadata-list-element.component.html @@ -0,0 +1,9 @@ + diff --git a/src/app/shared/object-list/metadata-representation-list-element/browse-link/browse-link-metadata-list-element.component.spec.ts b/src/app/shared/object-list/metadata-representation-list-element/browse-link/browse-link-metadata-list-element.component.spec.ts new file mode 100644 index 0000000000..32919d9758 --- /dev/null +++ b/src/app/shared/object-list/metadata-representation-list-element/browse-link/browse-link-metadata-list-element.component.spec.ts @@ -0,0 +1,62 @@ +import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing'; +import { ChangeDetectionStrategy, NO_ERRORS_SCHEMA } from '@angular/core'; +import { BrowseLinkMetadataListElementComponent } from './browse-link-metadata-list-element.component'; +import { MetadatumRepresentation } from '../../../../core/shared/metadata-representation/metadatum/metadatum-representation.model'; + +const mockMetadataRepresentation = Object.assign(new MetadatumRepresentation('type'), { + key: 'dc.contributor.author', + value: 'Test Author' +}); + +const mockMetadataRepresentationWithUrl = Object.assign(new MetadatumRepresentation('type'), { + key: 'dc.subject', + value: 'http://purl.org/test/subject' +}); + +describe('BrowseLinkMetadataListElementComponent', () => { + let comp: BrowseLinkMetadataListElementComponent; + let fixture: ComponentFixture; + + beforeEach(waitForAsync(() => { + TestBed.configureTestingModule({ + imports: [], + declarations: [BrowseLinkMetadataListElementComponent], + schemas: [NO_ERRORS_SCHEMA] + }).overrideComponent(BrowseLinkMetadataListElementComponent, { + set: { changeDetection: ChangeDetectionStrategy.Default } + }).compileComponents(); + })); + + beforeEach(waitForAsync(() => { + fixture = TestBed.createComponent(BrowseLinkMetadataListElementComponent); + comp = fixture.componentInstance; + comp.metadataRepresentation = mockMetadataRepresentation; + fixture.detectChanges(); + })); + + waitForAsync(() => { + it('should contain the value as a browse link', () => { + expect(fixture.debugElement.nativeElement.textContent).toContain(mockMetadataRepresentation.value); + }); + it('should NOT match isLink', () => { + expect(comp.isLink).toBe(false); + }); + }); + + beforeEach(waitForAsync(() => { + fixture = TestBed.createComponent(BrowseLinkMetadataListElementComponent); + comp = fixture.componentInstance; + comp.metadataRepresentation = mockMetadataRepresentationWithUrl; + fixture.detectChanges(); + })); + + waitForAsync(() => { + it('should contain the value expected', () => { + expect(fixture.debugElement.nativeElement.textContent).toContain(mockMetadataRepresentationWithUrl.value); + }); + it('should match isLink', () => { + expect(comp.isLink).toBe(true); + }); + }); + +}); diff --git a/src/app/shared/object-list/metadata-representation-list-element/browse-link/browse-link-metadata-list-element.component.ts b/src/app/shared/object-list/metadata-representation-list-element/browse-link/browse-link-metadata-list-element.component.ts new file mode 100644 index 0000000000..0eb0ce05b0 --- /dev/null +++ b/src/app/shared/object-list/metadata-representation-list-element/browse-link/browse-link-metadata-list-element.component.ts @@ -0,0 +1,29 @@ +import { MetadataRepresentationType } from '../../../../core/shared/metadata-representation/metadata-representation.model'; +import { Component } from '@angular/core'; +import { MetadataRepresentationListElementComponent } from '../metadata-representation-list-element.component'; +import { metadataRepresentationComponent } from '../../../metadata-representation/metadata-representation.decorator'; +//@metadataRepresentationComponent('Publication', MetadataRepresentationType.PlainText) +// For now, authority controlled fields are rendered the same way as plain text fields +//@metadataRepresentationComponent('Publication', MetadataRepresentationType.AuthorityControlled) +@metadataRepresentationComponent('Publication', MetadataRepresentationType.BrowseLink) +@Component({ + selector: 'ds-browse-link-metadata-list-element', + templateUrl: './browse-link-metadata-list-element.component.html' +}) +/** + * A component for displaying MetadataRepresentation objects in the form of plain text + * It will simply use the value retrieved from MetadataRepresentation.getValue() to display as plain text + */ +export class BrowseLinkMetadataListElementComponent extends MetadataRepresentationListElementComponent { + /** + * Get the appropriate query parameters for this browse link, depending on whether the browse definition + * expects 'startsWith' (eg browse by date) or 'value' (eg browse by title) + */ + getQueryParams() { + let queryParams = {startsWith: this.metadataRepresentation.getValue()}; + if (this.metadataRepresentation.browseDefinition.metadataBrowse) { + return {value: this.metadataRepresentation.getValue()}; + } + return queryParams; + } +} diff --git a/src/app/shared/object-list/metadata-representation-list-element/metadata-representation-list-element.component.spec.ts b/src/app/shared/object-list/metadata-representation-list-element/metadata-representation-list-element.component.spec.ts new file mode 100644 index 0000000000..f0cc150b3e --- /dev/null +++ b/src/app/shared/object-list/metadata-representation-list-element/metadata-representation-list-element.component.spec.ts @@ -0,0 +1,59 @@ +import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing'; +import { ChangeDetectionStrategy, NO_ERRORS_SCHEMA } from '@angular/core'; +import { MetadatumRepresentation } from '../../../core/shared/metadata-representation/metadatum/metadatum-representation.model'; +import { mockData } from '../../testing/browse-definition-data-service.stub'; +import { MetadataRepresentationListElementComponent } from './metadata-representation-list-element.component'; + +// Mock metadata representation values +const mockMetadataRepresentation = Object.assign(new MetadatumRepresentation('type', mockData[1]), { + key: 'dc.contributor.author', + value: 'Test Author' +}); +const mockMetadataRepresentationUrl = Object.assign(new MetadatumRepresentation('type', mockData[1]), { + key: 'dc.subject', + value: 'https://www.google.com' +}); + +describe('MetadataRepresentationListElementComponent', () => { + let comp: MetadataRepresentationListElementComponent; + let fixture: ComponentFixture; + + beforeEach(waitForAsync(() => { + TestBed.configureTestingModule({ + imports: [], + declarations: [MetadataRepresentationListElementComponent], + schemas: [NO_ERRORS_SCHEMA] + }).overrideComponent(MetadataRepresentationListElementComponent, { + set: { changeDetection: ChangeDetectionStrategy.Default } + }).compileComponents(); + })); + + beforeEach(waitForAsync(() => { + fixture = TestBed.createComponent(MetadataRepresentationListElementComponent); + comp = fixture.componentInstance; + fixture.detectChanges(); + })); + + describe('when the value is not a URL', () => { + beforeEach(() => { + comp.metadataRepresentation = mockMetadataRepresentation; + }); + it('isLink correctly detects a non-URL string as false', () => { + waitForAsync(() => { + expect(comp.isLink()).toBe(false); + }); + }); + }); + + describe('when the value is a URL', () => { + beforeEach(() => { + comp.metadataRepresentation = mockMetadataRepresentationUrl; + }); + it('isLink correctly detects a URL string as true', () => { + waitForAsync(() => { + expect(comp.isLink()).toBe(true); + }); + }); + }); + +}); diff --git a/src/app/shared/object-list/metadata-representation-list-element/metadata-representation-list-element.component.ts b/src/app/shared/object-list/metadata-representation-list-element/metadata-representation-list-element.component.ts index 2e14485fbb..b69f6b37dc 100644 --- a/src/app/shared/object-list/metadata-representation-list-element/metadata-representation-list-element.component.ts +++ b/src/app/shared/object-list/metadata-representation-list-element/metadata-representation-list-element.component.ts @@ -13,4 +13,14 @@ export class MetadataRepresentationListElementComponent { * The metadata representation of this component */ metadataRepresentation: MetadataRepresentation; + + /** + * Returns true if this component's value matches a basic regex "Is this an HTTP URL" test + */ + isLink(): boolean { + // Match any string that begins with http:// or https:// + const linkPattern = new RegExp(/^https?\/\/.*/); + return linkPattern.test(this.metadataRepresentation.getValue()); + } + } diff --git a/src/app/shared/object-list/metadata-representation-list-element/plain-text/plain-text-metadata-list-element.component.html b/src/app/shared/object-list/metadata-representation-list-element/plain-text/plain-text-metadata-list-element.component.html index 31b670b1a3..7b611a7d1f 100644 --- a/src/app/shared/object-list/metadata-representation-list-element/plain-text/plain-text-metadata-list-element.component.html +++ b/src/app/shared/object-list/metadata-representation-list-element/plain-text/plain-text-metadata-list-element.component.html @@ -1,3 +1,17 @@
- {{metadataRepresentation.getValue()}} + + + {{metadataRepresentation.getValue()}} + + + {{metadataRepresentation.getValue()}} + + {{metadataRepresentation.getValue()}} + + {{metadataRepresentation.getValue()}} +
diff --git a/src/app/shared/object-list/metadata-representation-list-element/plain-text/plain-text-metadata-list-element.component.spec.ts b/src/app/shared/object-list/metadata-representation-list-element/plain-text/plain-text-metadata-list-element.component.spec.ts index af09d3c204..cfb812a475 100644 --- a/src/app/shared/object-list/metadata-representation-list-element/plain-text/plain-text-metadata-list-element.component.spec.ts +++ b/src/app/shared/object-list/metadata-representation-list-element/plain-text/plain-text-metadata-list-element.component.spec.ts @@ -2,8 +2,12 @@ import { ComponentFixture, TestBed, waitForAsync } from '@angular/core/testing'; import { ChangeDetectionStrategy, NO_ERRORS_SCHEMA } from '@angular/core'; import { PlainTextMetadataListElementComponent } from './plain-text-metadata-list-element.component'; import { MetadatumRepresentation } from '../../../../core/shared/metadata-representation/metadatum/metadatum-representation.model'; +import { By } from '@angular/platform-browser'; +import { mockData } from '../../../testing/browse-definition-data-service.stub'; -const mockMetadataRepresentation = Object.assign(new MetadatumRepresentation('type'), { +// Render the mock representation with the default mock author browse definition so it is also rendered as a link +// without affecting other tests +const mockMetadataRepresentation = Object.assign(new MetadatumRepresentation('type', mockData[1]), { key: 'dc.contributor.author', value: 'Test Author' }); @@ -33,4 +37,8 @@ describe('PlainTextMetadataListElementComponent', () => { expect(fixture.debugElement.nativeElement.textContent).toContain(mockMetadataRepresentation.value); }); + it('should contain the browse link as plain text', () => { + expect(fixture.debugElement.query(By.css('a.ds-browse-link')).nativeElement.innerHTML).toContain(mockMetadataRepresentation.value); + }); + }); diff --git a/src/app/shared/object-list/metadata-representation-list-element/plain-text/plain-text-metadata-list-element.component.ts b/src/app/shared/object-list/metadata-representation-list-element/plain-text/plain-text-metadata-list-element.component.ts index 198c3712d9..2d21a7afe8 100644 --- a/src/app/shared/object-list/metadata-representation-list-element/plain-text/plain-text-metadata-list-element.component.ts +++ b/src/app/shared/object-list/metadata-representation-list-element/plain-text/plain-text-metadata-list-element.component.ts @@ -15,4 +15,15 @@ import { metadataRepresentationComponent } from '../../../metadata-representatio * It will simply use the value retrieved from MetadataRepresentation.getValue() to display as plain text */ export class PlainTextMetadataListElementComponent extends MetadataRepresentationListElementComponent { + /** + * Get the appropriate query parameters for this browse link, depending on whether the browse definition + * expects 'startsWith' (eg browse by date) or 'value' (eg browse by title) + */ + getQueryParams() { + let queryParams = {startsWith: this.metadataRepresentation.getValue()}; + if (this.metadataRepresentation.browseDefinition.metadataBrowse) { + return {value: this.metadataRepresentation.getValue()}; + } + return queryParams; + } } diff --git a/src/app/shared/shared.module.ts b/src/app/shared/shared.module.ts index dfe8768014..bd46380452 100644 --- a/src/app/shared/shared.module.ts +++ b/src/app/shared/shared.module.ts @@ -84,6 +84,8 @@ import { LangSwitchComponent } from './lang-switch/lang-switch.component'; import { PlainTextMetadataListElementComponent } from './object-list/metadata-representation-list-element/plain-text/plain-text-metadata-list-element.component'; +import { BrowseLinkMetadataListElementComponent } + from './object-list/metadata-representation-list-element/browse-link/browse-link-metadata-list-element.component'; import { ItemMetadataListElementComponent } from './object-list/metadata-representation-list-element/item/item-metadata-list-element.component'; @@ -383,6 +385,7 @@ const ENTRY_COMPONENTS = [ EditItemSelectorComponent, ThemedEditItemSelectorComponent, PlainTextMetadataListElementComponent, + BrowseLinkMetadataListElementComponent, ItemMetadataListElementComponent, MetadataRepresentationListElementComponent, ItemMetadataRepresentationListElementComponent, diff --git a/src/app/shared/testing/browse-definition-data-service.stub.ts b/src/app/shared/testing/browse-definition-data-service.stub.ts new file mode 100644 index 0000000000..ec1fc2f05e --- /dev/null +++ b/src/app/shared/testing/browse-definition-data-service.stub.ts @@ -0,0 +1,63 @@ +import { EMPTY, Observable, of as observableOf } from 'rxjs'; +import { RemoteData } from '../../core/data/remote-data'; +import { buildPaginatedList, PaginatedList } from '../../core/data/paginated-list.model'; +import { BrowseDefinition } from '../../core/shared/browse-definition.model'; +import { BrowseService } from '../../core/browse/browse.service'; +import { createSuccessfulRemoteDataObject } from '../remote-data.utils'; +import { PageInfo } from '../../core/shared/page-info.model'; + +// This data is in post-serialized form (metadata -> metadataKeys) +export const mockData: BrowseDefinition[] = [ + Object.assign(new BrowseDefinition, { + 'id' : 'dateissued', + 'metadataBrowse' : false, + 'dataType' : 'date', + 'sortOptions' : EMPTY, + 'order' : 'ASC', + 'type' : 'browse', + 'metadataKeys' : [ 'dc.date.issued' ], + '_links' : EMPTY + }), + Object.assign(new BrowseDefinition, { + 'id' : 'author', + 'metadataBrowse' : true, + 'dataType' : 'text', + 'sortOptions' : EMPTY, + 'order' : 'ASC', + 'type' : 'browse', + 'metadataKeys' : [ 'dc.contributor.*', 'dc.creator' ], + '_links' : EMPTY + }) +]; + +export const BrowseDefinitionDataServiceStub: any = { + + /** + * Get all BrowseDefinitions + */ + findAll(): Observable>> { + return observableOf(createSuccessfulRemoteDataObject(buildPaginatedList(new PageInfo(), mockData))); + }, + + /** + * Get all BrowseDefinitions with any link configuration + */ + findAllLinked(): Observable>> { + return observableOf(createSuccessfulRemoteDataObject(buildPaginatedList(new PageInfo(), mockData))); + }, + + /** + * Get the browse URL by providing a list of metadata keys + * + * @param metadataKeys a list of fields eg. ['dc.contributor.author', 'dc.creator'] + */ + findByFields(metadataKeys: string[]): Observable> { + let searchKeyArray: string[] = []; + metadataKeys.forEach((metadataKey) => { + searchKeyArray = searchKeyArray.concat(BrowseService.toSearchKeyArray(metadataKey)); + }); + // Return just the first, as a pretend match + return observableOf(createSuccessfulRemoteDataObject(mockData[0])); + } + +};