diff --git a/src/app/core/breadcrumbs/dso-breadcrumbs.service.ts b/src/app/core/breadcrumbs/dso-breadcrumbs.service.ts index 0e6af2291b..eaf1fed759 100644 --- a/src/app/core/breadcrumbs/dso-breadcrumbs.service.ts +++ b/src/app/core/breadcrumbs/dso-breadcrumbs.service.ts @@ -1,11 +1,12 @@ import { Breadcrumb } from '../../breadcrumbs/breadcrumb/breadcrumb.model'; import { BreadcrumbsService } from './breadcrumbs.service'; +import { DSONameService } from './dso-name.service'; import { Observable, of as observableOf } from 'rxjs'; import { ChildHALResource } from '../shared/child-hal-resource.model'; import { LinkService } from '../cache/builders/link.service'; import { DSpaceObject } from '../shared/dspace-object.model'; import { followLink } from '../../shared/utils/follow-link-config.model'; -import { filter, find, map, switchMap } from 'rxjs/operators'; +import { find, map, switchMap } from 'rxjs/operators'; import { getDSOPath } from '../../app-routing.module'; import { RemoteData } from '../data/remote-data'; import { hasValue } from '../../shared/empty.util'; @@ -13,12 +14,16 @@ import { Injectable } from '@angular/core'; @Injectable() export class DSOBreadcrumbsService implements BreadcrumbsService { - constructor(private linkService: LinkService) { + constructor( + private linkService: LinkService, + private dsoNameService: DSONameService + ) { } getBreadcrumbs(key: ChildHALResource & DSpaceObject, url: string): Observable { - const crumb = new Breadcrumb(key.name, url); + const label = this.dsoNameService.getName(key); + const crumb = new Breadcrumb(label, url); const propertyName = key.getParentLinkKey(); return this.linkService.resolveLink(key, followLink(propertyName))[propertyName].pipe( find((childRD: RemoteData) => childRD.hasSucceeded || childRD.statusCode === 204), diff --git a/src/app/core/breadcrumbs/dso-name.service.spec.ts b/src/app/core/breadcrumbs/dso-name.service.spec.ts new file mode 100644 index 0000000000..aa06116ed5 --- /dev/null +++ b/src/app/core/breadcrumbs/dso-name.service.spec.ts @@ -0,0 +1,116 @@ +import { ListableObject } from '../../shared/object-collection/shared/listable-object.model'; +import { DSpaceObject } from '../shared/dspace-object.model'; +import { GenericConstructor } from '../shared/generic-constructor'; +import { Item } from '../shared/item.model'; +import { MetadataValueFilter } from '../shared/metadata.models'; +import { DSONameService } from './dso-name.service'; + +describe(`DSONameService`, () => { + let service: DSONameService; + let mockPersonName: string; + let mockPerson: DSpaceObject; + let mockOrgUnitName: string; + let mockOrgUnit: DSpaceObject; + let mockDSOName: string; + let mockDSO: DSpaceObject; + + beforeEach(() => { + mockPersonName = 'Doe, John'; + mockPerson = Object.assign(new DSpaceObject(), { + firstMetadataValue(keyOrKeys: string | string[], valueFilter?: MetadataValueFilter): string { + return mockPersonName + }, + getRenderTypes(): Array> { + return ['Person', Item, DSpaceObject]; + } + }); + + mockOrgUnitName = 'Molecular Spectroscopy'; + mockOrgUnit = Object.assign(new DSpaceObject(), { + firstMetadataValue(keyOrKeys: string | string[], valueFilter?: MetadataValueFilter): string { + return mockOrgUnitName + }, + getRenderTypes(): Array> { + return ['OrgUnit', Item, DSpaceObject]; + } + }); + + mockDSOName = 'Lorem Ipsum'; + mockDSO = Object.assign(new DSpaceObject(), { + firstMetadataValue(keyOrKeys: string | string[], valueFilter?: MetadataValueFilter): string { + return mockDSOName + }, + getRenderTypes(): Array> { + return [DSpaceObject]; + } + }); + + service = new DSONameService(); + }); + + describe(`getName`, () => { + it(`should use the Person factory for Person entities`, () => { + spyOn((service as any).factories, 'Person').and.returnValue('Bingo!'); + + const result = service.getName(mockPerson); + + expect((service as any).factories.Person).toHaveBeenCalledWith(mockPerson); + expect(result).toBe('Bingo!'); + }); + + it(`should use the OrgUnit factory for OrgUnit entities`, () => { + spyOn((service as any).factories, 'OrgUnit').and.returnValue('Bingo!'); + + const result = service.getName(mockOrgUnit); + + expect((service as any).factories.OrgUnit).toHaveBeenCalledWith(mockOrgUnit); + expect(result).toBe('Bingo!'); + }); + + it(`should use the Default factory for regular DSpaceObjects`, () => { + spyOn((service as any).factories, 'Default').and.returnValue('Bingo!'); + + const result = service.getName(mockDSO); + + expect((service as any).factories.Default).toHaveBeenCalledWith(mockDSO); + expect(result).toBe('Bingo!'); + }); + }); + + describe(`factories.Person`, () => { + beforeEach(() => { + spyOn(mockPerson, 'firstMetadataValue').and.returnValues(...mockPersonName.split(', ')); + }); + + it(`should return 'person.familyName, person.givenName'`, () => { + const result = (service as any).factories.Person(mockPerson); + expect(result).toBe(mockPersonName); + expect(mockPerson.firstMetadataValue).toHaveBeenCalledWith('person.familyName'); + expect(mockPerson.firstMetadataValue).toHaveBeenCalledWith('person.givenName'); + }); + }); + + describe(`factories.OrgUnit`, () => { + beforeEach(() => { + spyOn(mockOrgUnit, 'firstMetadataValue').and.callThrough(); + }); + + it(`should return 'organization.legalName'`, () => { + const result = (service as any).factories.OrgUnit(mockOrgUnit); + expect(result).toBe(mockOrgUnitName); + expect(mockOrgUnit.firstMetadataValue).toHaveBeenCalledWith('organization.legalName'); + }); + }); + + describe(`factories.Default`, () => { + beforeEach(() => { + spyOn(mockDSO, 'firstMetadataValue').and.callThrough(); + }); + + it(`should return 'dc.title'`, () => { + const result = (service as any).factories.Default(mockDSO); + expect(result).toBe(mockDSOName); + expect(mockDSO.firstMetadataValue).toHaveBeenCalledWith('dc.title'); + }); + }); +}); diff --git a/src/app/core/breadcrumbs/dso-name.service.ts b/src/app/core/breadcrumbs/dso-name.service.ts new file mode 100644 index 0000000000..161c4f7254 --- /dev/null +++ b/src/app/core/breadcrumbs/dso-name.service.ts @@ -0,0 +1,53 @@ +import { Injectable } from '@angular/core'; +import { hasValue } from '../../shared/empty.util'; +import { DSpaceObject } from '../shared/dspace-object.model'; + +/** + * Returns a name for a {@link DSpaceObject} based + * on its render types. + */ +@Injectable({ + providedIn: 'root' +}) +export class DSONameService { + + /** + * Functions to generate the specific names. + * + * If this list ever expands it will probably be worth it to + * refactor this using decorators for specific entity types, + * or perhaps by using a dedicated model for each entity type + * + * With only two exceptions those solutions seem overkill for now. + */ + private factories = { + Person: (dso: DSpaceObject): string => { + return `${dso.firstMetadataValue('person.familyName')}, ${dso.firstMetadataValue('person.givenName')}`; + }, + OrgUnit: (dso: DSpaceObject): string => { + return dso.firstMetadataValue('organization.legalName'); + }, + Default: (dso: DSpaceObject): string => { + return dso.firstMetadataValue('dc.title'); + } + }; + + /** + * Get the name for the given {@link DSpaceObject} + * + * @param dso The {@link DSpaceObject} you want a name for + */ + getName(dso: DSpaceObject): string { + const types = dso.getRenderTypes(); + const match = types + .filter((type) => typeof type === 'string') + .find((type: string) => Object.keys(this.factories).includes(type)) as string; + + if (hasValue(match)) { + return this.factories[match](dso); + } else { + return this.factories.Default(dso); + } + } + +} diff --git a/src/app/core/cache/builders/link.service.ts b/src/app/core/cache/builders/link.service.ts index c41a5484a1..e57adaa598 100644 --- a/src/app/core/cache/builders/link.service.ts +++ b/src/app/core/cache/builders/link.service.ts @@ -55,16 +55,20 @@ export class LinkService { parent: this.parentInjector }).get(provider); - const href = model._links[matchingLinkDef.linkName].href; + const link = model._links[matchingLinkDef.linkName]; - try { - if (matchingLinkDef.isList) { - model[linkToFollow.name] = service.findAllByHref(href, linkToFollow.findListOptions, ...linkToFollow.linksToFollow); - } else { - model[linkToFollow.name] = service.findByHref(href, ...linkToFollow.linksToFollow); + if (hasValue(link)) { + const href = link.href; + + try { + if (matchingLinkDef.isList) { + model[linkToFollow.name] = service.findAllByHref(href, linkToFollow.findListOptions, ...linkToFollow.linksToFollow); + } else { + model[linkToFollow.name] = service.findByHref(href, ...linkToFollow.linksToFollow); + } + } catch (e) { + throw new Error(`Something went wrong when using @dataService(${matchingLinkDef.resourceType.value}) ${hasValue(service) ? '' : '(undefined) '}to resolve link ${linkToFollow.name} from ${href}`); } - } catch (e) { - throw new Error(`Something went wrong when using @dataService(${matchingLinkDef.resourceType.value}) ${hasValue(service) ? '' : '(undefined) '}to resolve link ${linkToFollow.name} from ${href}`); } } return model; diff --git a/src/app/core/metadata/metadata.service.ts b/src/app/core/metadata/metadata.service.ts index 1417005b9d..dbba9d83f6 100644 --- a/src/app/core/metadata/metadata.service.ts +++ b/src/app/core/metadata/metadata.service.ts @@ -10,6 +10,7 @@ import { catchError, distinctUntilKeyChanged, filter, first, map, take } from 'r import { GLOBAL_CONFIG, GlobalConfig } from '../../../config'; import { hasValue, isNotEmpty } from '../../shared/empty.util'; +import { DSONameService } from '../breadcrumbs/dso-name.service'; import { CacheableObject } from '../cache/object-cache.reducer'; import { BitstreamDataService } from '../data/bitstream-data.service'; import { BitstreamFormatDataService } from '../data/bitstream-format-data.service'; @@ -35,6 +36,7 @@ export class MetadataService { private translate: TranslateService, private meta: Meta, private title: Title, + private dsoNameService: DSONameService, private bitstreamDataService: BitstreamDataService, private bitstreamFormatDataService: BitstreamFormatDataService, @Inject(GLOBAL_CONFIG) private envConfig: GlobalConfig @@ -154,7 +156,7 @@ export class MetadataService { * Add to the */ private setTitleTag(): void { - const value = this.getMetaTagValue('dc.title'); + const value = this.dsoNameService.getName(this.currentObject.getValue()); this.addMetaTag('title', value); this.title.setTitle(value); } diff --git a/src/app/core/shared/dspace-object.model.ts b/src/app/core/shared/dspace-object.model.ts index 2e1afe9c8a..60a1160d3e 100644 --- a/src/app/core/shared/dspace-object.model.ts +++ b/src/app/core/shared/dspace-object.model.ts @@ -69,6 +69,7 @@ export class DSpaceObject extends ListableObject implements CacheableObject { /** * The name for this DSpaceObject + * @deprecated use {@link DSONameService} instead */ get name(): string { return (isUndefined(this._name)) ? this.firstMetadataValue('dc.title') : this._name;