+
+ +
+
diff --git a/src/app/app.module.ts b/src/app/app.module.ts index 7bc4ee1c5a..be528ccf3f 100755 --- a/src/app/app.module.ts +++ b/src/app/app.module.ts @@ -39,6 +39,7 @@ import { DSpaceRouterStateSerializer } from './shared/ngrx/dspace-router-state-s import { NotificationComponent } from './shared/notifications/notification/notification.component'; import { NotificationsBoardComponent } from './shared/notifications/notifications-board/notifications-board.component'; import { SharedModule } from './shared/shared.module'; +import { BreadcrumbsComponent } from './breadcrumbs/breadcrumbs.component'; export function getConfig() { return ENV_CONFIG; @@ -128,6 +129,7 @@ const EXPORTS = [ ], declarations: [ ...DECLARATIONS, + BreadcrumbsComponent, ], exports: [ ...EXPORTS diff --git a/src/app/breadcrumbs/breadcrumb/breadcrumb.model.ts b/src/app/breadcrumbs/breadcrumb/breadcrumb.model.ts new file mode 100644 index 0000000000..99cf66533a --- /dev/null +++ b/src/app/breadcrumbs/breadcrumb/breadcrumb.model.ts @@ -0,0 +1,6 @@ +export class Breadcrumb { + constructor( + public text: string, + public url?: string) { + } +} diff --git a/src/app/breadcrumbs/breadcrumbs.component.html b/src/app/breadcrumbs/breadcrumbs.component.html new file mode 100644 index 0000000000..3a8730ea30 --- /dev/null +++ b/src/app/breadcrumbs/breadcrumbs.component.html @@ -0,0 +1,16 @@ + + + + + + + + + diff --git a/src/app/breadcrumbs/breadcrumbs.component.scss b/src/app/breadcrumbs/breadcrumbs.component.scss new file mode 100644 index 0000000000..e69de29bb2 diff --git a/src/app/breadcrumbs/breadcrumbs.component.spec.ts b/src/app/breadcrumbs/breadcrumbs.component.spec.ts new file mode 100644 index 0000000000..175ef83757 --- /dev/null +++ b/src/app/breadcrumbs/breadcrumbs.component.spec.ts @@ -0,0 +1,25 @@ +import { async, ComponentFixture, TestBed } from '@angular/core/testing'; + +import { BreadcrumbsComponent } from './breadcrumbs.component'; + +describe('BreadcrumbsComponent', () => { + let component: BreadcrumbsComponent; + let fixture: ComponentFixture; + + beforeEach(async(() => { + TestBed.configureTestingModule({ + declarations: [ BreadcrumbsComponent ] + }) + .compileComponents(); + })); + + beforeEach(() => { + fixture = TestBed.createComponent(BreadcrumbsComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); +}); diff --git a/src/app/breadcrumbs/breadcrumbs.component.ts b/src/app/breadcrumbs/breadcrumbs.component.ts new file mode 100644 index 0000000000..5fd0f93787 --- /dev/null +++ b/src/app/breadcrumbs/breadcrumbs.component.ts @@ -0,0 +1,53 @@ +import { Component, OnDestroy, OnInit } from '@angular/core'; +import { ActivatedRoute, NavigationEnd, Router } from '@angular/router'; +import { Breadcrumb } from './breadcrumb/breadcrumb.model'; +import { hasValue, isNotUndefined } from '../shared/empty.util'; +import { filter, map } from 'rxjs/operators'; +import { Subscription } from 'rxjs'; + +@Component({ + selector: 'ds-breadcrumbs', + templateUrl: './breadcrumbs.component.html', + styleUrls: ['./breadcrumbs.component.scss'] +}) +export class BreadcrumbsComponent implements OnDestroy { + breadcrumbs; + showBreadcrumbs; + subscription: Subscription; + + constructor( + private route: ActivatedRoute, + private router: Router + ) { + this.subscription = this.router.events.pipe( + filter((e): e is NavigationEnd => e instanceof NavigationEnd) + ).subscribe(() => { + this.reset(); + this.resolveBreadcrumb(this.route.root); + } + ) + } + + resolveBreadcrumb(route: ActivatedRoute) { + const data = route.snapshot.data; + if (hasValue(data) && hasValue(data.breadcrumb)) { + this.breadcrumbs.push(data.breadcrumb); + } + if (route.children.length > 0) { + this.resolveBreadcrumb(route.firstChild); + } else if (isNotUndefined(data.showBreadcrumbs)) { + this.showBreadcrumbs = data.showBreadcrumbs; + } + } + + ngOnDestroy(): void { + if (hasValue(this.subscription)) { + this.subscription.unsubscribe(); + } + } + + reset() { + this.breadcrumbs = []; + this.showBreadcrumbs = true; + } +} From 4ea264dd7f73d3b6c96eda942dde38d766d8d029 Mon Sep 17 00:00:00 2001 From: lotte Date: Thu, 20 Feb 2020 16:07:20 +0100 Subject: [PATCH 02/12] dso + i18n breadcrumbs with providers + resolvers --- .../breadcrumb/breadcrumb-config.model.ts | 7 ++++ src/app/breadcrumbs/breadcrumbs.component.ts | 36 ++++++++++++------- .../core/breadcrumbs/breadcrumbs.service.ts | 6 ++++ .../breadcrumbs/dso-breadcrumb.resolver.ts | 26 ++++++++++++++ .../breadcrumbs/dso-breadcrumbs.service.ts | 9 +++++ .../breadcrumbs/i18n-breadcrumb.resolver.ts | 29 +++++++++++++++ .../breadcrumbs/i18n-breadcrumbs.service.ts | 11 ++++++ 7 files changed, 111 insertions(+), 13 deletions(-) create mode 100644 src/app/breadcrumbs/breadcrumb/breadcrumb-config.model.ts create mode 100644 src/app/core/breadcrumbs/breadcrumbs.service.ts create mode 100644 src/app/core/breadcrumbs/dso-breadcrumb.resolver.ts create mode 100644 src/app/core/breadcrumbs/dso-breadcrumbs.service.ts create mode 100644 src/app/core/breadcrumbs/i18n-breadcrumb.resolver.ts create mode 100644 src/app/core/breadcrumbs/i18n-breadcrumbs.service.ts diff --git a/src/app/breadcrumbs/breadcrumb/breadcrumb-config.model.ts b/src/app/breadcrumbs/breadcrumb/breadcrumb-config.model.ts new file mode 100644 index 0000000000..cb43415d70 --- /dev/null +++ b/src/app/breadcrumbs/breadcrumb/breadcrumb-config.model.ts @@ -0,0 +1,7 @@ +import { BreadcrumbsService } from '../../core/breadcrumbs/breadcrumbs.service'; + +export interface BreadcrumbConfig { + provider: BreadcrumbsService; + key: string; + url?: string; +} diff --git a/src/app/breadcrumbs/breadcrumbs.component.ts b/src/app/breadcrumbs/breadcrumbs.component.ts index 5fd0f93787..c8ee9f5a1b 100644 --- a/src/app/breadcrumbs/breadcrumbs.component.ts +++ b/src/app/breadcrumbs/breadcrumbs.component.ts @@ -2,8 +2,9 @@ import { Component, OnDestroy, OnInit } from '@angular/core'; import { ActivatedRoute, NavigationEnd, Router } from '@angular/router'; import { Breadcrumb } from './breadcrumb/breadcrumb.model'; import { hasValue, isNotUndefined } from '../shared/empty.util'; -import { filter, map } from 'rxjs/operators'; -import { Subscription } from 'rxjs'; +import { filter, map, switchMap, tap } from 'rxjs/operators'; +import { combineLatest, Observable, Subscription } from 'rxjs'; +import { BreadcrumbConfig } from './breadcrumb/breadcrumb-config.model'; @Component({ selector: 'ds-breadcrumbs', @@ -11,8 +12,8 @@ import { Subscription } from 'rxjs'; styleUrls: ['./breadcrumbs.component.scss'] }) export class BreadcrumbsComponent implements OnDestroy { - breadcrumbs; - showBreadcrumbs; + breadcrumbs: Breadcrumb[]; + showBreadcrumbs: boolean; subscription: Subscription; constructor( @@ -20,23 +21,32 @@ export class BreadcrumbsComponent implements OnDestroy { private router: Router ) { this.subscription = this.router.events.pipe( - filter((e): e is NavigationEnd => e instanceof NavigationEnd) - ).subscribe(() => { - this.reset(); - this.resolveBreadcrumb(this.route.root); + filter((e): e is NavigationEnd => e instanceof NavigationEnd), + tap(() => this.reset()), + switchMap(() => this.resolveBreadcrumb(this.route.root)) + ).subscribe((breadcrumbs) => { + this.breadcrumbs = breadcrumbs; } ) } - resolveBreadcrumb(route: ActivatedRoute) { + resolveBreadcrumb(route: ActivatedRoute): Observable { const data = route.snapshot.data; if (hasValue(data) && hasValue(data.breadcrumb)) { - this.breadcrumbs.push(data.breadcrumb); + const { provider, key, url }: BreadcrumbConfig = data.breadcrumb; + if (route.children.length > 0) { + return combineLatest(provider.getBreadcrumbs(key, url), this.resolveBreadcrumb(route.firstChild)) + .pipe(map((crumbs) => [].concat.apply([], crumbs))); + } else { + if (isNotUndefined(data.showBreadcrumbs)) { + this.showBreadcrumbs = data.showBreadcrumbs; + } + return provider.getBreadcrumbs(key, url); + } } if (route.children.length > 0) { - this.resolveBreadcrumb(route.firstChild); - } else if (isNotUndefined(data.showBreadcrumbs)) { - this.showBreadcrumbs = data.showBreadcrumbs; + + return this.resolveBreadcrumb(route.firstChild) } } diff --git a/src/app/core/breadcrumbs/breadcrumbs.service.ts b/src/app/core/breadcrumbs/breadcrumbs.service.ts new file mode 100644 index 0000000000..8ad2ed0334 --- /dev/null +++ b/src/app/core/breadcrumbs/breadcrumbs.service.ts @@ -0,0 +1,6 @@ +import { Breadcrumb } from '../../breadcrumbs/breadcrumb/breadcrumb.model'; +import { Observable } from 'rxjs'; + +export interface BreadcrumbsService { + getBreadcrumbs(key: string, url: string): Observable; +} diff --git a/src/app/core/breadcrumbs/dso-breadcrumb.resolver.ts b/src/app/core/breadcrumbs/dso-breadcrumb.resolver.ts new file mode 100644 index 0000000000..4d8afcf1cc --- /dev/null +++ b/src/app/core/breadcrumbs/dso-breadcrumb.resolver.ts @@ -0,0 +1,26 @@ +import { BreadcrumbConfig } from '../../breadcrumbs/breadcrumb/breadcrumb-config.model'; +import { Injectable } from '@angular/core'; +import { ActivatedRouteSnapshot, Resolve, RouterStateSnapshot } from '@angular/router'; +import { DSOBreadcrumbsService } from './dso-breadcrumbs.service'; + +/** + * The class that resolve the BreadcrumbConfig object for a route + */ +@Injectable() +export class DSOBreadcrumbResolver implements Resolve { + constructor(private breadcrumbService: DSOBreadcrumbsService) { + } + + /** + * Method for resolving a site object + * @param {ActivatedRouteSnapshot} route The current ActivatedRouteSnapshot + * @param {RouterStateSnapshot} state The current RouterStateSnapshot + * @returns BreadcrumbConfig object + */ + resolve(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): BreadcrumbConfig { + const uuid = route.params.id; + const fullPath = route.url.join(''); + const url = fullPath.substr(0, fullPath.indexOf(uuid)) + uuid; + return { provider: this.breadcrumbService, key: uuid, url: url }; + } +} diff --git a/src/app/core/breadcrumbs/dso-breadcrumbs.service.ts b/src/app/core/breadcrumbs/dso-breadcrumbs.service.ts new file mode 100644 index 0000000000..452c6c5678 --- /dev/null +++ b/src/app/core/breadcrumbs/dso-breadcrumbs.service.ts @@ -0,0 +1,9 @@ +import { Breadcrumb } from '../../breadcrumbs/breadcrumb/breadcrumb.model'; +import { BreadcrumbsService } from './breadcrumbs.service'; +import { Observable } from 'rxjs'; + +export class DSOBreadcrumbsService implements BreadcrumbsService { + getBreadcrumbs(key: string, url: string): Observable { + return undefined; + } +} diff --git a/src/app/core/breadcrumbs/i18n-breadcrumb.resolver.ts b/src/app/core/breadcrumbs/i18n-breadcrumb.resolver.ts new file mode 100644 index 0000000000..6b5d344bc6 --- /dev/null +++ b/src/app/core/breadcrumbs/i18n-breadcrumb.resolver.ts @@ -0,0 +1,29 @@ +import { BreadcrumbConfig } from '../../breadcrumbs/breadcrumb/breadcrumb-config.model'; +import { Injectable } from '@angular/core'; +import { ActivatedRouteSnapshot, Resolve, RouterStateSnapshot } from '@angular/router'; +import { I18nBreadcrumbsService } from './i18n-breadcrumbs.service'; +import { hasNoValue } from '../../shared/empty.util'; + +/** + * The class that resolve the BreadcrumbConfig object for a route + */ +@Injectable() +export class I18nBreadcrumbResolver implements Resolve { + constructor(private breadcrumbService: I18nBreadcrumbsService) { + } + + /** + * Method for resolving a site object + * @param {ActivatedRouteSnapshot} route The current ActivatedRouteSnapshot + * @param {RouterStateSnapshot} state The current RouterStateSnapshot + * @returns BreadcrumbConfig object + */ + resolve(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): BreadcrumbConfig { + const key = route.data.breadcrumbKey; + if (hasNoValue(key)) { + throw new Error('You provided an i18nBreadcrumbResolver for url \"' + route.url + '\" but no breadcrumbKey in the route\'s data') + } + const fullPath = route.url.join(''); + return { provider: this.breadcrumbService, key: key, url: fullPath }; + } +} diff --git a/src/app/core/breadcrumbs/i18n-breadcrumbs.service.ts b/src/app/core/breadcrumbs/i18n-breadcrumbs.service.ts new file mode 100644 index 0000000000..8aefa04802 --- /dev/null +++ b/src/app/core/breadcrumbs/i18n-breadcrumbs.service.ts @@ -0,0 +1,11 @@ +import { Breadcrumb } from '../../breadcrumbs/breadcrumb/breadcrumb.model'; +import { BreadcrumbsService } from './breadcrumbs.service'; +import { Observable, of as observableOf } from 'rxjs'; + +export const BREADCRUMB_MESSAGE_PREFIX = 'breadcrumbs.'; + +export class I18nBreadcrumbsService implements BreadcrumbsService { + getBreadcrumbs(key: string, url: string): Observable { + return observableOf([new Breadcrumb(BREADCRUMB_MESSAGE_PREFIX + key, url)]); + } +} From 725f20a9d0b2182fd0c5655f1c0599fcf835216b Mon Sep 17 00:00:00 2001 From: lotte Date: Fri, 21 Feb 2020 17:06:50 +0100 Subject: [PATCH 03/12] breadcrumbs for DSOs --- .../+item-page/item-page-routing.module.ts | 12 +++++-- .../search-page-routing.module.ts | 9 +++-- src/app/app-routing.module.ts | 20 ++++++++++- .../breadcrumb/breadcrumb-config.model.ts | 6 ++-- src/app/breadcrumbs/breadcrumbs.component.ts | 20 +++++------ .../core/breadcrumbs/breadcrumbs.service.ts | 4 +-- .../collection-breadcrumb.resolver.ts | 17 +++++++++ .../community-breadcrumb.resolver.ts | 15 ++++++++ .../breadcrumbs/dso-breadcrumb.resolver.ts | 25 +++++++++---- .../breadcrumbs/dso-breadcrumbs.service.ts | 35 ++++++++++++++++--- .../breadcrumbs/i18n-breadcrumb.resolver.ts | 4 +-- .../breadcrumbs/i18n-breadcrumbs.service.ts | 4 ++- .../breadcrumbs/item-breadcrumb.resolver.ts | 15 ++++++++ src/app/core/data/remote-data.ts | 6 ++-- .../core/shared/child-hal-resource.model.ts | 5 +++ src/app/core/shared/collection.model.ts | 17 ++++++++- src/app/core/shared/community.model.ts | 15 +++++++- src/app/core/shared/item.model.ts | 7 +++- 18 files changed, 197 insertions(+), 39 deletions(-) create mode 100644 src/app/core/breadcrumbs/collection-breadcrumb.resolver.ts create mode 100644 src/app/core/breadcrumbs/community-breadcrumb.resolver.ts create mode 100644 src/app/core/breadcrumbs/item-breadcrumb.resolver.ts create mode 100644 src/app/core/shared/child-hal-resource.model.ts diff --git a/src/app/+item-page/item-page-routing.module.ts b/src/app/+item-page/item-page-routing.module.ts index ec562842aa..d2ad39d70d 100644 --- a/src/app/+item-page/item-page-routing.module.ts +++ b/src/app/+item-page/item-page-routing.module.ts @@ -7,6 +7,9 @@ import { ItemPageResolver } from './item-page.resolver'; import { URLCombiner } from '../core/url-combiner/url-combiner'; import { getItemModulePath } from '../app-routing.module'; import { AuthenticatedGuard } from '../core/auth/authenticated.guard'; +import { ItemBreadcrumbResolver } from '../core/breadcrumbs/item-breadcrumb.resolver'; +import { DSOBreadcrumbsService } from '../core/breadcrumbs/dso-breadcrumbs.service'; +import { LinkService } from '../core/cache/builders/link.service'; export function getItemPageRoute(itemId: string) { return new URLCombiner(getItemModulePath(), itemId).toString(); @@ -25,14 +28,16 @@ const ITEM_EDIT_PATH = ':id/edit'; component: ItemPageComponent, pathMatch: 'full', resolve: { - item: ItemPageResolver + item: ItemPageResolver, + breadcrumb: ItemBreadcrumbResolver } }, { path: ':id/full', component: FullItemPageComponent, resolve: { - item: ItemPageResolver + item: ItemPageResolver, + breadcrumb: ItemBreadcrumbResolver } }, { @@ -44,6 +49,9 @@ const ITEM_EDIT_PATH = ':id/edit'; ], providers: [ ItemPageResolver, + ItemBreadcrumbResolver, + DSOBreadcrumbsService, + LinkService ] }) export class ItemPageRoutingModule { diff --git a/src/app/+search-page/search-page-routing.module.ts b/src/app/+search-page/search-page-routing.module.ts index e7de31c06c..463aa8e7c4 100644 --- a/src/app/+search-page/search-page-routing.module.ts +++ b/src/app/+search-page/search-page-routing.module.ts @@ -4,14 +4,19 @@ import { RouterModule } from '@angular/router'; import { ConfigurationSearchPageGuard } from './configuration-search-page.guard'; import { ConfigurationSearchPageComponent } from './configuration-search-page.component'; import { SearchPageComponent } from './search-page.component'; -import { Breadcrumb } from '../breadcrumbs/breadcrumb/breadcrumb.model'; +import { I18nBreadcrumbResolver } from '../core/breadcrumbs/i18n-breadcrumb.resolver'; +import { I18nBreadcrumbsService } from '../core/breadcrumbs/i18n-breadcrumbs.service'; @NgModule({ imports: [ RouterModule.forChild([ - { path: '', component: SearchPageComponent, data: { title: 'search.title', breadcrumb: new Breadcrumb('Search', '/search') } }, + { path: '', component: SearchPageComponent, resolve: { breadcrumb: I18nBreadcrumbResolver }, data: { title: 'search.title', breadcrumbKey: 'Search' } }, { path: ':configuration', component: ConfigurationSearchPageComponent, canActivate: [ConfigurationSearchPageGuard] } ]) + ], + providers: [ + I18nBreadcrumbResolver, + I18nBreadcrumbsService ] }) export class SearchPageRoutingModule { diff --git a/src/app/app-routing.module.ts b/src/app/app-routing.module.ts index 471609745d..ede1879894 100644 --- a/src/app/app-routing.module.ts +++ b/src/app/app-routing.module.ts @@ -4,6 +4,13 @@ import { RouterModule } from '@angular/router'; import { PageNotFoundComponent } from './pagenotfound/pagenotfound.component'; import { AuthenticatedGuard } from './core/auth/authenticated.guard'; import { Breadcrumb } from './breadcrumbs/breadcrumb/breadcrumb.model'; +import { DSpaceObject } from './core/shared/dspace-object.model'; +import { Community } from './core/shared/community.model'; +import { getCommunityPageRoute } from './+community-page/community-page-routing.module'; +import { Collection } from './core/shared/collection.model'; +import { Item } from './core/shared/item.model'; +import { getItemPageRoute } from './+item-page/item-page-routing.module'; +import { getCollectionPageRoute } from './+collection-page/collection-page-routing.module'; const ITEM_MODULE_PATH = 'items'; @@ -29,6 +36,17 @@ export function getAdminModulePath() { return `/${ADMIN_MODULE_PATH}`; } +export function getDSOPath(dso: DSpaceObject): string { + switch ((dso as any).type) { + case Community.type.value: + return getCommunityPageRoute(dso.uuid); + case Collection.type.value: + return getCollectionPageRoute(dso.uuid); + case Item.type.value: + return getItemPageRoute(dso.uuid); + } +} + @NgModule({ imports: [ RouterModule.forRoot([ @@ -41,7 +59,7 @@ export function getAdminModulePath() { { path: COLLECTION_MODULE_PATH, loadChildren: './+collection-page/collection-page.module#CollectionPageModule' }, { path: ITEM_MODULE_PATH, loadChildren: './+item-page/item-page.module#ItemPageModule' }, { path: 'mydspace', loadChildren: './+my-dspace-page/my-dspace-page.module#MyDSpacePageModule', canActivate: [AuthenticatedGuard] }, - { path: 'search', loadChildren: './+search-page/search-page.module#SearchPageModule', data: { breadcrumb: new Breadcrumb('Search', '/search') } }, + { path: 'search', loadChildren: './+search-page/search-page.module#SearchPageModule' }, { path: 'browse', loadChildren: './+browse-by/browse-by.module#BrowseByModule' }, { path: ADMIN_MODULE_PATH, loadChildren: './+admin/admin.module#AdminModule', canActivate: [AuthenticatedGuard] }, { path: 'login', loadChildren: './+login-page/login-page.module#LoginPageModule' }, diff --git a/src/app/breadcrumbs/breadcrumb/breadcrumb-config.model.ts b/src/app/breadcrumbs/breadcrumb/breadcrumb-config.model.ts index cb43415d70..17ec96e2bd 100644 --- a/src/app/breadcrumbs/breadcrumb/breadcrumb-config.model.ts +++ b/src/app/breadcrumbs/breadcrumb/breadcrumb-config.model.ts @@ -1,7 +1,7 @@ import { BreadcrumbsService } from '../../core/breadcrumbs/breadcrumbs.service'; -export interface BreadcrumbConfig { - provider: BreadcrumbsService; - key: string; +export interface BreadcrumbConfig { + provider: BreadcrumbsService; + key: T; url?: string; } diff --git a/src/app/breadcrumbs/breadcrumbs.component.ts b/src/app/breadcrumbs/breadcrumbs.component.ts index c8ee9f5a1b..944af9b55d 100644 --- a/src/app/breadcrumbs/breadcrumbs.component.ts +++ b/src/app/breadcrumbs/breadcrumbs.component.ts @@ -3,7 +3,7 @@ import { ActivatedRoute, NavigationEnd, Router } from '@angular/router'; import { Breadcrumb } from './breadcrumb/breadcrumb.model'; import { hasValue, isNotUndefined } from '../shared/empty.util'; import { filter, map, switchMap, tap } from 'rxjs/operators'; -import { combineLatest, Observable, Subscription } from 'rxjs'; +import { combineLatest, Observable, Subscription, of as observableOf } from 'rxjs'; import { BreadcrumbConfig } from './breadcrumb/breadcrumb-config.model'; @Component({ @@ -32,22 +32,22 @@ export class BreadcrumbsComponent implements OnDestroy { resolveBreadcrumb(route: ActivatedRoute): Observable { const data = route.snapshot.data; + const last: boolean = route.children.length === 0; + + if (last && isNotUndefined(data.showBreadcrumbs)) { + this.showBreadcrumbs = data.showBreadcrumbs; + } + if (hasValue(data) && hasValue(data.breadcrumb)) { - const { provider, key, url }: BreadcrumbConfig = data.breadcrumb; - if (route.children.length > 0) { + const { provider, key, url } = data.breadcrumb; + if (!last) { return combineLatest(provider.getBreadcrumbs(key, url), this.resolveBreadcrumb(route.firstChild)) .pipe(map((crumbs) => [].concat.apply([], crumbs))); } else { - if (isNotUndefined(data.showBreadcrumbs)) { - this.showBreadcrumbs = data.showBreadcrumbs; - } return provider.getBreadcrumbs(key, url); } } - if (route.children.length > 0) { - - return this.resolveBreadcrumb(route.firstChild) - } + return !last ? this.resolveBreadcrumb(route.firstChild) : observableOf([]); } ngOnDestroy(): void { diff --git a/src/app/core/breadcrumbs/breadcrumbs.service.ts b/src/app/core/breadcrumbs/breadcrumbs.service.ts index 8ad2ed0334..30c6c44cf7 100644 --- a/src/app/core/breadcrumbs/breadcrumbs.service.ts +++ b/src/app/core/breadcrumbs/breadcrumbs.service.ts @@ -1,6 +1,6 @@ import { Breadcrumb } from '../../breadcrumbs/breadcrumb/breadcrumb.model'; import { Observable } from 'rxjs'; -export interface BreadcrumbsService { - getBreadcrumbs(key: string, url: string): Observable; +export interface BreadcrumbsService { + getBreadcrumbs(key: T, url: string): Observable; } diff --git a/src/app/core/breadcrumbs/collection-breadcrumb.resolver.ts b/src/app/core/breadcrumbs/collection-breadcrumb.resolver.ts new file mode 100644 index 0000000000..ec48e89421 --- /dev/null +++ b/src/app/core/breadcrumbs/collection-breadcrumb.resolver.ts @@ -0,0 +1,17 @@ +import { Injectable } from '@angular/core'; +import { DSOBreadcrumbsService } from './dso-breadcrumbs.service'; +import { ItemDataService } from '../data/item-data.service'; +import { Item } from '../shared/item.model'; +import { DSOBreadcrumbResolver } from './dso-breadcrumb.resolver'; +import { Collection } from '../shared/collection.model'; +import { CollectionDataService } from '../data/collection-data.service'; + +/** + * The class that resolve the BreadcrumbConfig object for a route + */ +@Injectable() +export class CollectionBreadcrumbResolver extends DSOBreadcrumbResolver { + constructor(protected breadcrumbService: DSOBreadcrumbsService, protected dataService: CollectionDataService) { + super(breadcrumbService, dataService); + } +} diff --git a/src/app/core/breadcrumbs/community-breadcrumb.resolver.ts b/src/app/core/breadcrumbs/community-breadcrumb.resolver.ts new file mode 100644 index 0000000000..3080823b97 --- /dev/null +++ b/src/app/core/breadcrumbs/community-breadcrumb.resolver.ts @@ -0,0 +1,15 @@ +import { Injectable } from '@angular/core'; +import { DSOBreadcrumbsService } from './dso-breadcrumbs.service'; +import { DSOBreadcrumbResolver } from './dso-breadcrumb.resolver'; +import { CommunityDataService } from '../data/community-data.service'; +import { Community } from '../shared/community.model'; + +/** + * The class that resolve the BreadcrumbConfig object for a route + */ +@Injectable() +export class CommunityBreadcrumbResolver extends DSOBreadcrumbResolver { + constructor(protected breadcrumbService: DSOBreadcrumbsService, protected dataService: CommunityDataService) { + super(breadcrumbService, dataService); + } +} diff --git a/src/app/core/breadcrumbs/dso-breadcrumb.resolver.ts b/src/app/core/breadcrumbs/dso-breadcrumb.resolver.ts index 4d8afcf1cc..6e90d8d123 100644 --- a/src/app/core/breadcrumbs/dso-breadcrumb.resolver.ts +++ b/src/app/core/breadcrumbs/dso-breadcrumb.resolver.ts @@ -2,13 +2,19 @@ import { BreadcrumbConfig } from '../../breadcrumbs/breadcrumb/breadcrumb-config import { Injectable } from '@angular/core'; import { ActivatedRouteSnapshot, Resolve, RouterStateSnapshot } from '@angular/router'; import { DSOBreadcrumbsService } from './dso-breadcrumbs.service'; +import { DataService } from '../data/data.service'; +import { getRemoteDataPayload, getSucceededRemoteData } from '../shared/operators'; +import { map } from 'rxjs/operators'; +import { Observable } from 'rxjs'; +import { DSpaceObject } from '../shared/dspace-object.model'; +import { ChildHALResource } from '../shared/child-hal-resource.model'; /** * The class that resolve the BreadcrumbConfig object for a route */ @Injectable() -export class DSOBreadcrumbResolver implements Resolve { - constructor(private breadcrumbService: DSOBreadcrumbsService) { +export class DSOBreadcrumbResolver implements Resolve> { + constructor(protected breadcrumbService: DSOBreadcrumbsService, protected dataService: DataService) { } /** @@ -17,10 +23,17 @@ export class DSOBreadcrumbResolver implements Resolve { * @param {RouterStateSnapshot} state The current RouterStateSnapshot * @returns BreadcrumbConfig object */ - resolve(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): BreadcrumbConfig { + resolve(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): Observable> { const uuid = route.params.id; - const fullPath = route.url.join(''); - const url = fullPath.substr(0, fullPath.indexOf(uuid)) + uuid; - return { provider: this.breadcrumbService, key: uuid, url: url }; + return this.dataService.findById(uuid).pipe( + getSucceededRemoteData(), + getRemoteDataPayload(), + map((object: T) => { + const fullPath = route.url.join(''); + const url = fullPath.substr(0, fullPath.indexOf(uuid)) + uuid; + return { provider: this.breadcrumbService, key: object, url: url }; + }) + ); + } } diff --git a/src/app/core/breadcrumbs/dso-breadcrumbs.service.ts b/src/app/core/breadcrumbs/dso-breadcrumbs.service.ts index 452c6c5678..30344a3a77 100644 --- a/src/app/core/breadcrumbs/dso-breadcrumbs.service.ts +++ b/src/app/core/breadcrumbs/dso-breadcrumbs.service.ts @@ -1,9 +1,36 @@ import { Breadcrumb } from '../../breadcrumbs/breadcrumb/breadcrumb.model'; import { BreadcrumbsService } from './breadcrumbs.service'; -import { Observable } from 'rxjs'; +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 { getDSOPath } from '../../app-routing.module'; +import { RemoteData } from '../data/remote-data'; +import { hasValue } from '../../shared/empty.util'; +import { Injectable } from '@angular/core'; -export class DSOBreadcrumbsService implements BreadcrumbsService { - getBreadcrumbs(key: string, url: string): Observable { - return undefined; +@Injectable() +export class DSOBreadcrumbsService implements BreadcrumbsService { + constructor(private linkService: LinkService) { + + } + + getBreadcrumbs(key: ChildHALResource & DSpaceObject, url: string): Observable { + const crumb = new Breadcrumb(key.name, url); + const propertyName = key.getParentLinkKey(); + return this.linkService.resolveLink(key, followLink(propertyName))[propertyName].pipe( + filter((childRD: RemoteData) => childRD.isSuccessful === true && childRD.requestPending === false && childRD.responsePending === false), + switchMap((childRD: RemoteData) => { + if (hasValue(childRD.payload)) { + const child = childRD.payload; + return this.getBreadcrumbs(child, getDSOPath(child)) + } + return observableOf([]); + + }), + map((breadcrumbs: Breadcrumb[]) => [...breadcrumbs, crumb]) + ); } } diff --git a/src/app/core/breadcrumbs/i18n-breadcrumb.resolver.ts b/src/app/core/breadcrumbs/i18n-breadcrumb.resolver.ts index 6b5d344bc6..800a7b75d0 100644 --- a/src/app/core/breadcrumbs/i18n-breadcrumb.resolver.ts +++ b/src/app/core/breadcrumbs/i18n-breadcrumb.resolver.ts @@ -8,7 +8,7 @@ import { hasNoValue } from '../../shared/empty.util'; * The class that resolve the BreadcrumbConfig object for a route */ @Injectable() -export class I18nBreadcrumbResolver implements Resolve { +export class I18nBreadcrumbResolver implements Resolve> { constructor(private breadcrumbService: I18nBreadcrumbsService) { } @@ -18,7 +18,7 @@ export class I18nBreadcrumbResolver implements Resolve { * @param {RouterStateSnapshot} state The current RouterStateSnapshot * @returns BreadcrumbConfig object */ - resolve(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): BreadcrumbConfig { + resolve(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): BreadcrumbConfig { const key = route.data.breadcrumbKey; if (hasNoValue(key)) { throw new Error('You provided an i18nBreadcrumbResolver for url \"' + route.url + '\" but no breadcrumbKey in the route\'s data') diff --git a/src/app/core/breadcrumbs/i18n-breadcrumbs.service.ts b/src/app/core/breadcrumbs/i18n-breadcrumbs.service.ts index 8aefa04802..6a9c8916a2 100644 --- a/src/app/core/breadcrumbs/i18n-breadcrumbs.service.ts +++ b/src/app/core/breadcrumbs/i18n-breadcrumbs.service.ts @@ -1,10 +1,12 @@ import { Breadcrumb } from '../../breadcrumbs/breadcrumb/breadcrumb.model'; import { BreadcrumbsService } from './breadcrumbs.service'; import { Observable, of as observableOf } from 'rxjs'; +import { Injectable } from '@angular/core'; export const BREADCRUMB_MESSAGE_PREFIX = 'breadcrumbs.'; -export class I18nBreadcrumbsService implements BreadcrumbsService { +@Injectable() +export class I18nBreadcrumbsService implements BreadcrumbsService { getBreadcrumbs(key: string, url: string): Observable { return observableOf([new Breadcrumb(BREADCRUMB_MESSAGE_PREFIX + key, url)]); } diff --git a/src/app/core/breadcrumbs/item-breadcrumb.resolver.ts b/src/app/core/breadcrumbs/item-breadcrumb.resolver.ts new file mode 100644 index 0000000000..fdfd3c11f9 --- /dev/null +++ b/src/app/core/breadcrumbs/item-breadcrumb.resolver.ts @@ -0,0 +1,15 @@ +import { Injectable } from '@angular/core'; +import { DSOBreadcrumbsService } from './dso-breadcrumbs.service'; +import { ItemDataService } from '../data/item-data.service'; +import { Item } from '../shared/item.model'; +import { DSOBreadcrumbResolver } from './dso-breadcrumb.resolver'; + +/** + * The class that resolve the BreadcrumbConfig object for a route + */ +@Injectable() +export class ItemBreadcrumbResolver extends DSOBreadcrumbResolver { + constructor(protected breadcrumbService: DSOBreadcrumbsService, protected dataService: ItemDataService) { + super(breadcrumbService, dataService); + } +} diff --git a/src/app/core/data/remote-data.ts b/src/app/core/data/remote-data.ts index 3be9248907..4397464055 100644 --- a/src/app/core/data/remote-data.ts +++ b/src/app/core/data/remote-data.ts @@ -13,9 +13,9 @@ export enum RemoteDataState { */ export class RemoteData { constructor( - private requestPending?: boolean, - private responsePending?: boolean, - private isSuccessful?: boolean, + public requestPending?: boolean, + public responsePending?: boolean, + public isSuccessful?: boolean, public error?: RemoteDataError, public payload?: T ) { diff --git a/src/app/core/shared/child-hal-resource.model.ts b/src/app/core/shared/child-hal-resource.model.ts new file mode 100644 index 0000000000..b7682e2631 --- /dev/null +++ b/src/app/core/shared/child-hal-resource.model.ts @@ -0,0 +1,5 @@ +import { HALResource } from './hal-resource.model'; + +export interface ChildHALResource extends HALResource { + getParentLinkKey(): keyof this['_links']; +} diff --git a/src/app/core/shared/collection.model.ts b/src/app/core/shared/collection.model.ts index d5c6221428..ba2f448bba 100644 --- a/src/app/core/shared/collection.model.ts +++ b/src/app/core/shared/collection.model.ts @@ -12,10 +12,13 @@ import { License } from './license.model'; import { LICENSE } from './license.resource-type'; import { ResourcePolicy } from './resource-policy.model'; import { RESOURCE_POLICY } from './resource-policy.resource-type'; +import { COMMUNITY } from './community.resource-type'; +import { Community } from './community.model'; +import { ChildHALResource } from './child-hal-resource.model'; @typedObject @inheritSerialization(DSpaceObject) -export class Collection extends DSpaceObject { +export class Collection extends DSpaceObject implements ChildHALResource { static type = COLLECTION; /** @@ -35,6 +38,7 @@ export class Collection extends DSpaceObject { itemtemplate: HALLink; defaultAccessConditions: HALLink; logo: HALLink; + parentCommunity: HALLink; self: HALLink; }; @@ -59,6 +63,13 @@ export class Collection extends DSpaceObject { @link(RESOURCE_POLICY, true) defaultAccessConditions?: Observable>>; + /** + * The Community that is a direct parent of this Collection + * Will be undefined unless the parent community HALLink has been resolved. + */ + @link(COMMUNITY, false) + parentCommunity?: Observable>; + /** * The introductory text of this Collection * Corresponds to the metadata field dc.description @@ -98,4 +109,8 @@ export class Collection extends DSpaceObject { get sidebarText(): string { return this.firstMetadataValue('dc.description.tableofcontents'); } + + getParentLinkKey(): keyof this['_links'] { + return 'parentCommunity'; + } } diff --git a/src/app/core/shared/community.model.ts b/src/app/core/shared/community.model.ts index 703c4b3eef..e18ec743e8 100644 --- a/src/app/core/shared/community.model.ts +++ b/src/app/core/shared/community.model.ts @@ -10,10 +10,11 @@ import { COLLECTION } from './collection.resource-type'; import { COMMUNITY } from './community.resource-type'; import { DSpaceObject } from './dspace-object.model'; import { HALLink } from './hal-link.model'; +import { ChildHALResource } from './child-hal-resource.model'; @typedObject @inheritSerialization(DSpaceObject) -export class Community extends DSpaceObject { +export class Community extends DSpaceObject implements ChildHALResource { static type = COMMUNITY; /** @@ -30,6 +31,7 @@ export class Community extends DSpaceObject { collections: HALLink; logo: HALLink; subcommunities: HALLink; + parentCommunity: HALLink; self: HALLink; }; @@ -54,6 +56,13 @@ export class Community extends DSpaceObject { @link(COMMUNITY, true) subcommunities?: Observable>>; + /** + * The Community that is a direct parent of this Community + * Will be undefined unless the parent community HALLink has been resolved. + */ + @link(COMMUNITY, false) + parentCommunity?: Observable>; + /** * The introductory text of this Community * Corresponds to the metadata field dc.description @@ -85,4 +94,8 @@ export class Community extends DSpaceObject { get sidebarText(): string { return this.firstMetadataValue('dc.description.tableofcontents'); } + + getParentLinkKey(): keyof this['_links'] { + return 'parentCommunity'; + } } diff --git a/src/app/core/shared/item.model.ts b/src/app/core/shared/item.model.ts index 3fd35280da..e7f0ae9e10 100644 --- a/src/app/core/shared/item.model.ts +++ b/src/app/core/shared/item.model.ts @@ -17,13 +17,14 @@ import { HALLink } from './hal-link.model'; import { Relationship } from './item-relationships/relationship.model'; import { RELATIONSHIP } from './item-relationships/relationship.resource-type'; import { ITEM } from './item.resource-type'; +import { ChildHALResource } from './child-hal-resource.model'; /** * Class representing a DSpace Item */ @typedObject @inheritSerialization(DSpaceObject) -export class Item extends DSpaceObject { +export class Item extends DSpaceObject implements ChildHALResource { static type = ITEM; /** @@ -100,4 +101,8 @@ export class Item extends DSpaceObject { } return [entityType, ...super.getRenderTypes()]; } + + getParentLinkKey(): keyof this['_links'] { + return 'owningCollection'; + } } From f67387ed65984ff93783bd72b0632ec4aafa1416 Mon Sep 17 00:00:00 2001 From: lotte Date: Mon, 24 Feb 2020 16:06:12 +0100 Subject: [PATCH 04/12] first draft breadcrumbs --- .../collection-item-mapper.component.ts | 2 +- .../collection-page-routing.module.ts | 70 +++++---- .../collection-page.component.ts | 2 +- .../edit-collection-page.routing.module.ts | 7 - .../community-page-routing.module.ts | 55 ++++--- .../community-page.component.ts | 2 +- .../edit-community-page.routing.module.ts | 6 - .../edit-item-page.routing.module.ts | 141 ++++++++---------- .../+item-page/item-page-routing.module.ts | 41 ++--- .../+login-page/login-page-routing.module.ts | 6 +- .../search-page-routing.module.ts | 13 +- .../breadcrumbs/breadcrumbs.component.html | 7 +- src/app/breadcrumbs/breadcrumbs.component.ts | 8 +- .../breadcrumbs/dso-breadcrumb.resolver.ts | 2 +- .../breadcrumbs/dso-breadcrumbs.service.ts | 2 +- .../builders/remote-data-build.service.ts | 15 +- src/app/core/data/remote-data.ts | 9 +- 17 files changed, 197 insertions(+), 191 deletions(-) diff --git a/src/app/+collection-page/collection-item-mapper/collection-item-mapper.component.ts b/src/app/+collection-page/collection-item-mapper/collection-item-mapper.component.ts index ec384df641..64ad426584 100644 --- a/src/app/+collection-page/collection-item-mapper/collection-item-mapper.component.ts +++ b/src/app/+collection-page/collection-item-mapper/collection-item-mapper.component.ts @@ -102,7 +102,7 @@ export class CollectionItemMapperComponent implements OnInit { } ngOnInit(): void { - this.collectionRD$ = this.route.data.pipe(map((data) => data.collection)).pipe(getSucceededRemoteData()) as Observable>; + this.collectionRD$ = this.route.data.pipe(map((data) => data.dso)).pipe(getSucceededRemoteData()) as Observable>; this.searchOptions$ = this.searchConfigService.paginatedSearchOptions; this.loadItemLists(); } diff --git a/src/app/+collection-page/collection-page-routing.module.ts b/src/app/+collection-page/collection-page-routing.module.ts index 2df7997e1e..4bd23f4f95 100644 --- a/src/app/+collection-page/collection-page-routing.module.ts +++ b/src/app/+collection-page/collection-page-routing.module.ts @@ -10,6 +10,9 @@ import { DeleteCollectionPageComponent } from './delete-collection-page/delete-c import { URLCombiner } from '../core/url-combiner/url-combiner'; import { getCollectionModulePath } from '../app-routing.module'; import { CollectionItemMapperComponent } from './collection-item-mapper/collection-item-mapper.component'; +import { CollectionBreadcrumbResolver } from '../core/breadcrumbs/collection-breadcrumb.resolver'; +import { DSOBreadcrumbsService } from '../core/breadcrumbs/dso-breadcrumbs.service'; +import { LinkService } from '../core/cache/builders/link.service'; export const COLLECTION_PARENT_PARAMETER = 'parent'; @@ -26,51 +29,54 @@ export function getCollectionCreatePath() { } const COLLECTION_CREATE_PATH = 'create'; -const COLLECTION_EDIT_PATH = ':id/edit'; +const COLLECTION_EDIT_PATH = 'edit'; @NgModule({ imports: [ RouterModule.forChild([ + { + path: ':id', + resolve: { + dso: CollectionPageResolver, + breadcrumb: CollectionBreadcrumbResolver + }, + children: [ + { + path: COLLECTION_EDIT_PATH, + loadChildren: './edit-collection-page/edit-collection-page.module#EditCollectionPageModule', + canActivate: [AuthenticatedGuard] + }, + { + path: 'delete', + pathMatch: 'full', + component: DeleteCollectionPageComponent, + canActivate: [AuthenticatedGuard], + }, + { + path: '', + component: CollectionPageComponent, + pathMatch: 'full', + }, + { + path: '/edit/mapper', + component: CollectionItemMapperComponent, + pathMatch: 'full', + canActivate: [AuthenticatedGuard] + } + ] + }, { path: COLLECTION_CREATE_PATH, component: CreateCollectionPageComponent, canActivate: [AuthenticatedGuard, CreateCollectionPageGuard] }, - { - path: COLLECTION_EDIT_PATH, - loadChildren: './edit-collection-page/edit-collection-page.module#EditCollectionPageModule', - canActivate: [AuthenticatedGuard] - }, - { - path: ':id/delete', - pathMatch: 'full', - component: DeleteCollectionPageComponent, - canActivate: [AuthenticatedGuard], - resolve: { - dso: CollectionPageResolver - } - }, - { - path: ':id', - component: CollectionPageComponent, - pathMatch: 'full', - resolve: { - collection: CollectionPageResolver - } - }, - { - path: ':id/edit/mapper', - component: CollectionItemMapperComponent, - pathMatch: 'full', - resolve: { - collection: CollectionPageResolver - }, - canActivate: [AuthenticatedGuard] - } ]) ], providers: [ CollectionPageResolver, + CollectionBreadcrumbResolver, + DSOBreadcrumbsService, + LinkService, CreateCollectionPageGuard ] }) diff --git a/src/app/+collection-page/collection-page.component.ts b/src/app/+collection-page/collection-page.component.ts index 4866cf3b60..7f54e0f9d7 100644 --- a/src/app/+collection-page/collection-page.component.ts +++ b/src/app/+collection-page/collection-page.component.ts @@ -62,7 +62,7 @@ export class CollectionPageComponent implements OnInit { ngOnInit(): void { this.collectionRD$ = this.route.data.pipe( - map((data) => data.collection as RemoteData), + map((data) => data.dso as RemoteData), redirectToPageNotFoundOn404(this.router), take(1) ); diff --git a/src/app/+collection-page/edit-collection-page/edit-collection-page.routing.module.ts b/src/app/+collection-page/edit-collection-page/edit-collection-page.routing.module.ts index fcfced9d81..4498198e8d 100644 --- a/src/app/+collection-page/edit-collection-page/edit-collection-page.routing.module.ts +++ b/src/app/+collection-page/edit-collection-page/edit-collection-page.routing.module.ts @@ -1,7 +1,6 @@ import { RouterModule } from '@angular/router'; import { NgModule } from '@angular/core'; import { EditCollectionPageComponent } from './edit-collection-page.component'; -import { CollectionPageResolver } from '../collection-page.resolver'; import { CollectionMetadataComponent } from './collection-metadata/collection-metadata.component'; import { CollectionRolesComponent } from './collection-roles/collection-roles.component'; import { CollectionSourceComponent } from './collection-source/collection-source.component'; @@ -16,9 +15,6 @@ import { CollectionCurateComponent } from './collection-curate/collection-curate { path: '', component: EditCollectionPageComponent, - resolve: { - dso: CollectionPageResolver - }, children: [ { path: '', @@ -51,9 +47,6 @@ import { CollectionCurateComponent } from './collection-curate/collection-curate ] } ]) - ], - providers: [ - CollectionPageResolver, ] }) export class EditCollectionPageRoutingModule { diff --git a/src/app/+community-page/community-page-routing.module.ts b/src/app/+community-page/community-page-routing.module.ts index df548e0617..7ee71034a6 100644 --- a/src/app/+community-page/community-page-routing.module.ts +++ b/src/app/+community-page/community-page-routing.module.ts @@ -9,6 +9,9 @@ import { CreateCommunityPageGuard } from './create-community-page/create-communi import { DeleteCommunityPageComponent } from './delete-community-page/delete-community-page.component'; import { URLCombiner } from '../core/url-combiner/url-combiner'; import { getCommunityModulePath } from '../app-routing.module'; +import { CommunityBreadcrumbResolver } from '../core/breadcrumbs/community-breadcrumb.resolver'; +import { DSOBreadcrumbsService } from '../core/breadcrumbs/dso-breadcrumbs.service'; +import { LinkService } from '../core/cache/builders/link.service'; export const COMMUNITY_PARENT_PARAMETER = 'parent'; @@ -25,42 +28,48 @@ export function getCommunityCreatePath() { } const COMMUNITY_CREATE_PATH = 'create'; -const COMMUNITY_EDIT_PATH = ':id/edit'; +const COMMUNITY_EDIT_PATH = 'edit'; @NgModule({ imports: [ RouterModule.forChild([ + { + path: ':id', + resolve: { + dso: CommunityPageResolver, + breadcrumb: CommunityBreadcrumbResolver + }, + children: [ + { + path: COMMUNITY_EDIT_PATH, + loadChildren: './edit-community-page/edit-community-page.module#EditCommunityPageModule', + canActivate: [AuthenticatedGuard] + }, + { + path: 'delete', + pathMatch: 'full', + component: DeleteCommunityPageComponent, + canActivate: [AuthenticatedGuard], + }, + { + path: '', + component: CommunityPageComponent, + pathMatch: 'full', + } + ] + }, { path: COMMUNITY_CREATE_PATH, component: CreateCommunityPageComponent, canActivate: [AuthenticatedGuard, CreateCommunityPageGuard] }, - { - path: COMMUNITY_EDIT_PATH, - loadChildren: './edit-community-page/edit-community-page.module#EditCommunityPageModule', - canActivate: [AuthenticatedGuard] - }, - { - path: ':id/delete', - pathMatch: 'full', - component: DeleteCommunityPageComponent, - canActivate: [AuthenticatedGuard], - resolve: { - dso: CommunityPageResolver - } - }, - { - path: ':id', - component: CommunityPageComponent, - pathMatch: 'full', - resolve: { - community: CommunityPageResolver - } - } ]) ], providers: [ CommunityPageResolver, + CommunityBreadcrumbResolver, + DSOBreadcrumbsService, + LinkService, CreateCommunityPageGuard ] }) diff --git a/src/app/+community-page/community-page.component.ts b/src/app/+community-page/community-page.component.ts index f337d70250..3621829927 100644 --- a/src/app/+community-page/community-page.component.ts +++ b/src/app/+community-page/community-page.component.ts @@ -46,7 +46,7 @@ export class CommunityPageComponent implements OnInit { ngOnInit(): void { this.communityRD$ = this.route.data.pipe( - map((data) => data.community as RemoteData), + map((data) => data.dso as RemoteData), redirectToPageNotFoundOn404(this.router) ); this.logoRD$ = this.communityRD$.pipe( diff --git a/src/app/+community-page/edit-community-page/edit-community-page.routing.module.ts b/src/app/+community-page/edit-community-page/edit-community-page.routing.module.ts index 1182db2de1..721a404f84 100644 --- a/src/app/+community-page/edit-community-page/edit-community-page.routing.module.ts +++ b/src/app/+community-page/edit-community-page/edit-community-page.routing.module.ts @@ -15,9 +15,6 @@ import { CommunityCurateComponent } from './community-curate/community-curate.co { path: '', component: EditCommunityPageComponent, - resolve: { - dso: CommunityPageResolver - }, children: [ { path: '', @@ -46,9 +43,6 @@ import { CommunityCurateComponent } from './community-curate/community-curate.co } ]) ], - providers: [ - CommunityPageResolver, - ] }) export class EditCommunityPageRoutingModule { diff --git a/src/app/+item-page/edit-item-page/edit-item-page.routing.module.ts b/src/app/+item-page/edit-item-page/edit-item-page.routing.module.ts index 1b386440c0..71d8f65568 100644 --- a/src/app/+item-page/edit-item-page/edit-item-page.routing.module.ts +++ b/src/app/+item-page/edit-item-page/edit-item-page.routing.module.ts @@ -13,6 +13,7 @@ import { ItemBitstreamsComponent } from './item-bitstreams/item-bitstreams.compo import { ItemCollectionMapperComponent } from './item-collection-mapper/item-collection-mapper.component'; import { ItemMoveComponent } from './item-move/item-move.component'; import { ItemRelationshipsComponent } from './item-relationships/item-relationships.component'; +import { I18nBreadcrumbResolver } from '../../core/breadcrumbs/i18n-breadcrumb.resolver'; const ITEM_EDIT_WITHDRAW_PATH = 'withdraw'; const ITEM_EDIT_REINSTATE_PATH = 'reinstate'; @@ -29,104 +30,88 @@ const ITEM_EDIT_MOVE_PATH = 'move'; RouterModule.forChild([ { path: '', - component: EditItemPageComponent, resolve: { - item: ItemPageResolver + breadcrumb: I18nBreadcrumbResolver }, + data: { breadcrumbKey: 'edit.item' }, children: [ { path: '', - redirectTo: 'status', - pathMatch: 'full' + component: EditItemPageComponent, + children: [ + { + path: '', + redirectTo: 'status', + pathMatch: 'full' + }, + { + path: 'status', + component: ItemStatusComponent, + data: { title: 'item.edit.tabs.status.title' } + }, + { + path: 'bitstreams', + component: ItemBitstreamsComponent, + data: { title: 'item.edit.tabs.bitstreams.title' } + }, + { + path: 'metadata', + component: ItemMetadataComponent, + data: { title: 'item.edit.tabs.metadata.title' } + }, + { + path: 'relationships', + component: ItemRelationshipsComponent, + data: { title: 'item.edit.tabs.relationships.title' } + }, + { + path: 'view', + /* TODO - change when view page exists */ + component: ItemBitstreamsComponent, + data: { title: 'item.edit.tabs.view.title' } + }, + { + path: 'curate', + /* TODO - change when curate page exists */ + component: ItemBitstreamsComponent, + data: { title: 'item.edit.tabs.curate.title' } + } + ] }, { - path: 'status', - component: ItemStatusComponent, - data: { title: 'item.edit.tabs.status.title' } + path: 'mapper', + component: ItemCollectionMapperComponent, }, { - path: 'bitstreams', - component: ItemBitstreamsComponent, - data: { title: 'item.edit.tabs.bitstreams.title' } + path: ITEM_EDIT_WITHDRAW_PATH, + component: ItemWithdrawComponent, }, { - path: 'metadata', - component: ItemMetadataComponent, - data: { title: 'item.edit.tabs.metadata.title' } + path: ITEM_EDIT_REINSTATE_PATH, + component: ItemReinstateComponent, }, { - path: 'relationships', - component: ItemRelationshipsComponent, - data: { title: 'item.edit.tabs.relationships.title' } + path: ITEM_EDIT_PRIVATE_PATH, + component: ItemPrivateComponent, }, { - path: 'view', - /* TODO - change when view page exists */ - component: ItemBitstreamsComponent, - data: { title: 'item.edit.tabs.view.title' } + path: ITEM_EDIT_PUBLIC_PATH, + component: ItemPublicComponent, }, { - path: 'curate', - /* TODO - change when curate page exists */ - component: ItemBitstreamsComponent, - data: { title: 'item.edit.tabs.curate.title' } + path: ITEM_EDIT_DELETE_PATH, + component: ItemDeleteComponent, }, + { + path: ITEM_EDIT_MOVE_PATH, + component: ItemMoveComponent, + data: { title: 'item.edit.move.title' }, + } ] - }, - { - path: 'mapper', - component: ItemCollectionMapperComponent, - resolve: { - item: ItemPageResolver - } - }, - { - path: ITEM_EDIT_WITHDRAW_PATH, - component: ItemWithdrawComponent, - resolve: { - item: ItemPageResolver - } - }, - { - path: ITEM_EDIT_REINSTATE_PATH, - component: ItemReinstateComponent, - resolve: { - item: ItemPageResolver - } - }, - { - path: ITEM_EDIT_PRIVATE_PATH, - component: ItemPrivateComponent, - resolve: { - item: ItemPageResolver - } - }, - { - path: ITEM_EDIT_PUBLIC_PATH, - component: ItemPublicComponent, - resolve: { - item: ItemPageResolver - } - }, - { - path: ITEM_EDIT_DELETE_PATH, - component: ItemDeleteComponent, - resolve: { - item: ItemPageResolver - } - }, - { - path: ITEM_EDIT_MOVE_PATH, - component: ItemMoveComponent, - data: { title: 'item.edit.move.title' }, - resolve: { - item: ItemPageResolver - } - }]) + } + ]) ], - providers: [ - ItemPageResolver, - ] + providers: [] }) export class EditItemPageRoutingModule { diff --git a/src/app/+item-page/item-page-routing.module.ts b/src/app/+item-page/item-page-routing.module.ts index d2ad39d70d..4ceece59d0 100644 --- a/src/app/+item-page/item-page-routing.module.ts +++ b/src/app/+item-page/item-page-routing.module.ts @@ -14,37 +14,39 @@ import { LinkService } from '../core/cache/builders/link.service'; export function getItemPageRoute(itemId: string) { return new URLCombiner(getItemModulePath(), itemId).toString(); } + export function getItemEditPath(id: string) { - return new URLCombiner(getItemModulePath(),ITEM_EDIT_PATH.replace(/:id/, id)).toString() + return new URLCombiner(getItemModulePath(), ITEM_EDIT_PATH.replace(/:id/, id)).toString() } -const ITEM_EDIT_PATH = ':id/edit'; +const ITEM_EDIT_PATH = 'edit'; @NgModule({ imports: [ RouterModule.forChild([ { path: ':id', - component: ItemPageComponent, - pathMatch: 'full', resolve: { item: ItemPageResolver, breadcrumb: ItemBreadcrumbResolver - } - }, - { - path: ':id/full', - component: FullItemPageComponent, - resolve: { - item: ItemPageResolver, - breadcrumb: ItemBreadcrumbResolver - } - }, - { - path: ITEM_EDIT_PATH, - loadChildren: './edit-item-page/edit-item-page.module#EditItemPageModule', - canActivate: [AuthenticatedGuard] - }, + }, + children: [ + { + path: '', + component: ItemPageComponent, + pathMatch: 'full', + }, + { + path: 'full', + component: FullItemPageComponent, + }, + { + path: ITEM_EDIT_PATH, + loadChildren: './edit-item-page/edit-item-page.module#EditItemPageModule', + canActivate: [AuthenticatedGuard] + } + ], + } ]) ], providers: [ @@ -53,6 +55,7 @@ const ITEM_EDIT_PATH = ':id/edit'; DSOBreadcrumbsService, LinkService ] + }) export class ItemPageRoutingModule { diff --git a/src/app/+login-page/login-page-routing.module.ts b/src/app/+login-page/login-page-routing.module.ts index d3c6425dd3..950ac44d8c 100644 --- a/src/app/+login-page/login-page-routing.module.ts +++ b/src/app/+login-page/login-page-routing.module.ts @@ -2,12 +2,14 @@ import { NgModule } from '@angular/core'; import { RouterModule } from '@angular/router'; import { LoginPageComponent } from './login-page.component'; +import { I18nBreadcrumbResolver } from '../core/breadcrumbs/i18n-breadcrumb.resolver'; @NgModule({ imports: [ RouterModule.forChild([ - { path: '', pathMatch: 'full', component: LoginPageComponent, data: { title: 'login.title' } } + { path: '', pathMatch: 'full', component: LoginPageComponent, resolve: { breadcrumb: I18nBreadcrumbResolver }, data: { breadcrumbKey: 'Search', title: 'login.title' } } ]) ] }) -export class LoginPageRoutingModule { } +export class LoginPageRoutingModule { +} diff --git a/src/app/+search-page/search-page-routing.module.ts b/src/app/+search-page/search-page-routing.module.ts index 463aa8e7c4..c76207da9b 100644 --- a/src/app/+search-page/search-page-routing.module.ts +++ b/src/app/+search-page/search-page-routing.module.ts @@ -9,10 +9,15 @@ import { I18nBreadcrumbsService } from '../core/breadcrumbs/i18n-breadcrumbs.ser @NgModule({ imports: [ - RouterModule.forChild([ - { path: '', component: SearchPageComponent, resolve: { breadcrumb: I18nBreadcrumbResolver }, data: { title: 'search.title', breadcrumbKey: 'Search' } }, - { path: ':configuration', component: ConfigurationSearchPageComponent, canActivate: [ConfigurationSearchPageGuard] } - ]) + RouterModule.forChild([{ + path: '', + resolve: { breadcrumb: I18nBreadcrumbResolver }, data: { title: 'search.title', breadcrumbKey: 'Search' }, + children: [ + { path: '', component: SearchPageComponent }, + { path: ':configuration', component: ConfigurationSearchPageComponent, canActivate: [ConfigurationSearchPageGuard] } + ] + }] + ) ], providers: [ I18nBreadcrumbResolver, diff --git a/src/app/breadcrumbs/breadcrumbs.component.html b/src/app/breadcrumbs/breadcrumbs.component.html index 3a8730ea30..b773964d1e 100644 --- a/src/app/breadcrumbs/breadcrumbs.component.html +++ b/src/app/breadcrumbs/breadcrumbs.component.html @@ -1,16 +1,17 @@ - + - + + diff --git a/src/app/breadcrumbs/breadcrumbs.component.ts b/src/app/breadcrumbs/breadcrumbs.component.ts index 944af9b55d..fc3527a241 100644 --- a/src/app/breadcrumbs/breadcrumbs.component.ts +++ b/src/app/breadcrumbs/breadcrumbs.component.ts @@ -32,13 +32,17 @@ export class BreadcrumbsComponent implements OnDestroy { resolveBreadcrumb(route: ActivatedRoute): Observable { const data = route.snapshot.data; - const last: boolean = route.children.length === 0; + const routeConfig = route.snapshot.routeConfig; + const last: boolean = route.children.length === 0; if (last && isNotUndefined(data.showBreadcrumbs)) { this.showBreadcrumbs = data.showBreadcrumbs; } - if (hasValue(data) && hasValue(data.breadcrumb)) { + if ( + hasValue(data) && hasValue(data.breadcrumb) && + hasValue(routeConfig) && hasValue(routeConfig.resolve) && hasValue(routeConfig.resolve.breadcrumb) + ) { const { provider, key, url } = data.breadcrumb; if (!last) { return combineLatest(provider.getBreadcrumbs(key, url), this.resolveBreadcrumb(route.firstChild)) diff --git a/src/app/core/breadcrumbs/dso-breadcrumb.resolver.ts b/src/app/core/breadcrumbs/dso-breadcrumb.resolver.ts index 6e90d8d123..2f4138d144 100644 --- a/src/app/core/breadcrumbs/dso-breadcrumb.resolver.ts +++ b/src/app/core/breadcrumbs/dso-breadcrumb.resolver.ts @@ -29,7 +29,7 @@ export class DSOBreadcrumbResolver im getSucceededRemoteData(), getRemoteDataPayload(), map((object: T) => { - const fullPath = route.url.join(''); + const fullPath = state.url; const url = fullPath.substr(0, fullPath.indexOf(uuid)) + uuid; return { provider: this.breadcrumbService, key: object, url: url }; }) diff --git a/src/app/core/breadcrumbs/dso-breadcrumbs.service.ts b/src/app/core/breadcrumbs/dso-breadcrumbs.service.ts index 30344a3a77..0e6af2291b 100644 --- a/src/app/core/breadcrumbs/dso-breadcrumbs.service.ts +++ b/src/app/core/breadcrumbs/dso-breadcrumbs.service.ts @@ -21,7 +21,7 @@ export class DSOBreadcrumbsService implements BreadcrumbsService) => childRD.isSuccessful === true && childRD.requestPending === false && childRD.responsePending === false), + find((childRD: RemoteData) => childRD.hasSucceeded || childRD.statusCode === 204), switchMap((childRD: RemoteData) => { if (hasValue(childRD.payload)) { const child = childRD.payload; diff --git a/src/app/core/cache/builders/remote-data-build.service.ts b/src/app/core/cache/builders/remote-data-build.service.ts index 94c660d672..df895e11a2 100644 --- a/src/app/core/cache/builders/remote-data-build.service.ts +++ b/src/app/core/cache/builders/remote-data-build.service.ts @@ -96,13 +96,14 @@ export class RemoteDataBuildService { const responsePending = hasValue(reqEntry) && hasValue(reqEntry.responsePending) ? reqEntry.responsePending : false; let isSuccessful: boolean; let error: RemoteDataError; - if (hasValue(reqEntry) && hasValue(reqEntry.response)) { - isSuccessful = reqEntry.response.isSuccessful; - const errorMessage = isSuccessful === false ? (reqEntry.response as ErrorResponse).errorMessage : undefined; + const response = reqEntry ? reqEntry.response : undefined; + if (hasValue(response)) { + isSuccessful = response.isSuccessful; + const errorMessage = isSuccessful === false ? (response as ErrorResponse).errorMessage : undefined; if (hasValue(errorMessage)) { error = new RemoteDataError( - (reqEntry.response as ErrorResponse).statusCode, - (reqEntry.response as ErrorResponse).statusText, + response.statusCode, + response.statusText, errorMessage ); } @@ -112,7 +113,9 @@ export class RemoteDataBuildService { responsePending, isSuccessful, error, - payload + payload, + hasValue(response) ? response.statusCode : undefined + ); }) ); diff --git a/src/app/core/data/remote-data.ts b/src/app/core/data/remote-data.ts index 4397464055..8502c8ba1d 100644 --- a/src/app/core/data/remote-data.ts +++ b/src/app/core/data/remote-data.ts @@ -13,11 +13,12 @@ export enum RemoteDataState { */ export class RemoteData { constructor( - public requestPending?: boolean, - public responsePending?: boolean, - public isSuccessful?: boolean, + private requestPending?: boolean, + private responsePending?: boolean, + private isSuccessful?: boolean, public error?: RemoteDataError, - public payload?: T + public payload?: T, + public statusCode?: number, ) { } From 5b326aea92590c47cb1c2c80cf0bc407a235274f Mon Sep 17 00:00:00 2001 From: Art Lowel Date: Wed, 26 Feb 2020 13:58:09 +0100 Subject: [PATCH 05/12] add DSONameService --- .../breadcrumbs/dso-breadcrumbs.service.ts | 11 +- .../core/breadcrumbs/dso-name.service.spec.ts | 116 ++++++++++++++++++ src/app/core/breadcrumbs/dso-name.service.ts | 53 ++++++++ src/app/core/cache/builders/link.service.ts | 20 +-- src/app/core/metadata/metadata.service.ts | 4 +- src/app/core/shared/dspace-object.model.ts | 1 + 6 files changed, 193 insertions(+), 12 deletions(-) create mode 100644 src/app/core/breadcrumbs/dso-name.service.spec.ts create mode 100644 src/app/core/breadcrumbs/dso-name.service.ts 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; From b4a63fccf4501eae550403215122f01fd378a29f Mon Sep 17 00:00:00 2001 From: Art Lowel Date: Wed, 26 Feb 2020 14:17:27 +0100 Subject: [PATCH 06/12] rollback debug change --- src/app/core/cache/builders/link.service.ts | 20 ++++++++------------ 1 file changed, 8 insertions(+), 12 deletions(-) diff --git a/src/app/core/cache/builders/link.service.ts b/src/app/core/cache/builders/link.service.ts index e57adaa598..c41a5484a1 100644 --- a/src/app/core/cache/builders/link.service.ts +++ b/src/app/core/cache/builders/link.service.ts @@ -55,20 +55,16 @@ export class LinkService { parent: this.parentInjector }).get(provider); - const link = model._links[matchingLinkDef.linkName]; + const href = model._links[matchingLinkDef.linkName].href; - 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}`); + 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}`); } } return model; From 4ae8997ada3eb09929f04ab5825187694abf19cb Mon Sep 17 00:00:00 2001 From: lotte Date: Wed, 26 Feb 2020 14:44:34 +0100 Subject: [PATCH 07/12] added tests for breadcrumbs service and component --- resources/i18n/en.json5 | 8 ++ .../collection-page-routing.module.ts | 2 +- .../edit-collection-page.routing.module.ts | 5 + .../community-page-routing.module.ts | 2 +- .../edit-community-page.routing.module.ts | 5 + .../edit-item-page.routing.module.ts | 2 +- .../+item-page/item-page-routing.module.ts | 2 +- .../+login-page/login-page-routing.module.ts | 2 +- .../search-page-routing.module.ts | 2 +- .../breadcrumbs/breadcrumbs.component.spec.ts | 90 +++++++++++++- src/app/breadcrumbs/breadcrumbs.component.ts | 18 +-- .../collection-breadcrumb.resolver.ts | 2 - .../dso-breadcrumbs.service.spec.ts | 117 ++++++++++++++++++ .../breadcrumbs/dso-breadcrumbs.service.ts | 10 +- .../i18n-breadcrumbs.service.spec.ts | 31 +++++ .../breadcrumbs/i18n-breadcrumbs.service.ts | 4 +- .../comcol-page-browse-by.component.ts | 7 +- 17 files changed, 280 insertions(+), 29 deletions(-) create mode 100644 src/app/core/breadcrumbs/dso-breadcrumbs.service.spec.ts create mode 100644 src/app/core/breadcrumbs/i18n-breadcrumbs.service.spec.ts diff --git a/resources/i18n/en.json5 b/resources/i18n/en.json5 index 8d956d1a0e..044cf216b8 100644 --- a/resources/i18n/en.json5 +++ b/resources/i18n/en.json5 @@ -266,6 +266,8 @@ "collection.edit.head": "Edit Collection", + "collection.edit.breadcrumbs": "Edit Collection", + "collection.edit.item-mapper.cancel": "Cancel", @@ -450,6 +452,7 @@ "community.edit.head": "Edit Community", + "community.edit.breadcrumbs": "Edit Community", "community.edit.logo.label": "Community logo", @@ -657,6 +660,8 @@ "item.edit.head": "Edit Item", + "item.edit.breadcrumbs": "Edit Item", + "item.edit.item-mapper.buttons.add": "Map item to selected collections", @@ -1077,6 +1082,8 @@ "login.title": "Login", + "login.breadcrumbs": "Login", + "logout.form.header": "Log out from DSpace", @@ -1473,6 +1480,7 @@ "search.title": "DSpace Angular :: Search", + "search.breadcrumbs": "Search", "search.filters.applied.f.author": "Author", diff --git a/src/app/+collection-page/collection-page-routing.module.ts b/src/app/+collection-page/collection-page-routing.module.ts index 4bd23f4f95..61cfda0d9e 100644 --- a/src/app/+collection-page/collection-page-routing.module.ts +++ b/src/app/+collection-page/collection-page-routing.module.ts @@ -21,7 +21,7 @@ export function getCollectionPageRoute(collectionId: string) { } export function getCollectionEditPath(id: string) { - return new URLCombiner(getCollectionModulePath(), COLLECTION_EDIT_PATH.replace(/:id/, id)).toString() + return new URLCombiner(getCollectionModulePath(), id, COLLECTION_EDIT_PATH).toString() } export function getCollectionCreatePath() { diff --git a/src/app/+collection-page/edit-collection-page/edit-collection-page.routing.module.ts b/src/app/+collection-page/edit-collection-page/edit-collection-page.routing.module.ts index 4498198e8d..e3d5749472 100644 --- a/src/app/+collection-page/edit-collection-page/edit-collection-page.routing.module.ts +++ b/src/app/+collection-page/edit-collection-page/edit-collection-page.routing.module.ts @@ -5,6 +5,7 @@ import { CollectionMetadataComponent } from './collection-metadata/collection-me import { CollectionRolesComponent } from './collection-roles/collection-roles.component'; import { CollectionSourceComponent } from './collection-source/collection-source.component'; import { CollectionCurateComponent } from './collection-curate/collection-curate.component'; +import { I18nBreadcrumbResolver } from '../../core/breadcrumbs/i18n-breadcrumb.resolver'; /** * Routing module that handles the routing for the Edit Collection page administrator functionality @@ -14,6 +15,10 @@ import { CollectionCurateComponent } from './collection-curate/collection-curate RouterModule.forChild([ { path: '', + resolve: { + breadcrumb: I18nBreadcrumbResolver + }, + data: { breadcrumbKey: 'collection.edit' }, component: EditCollectionPageComponent, children: [ { diff --git a/src/app/+community-page/community-page-routing.module.ts b/src/app/+community-page/community-page-routing.module.ts index 7ee71034a6..976a4ad0fe 100644 --- a/src/app/+community-page/community-page-routing.module.ts +++ b/src/app/+community-page/community-page-routing.module.ts @@ -20,7 +20,7 @@ export function getCommunityPageRoute(communityId: string) { } export function getCommunityEditPath(id: string) { - return new URLCombiner(getCommunityModulePath(), COMMUNITY_EDIT_PATH.replace(/:id/, id)).toString() + return new URLCombiner(getCommunityModulePath(), id, COMMUNITY_EDIT_PATH).toString() } export function getCommunityCreatePath() { diff --git a/src/app/+community-page/edit-community-page/edit-community-page.routing.module.ts b/src/app/+community-page/edit-community-page/edit-community-page.routing.module.ts index 721a404f84..f0a7813bac 100644 --- a/src/app/+community-page/edit-community-page/edit-community-page.routing.module.ts +++ b/src/app/+community-page/edit-community-page/edit-community-page.routing.module.ts @@ -5,6 +5,7 @@ import { NgModule } from '@angular/core'; import { CommunityMetadataComponent } from './community-metadata/community-metadata.component'; import { CommunityRolesComponent } from './community-roles/community-roles.component'; import { CommunityCurateComponent } from './community-curate/community-curate.component'; +import { I18nBreadcrumbResolver } from '../../core/breadcrumbs/i18n-breadcrumb.resolver'; /** * Routing module that handles the routing for the Edit Community page administrator functionality @@ -14,6 +15,10 @@ import { CommunityCurateComponent } from './community-curate/community-curate.co RouterModule.forChild([ { path: '', + resolve: { + breadcrumb: I18nBreadcrumbResolver + }, + data: { breadcrumbKey: 'community.edit' }, component: EditCommunityPageComponent, children: [ { diff --git a/src/app/+item-page/edit-item-page/edit-item-page.routing.module.ts b/src/app/+item-page/edit-item-page/edit-item-page.routing.module.ts index 71d8f65568..af78eeea6f 100644 --- a/src/app/+item-page/edit-item-page/edit-item-page.routing.module.ts +++ b/src/app/+item-page/edit-item-page/edit-item-page.routing.module.ts @@ -33,7 +33,7 @@ const ITEM_EDIT_MOVE_PATH = 'move'; resolve: { breadcrumb: I18nBreadcrumbResolver }, - data: { breadcrumbKey: 'edit.item' }, + data: { breadcrumbKey: 'item.edit' }, children: [ { path: '', diff --git a/src/app/+item-page/item-page-routing.module.ts b/src/app/+item-page/item-page-routing.module.ts index 4ceece59d0..686c5ff2fc 100644 --- a/src/app/+item-page/item-page-routing.module.ts +++ b/src/app/+item-page/item-page-routing.module.ts @@ -16,7 +16,7 @@ export function getItemPageRoute(itemId: string) { } export function getItemEditPath(id: string) { - return new URLCombiner(getItemModulePath(), ITEM_EDIT_PATH.replace(/:id/, id)).toString() + return new URLCombiner(getItemModulePath(), id, ITEM_EDIT_PATH).toString() } const ITEM_EDIT_PATH = 'edit'; diff --git a/src/app/+login-page/login-page-routing.module.ts b/src/app/+login-page/login-page-routing.module.ts index 950ac44d8c..cd023da55c 100644 --- a/src/app/+login-page/login-page-routing.module.ts +++ b/src/app/+login-page/login-page-routing.module.ts @@ -7,7 +7,7 @@ import { I18nBreadcrumbResolver } from '../core/breadcrumbs/i18n-breadcrumb.reso @NgModule({ imports: [ RouterModule.forChild([ - { path: '', pathMatch: 'full', component: LoginPageComponent, resolve: { breadcrumb: I18nBreadcrumbResolver }, data: { breadcrumbKey: 'Search', title: 'login.title' } } + { path: '', pathMatch: 'full', component: LoginPageComponent, resolve: { breadcrumb: I18nBreadcrumbResolver }, data: { breadcrumbKey: 'login', title: 'login.title' } } ]) ] }) diff --git a/src/app/+search-page/search-page-routing.module.ts b/src/app/+search-page/search-page-routing.module.ts index c76207da9b..6e36883394 100644 --- a/src/app/+search-page/search-page-routing.module.ts +++ b/src/app/+search-page/search-page-routing.module.ts @@ -11,7 +11,7 @@ import { I18nBreadcrumbsService } from '../core/breadcrumbs/i18n-breadcrumbs.ser imports: [ RouterModule.forChild([{ path: '', - resolve: { breadcrumb: I18nBreadcrumbResolver }, data: { title: 'search.title', breadcrumbKey: 'Search' }, + resolve: { breadcrumb: I18nBreadcrumbResolver }, data: { title: 'search.title', breadcrumbKey: 'search' }, children: [ { path: '', component: SearchPageComponent }, { path: ':configuration', component: ConfigurationSearchPageComponent, canActivate: [ConfigurationSearchPageGuard] } diff --git a/src/app/breadcrumbs/breadcrumbs.component.spec.ts b/src/app/breadcrumbs/breadcrumbs.component.spec.ts index 175ef83757..0ab1fed208 100644 --- a/src/app/breadcrumbs/breadcrumbs.component.spec.ts +++ b/src/app/breadcrumbs/breadcrumbs.component.spec.ts @@ -1,25 +1,111 @@ import { async, ComponentFixture, TestBed } from '@angular/core/testing'; import { BreadcrumbsComponent } from './breadcrumbs.component'; +import { ActivatedRoute, NavigationEnd, Router } from '@angular/router'; +import { Observable, of as observableOf } from 'rxjs'; +import { RouterTestingModule } from '@angular/router/testing'; +import { TranslateLoader, TranslateModule } from '@ngx-translate/core'; +import { MockTranslateLoader } from '../shared/testing/mock-translate-loader'; +import { BreadcrumbConfig } from './breadcrumb/breadcrumb-config.model'; +import { BreadcrumbsService } from '../core/breadcrumbs/breadcrumbs.service'; +import { Breadcrumb } from './breadcrumb/breadcrumb.model'; +import { getTestScheduler } from 'jasmine-marbles'; + +class TestBreadcrumbsService implements BreadcrumbsService { + getBreadcrumbs(key: string, url: string): Observable { + return observableOf([new Breadcrumb(key, url)]); + } +} describe('BreadcrumbsComponent', () => { let component: BreadcrumbsComponent; let fixture: ComponentFixture; + let router: any; + let route: any; + let breadcrumbProvider; + let breadcrumbConfigA: BreadcrumbConfig; + let breadcrumbConfigB: BreadcrumbConfig; + let expectedBreadcrumbs; + + function init() { + breadcrumbProvider = new TestBreadcrumbsService(); + + breadcrumbConfigA = { provider: breadcrumbProvider, key: 'example.path', url: 'example.com' }; + breadcrumbConfigB = { provider: breadcrumbProvider, key: 'another.path', url: 'another.com' }; + + route = { + root: { + snapshot: { + data: { breadcrumb: breadcrumbConfigA }, + routeConfig: { resolve: { breadcrumb: {} } } + }, + firstChild: { + snapshot: { + // Example without resolver should be ignored + data: { breadcrumb: breadcrumbConfigA }, + }, + firstChild: { + snapshot: { + data: { breadcrumb: breadcrumbConfigB }, + routeConfig: { resolve: { breadcrumb: {} } } + } + } + } + } + }; + + expectedBreadcrumbs = [ + new Breadcrumb(breadcrumbConfigA.key, breadcrumbConfigA.url), + new Breadcrumb(breadcrumbConfigB.key, breadcrumbConfigB.url) + ] + + } beforeEach(async(() => { + init(); TestBed.configureTestingModule({ - declarations: [ BreadcrumbsComponent ] + declarations: [BreadcrumbsComponent], + imports: [RouterTestingModule.withRoutes([]), TranslateModule.forRoot({ + loader: { + provide: TranslateLoader, + useClass: MockTranslateLoader + } + })], + providers: [ + { provide: ActivatedRoute, useValue: route } + + ] }) - .compileComponents(); + .compileComponents(); })); beforeEach(() => { fixture = TestBed.createComponent(BreadcrumbsComponent); component = fixture.componentInstance; + router = TestBed.get(Router); fixture.detectChanges(); }); it('should create', () => { expect(component).toBeTruthy(); }); + + describe('ngOnInit', () => { + beforeEach(() => { + spyOn(component, 'resolveBreadcrumbs').and.returnValue(observableOf([])) + }); + + it('should call resolveBreadcrumb on init', () => { + router.events = observableOf(new NavigationEnd(0, '', '')); + component.ngOnInit(); + expect(component.resolveBreadcrumbs).toHaveBeenCalledWith(route.root); + }) + }); + + describe('resolveBreadcrumbs', () => { + it('should return the correct breadcrumbs', () => { + const breadcrumbs = component.resolveBreadcrumbs(route.root); + getTestScheduler().expectObservable(breadcrumbs).toBe('(a|)', { a: expectedBreadcrumbs }) + }) + }) }); diff --git a/src/app/breadcrumbs/breadcrumbs.component.ts b/src/app/breadcrumbs/breadcrumbs.component.ts index fc3527a241..19b77fe59c 100644 --- a/src/app/breadcrumbs/breadcrumbs.component.ts +++ b/src/app/breadcrumbs/breadcrumbs.component.ts @@ -1,17 +1,16 @@ import { Component, OnDestroy, OnInit } from '@angular/core'; import { ActivatedRoute, NavigationEnd, Router } from '@angular/router'; import { Breadcrumb } from './breadcrumb/breadcrumb.model'; -import { hasValue, isNotUndefined } from '../shared/empty.util'; +import { hasNoValue, hasValue, isNotUndefined } from '../shared/empty.util'; import { filter, map, switchMap, tap } from 'rxjs/operators'; import { combineLatest, Observable, Subscription, of as observableOf } from 'rxjs'; -import { BreadcrumbConfig } from './breadcrumb/breadcrumb-config.model'; @Component({ selector: 'ds-breadcrumbs', templateUrl: './breadcrumbs.component.html', styleUrls: ['./breadcrumbs.component.scss'] }) -export class BreadcrumbsComponent implements OnDestroy { +export class BreadcrumbsComponent implements OnInit, OnDestroy { breadcrumbs: Breadcrumb[]; showBreadcrumbs: boolean; subscription: Subscription; @@ -20,21 +19,24 @@ export class BreadcrumbsComponent implements OnDestroy { private route: ActivatedRoute, private router: Router ) { + } + + ngOnInit(): void { this.subscription = this.router.events.pipe( filter((e): e is NavigationEnd => e instanceof NavigationEnd), tap(() => this.reset()), - switchMap(() => this.resolveBreadcrumb(this.route.root)) + switchMap(() => this.resolveBreadcrumbs(this.route.root)) ).subscribe((breadcrumbs) => { this.breadcrumbs = breadcrumbs; } ) } - resolveBreadcrumb(route: ActivatedRoute): Observable { + resolveBreadcrumbs(route: ActivatedRoute): Observable { const data = route.snapshot.data; const routeConfig = route.snapshot.routeConfig; - const last: boolean = route.children.length === 0; + const last: boolean = hasNoValue(route.firstChild); if (last && isNotUndefined(data.showBreadcrumbs)) { this.showBreadcrumbs = data.showBreadcrumbs; } @@ -45,13 +47,13 @@ export class BreadcrumbsComponent implements OnDestroy { ) { const { provider, key, url } = data.breadcrumb; if (!last) { - return combineLatest(provider.getBreadcrumbs(key, url), this.resolveBreadcrumb(route.firstChild)) + return combineLatest(provider.getBreadcrumbs(key, url), this.resolveBreadcrumbs(route.firstChild)) .pipe(map((crumbs) => [].concat.apply([], crumbs))); } else { return provider.getBreadcrumbs(key, url); } } - return !last ? this.resolveBreadcrumb(route.firstChild) : observableOf([]); + return !last ? this.resolveBreadcrumbs(route.firstChild) : observableOf([]); } ngOnDestroy(): void { diff --git a/src/app/core/breadcrumbs/collection-breadcrumb.resolver.ts b/src/app/core/breadcrumbs/collection-breadcrumb.resolver.ts index ec48e89421..78f90f149a 100644 --- a/src/app/core/breadcrumbs/collection-breadcrumb.resolver.ts +++ b/src/app/core/breadcrumbs/collection-breadcrumb.resolver.ts @@ -1,7 +1,5 @@ import { Injectable } from '@angular/core'; import { DSOBreadcrumbsService } from './dso-breadcrumbs.service'; -import { ItemDataService } from '../data/item-data.service'; -import { Item } from '../shared/item.model'; import { DSOBreadcrumbResolver } from './dso-breadcrumb.resolver'; import { Collection } from '../shared/collection.model'; import { CollectionDataService } from '../data/collection-data.service'; diff --git a/src/app/core/breadcrumbs/dso-breadcrumbs.service.spec.ts b/src/app/core/breadcrumbs/dso-breadcrumbs.service.spec.ts new file mode 100644 index 0000000000..dc6195071a --- /dev/null +++ b/src/app/core/breadcrumbs/dso-breadcrumbs.service.spec.ts @@ -0,0 +1,117 @@ +import { async, TestBed } from '@angular/core/testing'; +import { DSOBreadcrumbsService } from './dso-breadcrumbs.service'; +import { getMockLinkService } from '../../shared/mocks/mock-link-service'; +import { LinkService } from '../cache/builders/link.service'; +import { Item } from '../shared/item.model'; +import { createSuccessfulRemoteDataObject, createSuccessfulRemoteDataObject$ } from '../../shared/testing/utils'; +import { DSpaceObject } from '../shared/dspace-object.model'; +import { map } from 'rxjs/operators'; +import { of as observableOf } from 'rxjs'; +import { RemoteData } from '../data/remote-data'; +import { hasValue } from '../../shared/empty.util'; +import { Community } from '../shared/community.model'; +import { Collection } from '../shared/collection.model'; +import { Breadcrumb } from '../../breadcrumbs/breadcrumb/breadcrumb.model'; +import { getItemPageRoute } from '../../+item-page/item-page-routing.module'; +import { getCommunityPageRoute } from '../../+community-page/community-page-routing.module'; +import { getCollectionPageRoute } from '../../+collection-page/collection-page-routing.module'; +import { cold, getTestScheduler } from 'jasmine-marbles'; +import { getDSOPath } from '../../app-routing.module'; + +fdescribe('DSOBreadcrumbsService', () => { + let service: DSOBreadcrumbsService; + let linkService: any; + let testItem; + let testCollection; + let testCommunity; + + let itemPath; + let collectionPath; + let communityPath; + + let itemUUID; + let collectionUUID; + let communityUUID; + + let objects: DSpaceObject[]; + + function init() { + itemPath = '/items/'; + collectionPath = '/collection/'; + communityPath = '/community/'; + + itemUUID = '04dd18fc-03f9-4b9a-9304-ed7c313686d3'; + collectionUUID = '91dfa5b5-5440-4fb4-b869-02610342f886'; + communityUUID = '6c0bfa6b-ce82-4bf4-a2a8-fd7682c567e8'; + + testCommunity = Object.assign(new Community(), + { + type: 'community', + name: 'community', + uuid: communityUUID, + parentCommunity: observableOf(Object.assign(createSuccessfulRemoteDataObject(undefined), { statusCode: 204 })), + + _links: { + parentCommunity: 'site', + self: communityPath + communityUUID + } + } + ); + + testCollection = Object.assign(new Collection(), + { + type: 'collection', + name: 'collection', + uuid: collectionUUID, + parentCommunity: createSuccessfulRemoteDataObject$(testCommunity), + _links: { + parentCommunity: communityPath + communityUUID, + self: communityPath + collectionUUID + } + } + ); + + testItem = Object.assign(new Item(), + { + type: 'item', + name: 'item', + uuid: itemUUID, + owningCollection: createSuccessfulRemoteDataObject$(testCollection), + _links: { + owningCollection: collectionPath + collectionUUID, + self: itemPath + itemUUID + } + } + ); + + objects = [testItem, testCollection, testCommunity]; + + } + + beforeEach(async(() => { + init(); + TestBed.configureTestingModule({ + providers: [ + { provide: LinkService, useValue: getMockLinkService() } + ] + }).compileComponents(); + })); + + beforeEach(() => { + linkService = TestBed.get(LinkService); + linkService.resolveLink.and.callFake((object, link) => object); + service = new DSOBreadcrumbsService(linkService); + }); + + describe('getBreadcrumbs', () => { + it('should return the breadcrumbs based on an Item', () => { + const breadcrumbs = service.getBreadcrumbs(testItem, testItem._links.self); + const expectedCrumbs = [ + new Breadcrumb(testCommunity.name, getDSOPath(testCommunity)), + new Breadcrumb(testCollection.name, getDSOPath(testCollection)), + new Breadcrumb(testItem.name, getDSOPath(testItem)), + ]; + getTestScheduler().expectObservable(breadcrumbs).toBe('(a|)', { a: expectedCrumbs }); + }) + }); +}); diff --git a/src/app/core/breadcrumbs/dso-breadcrumbs.service.ts b/src/app/core/breadcrumbs/dso-breadcrumbs.service.ts index eaf1fed759..e4a22f1bf5 100644 --- a/src/app/core/breadcrumbs/dso-breadcrumbs.service.ts +++ b/src/app/core/breadcrumbs/dso-breadcrumbs.service.ts @@ -26,11 +26,11 @@ export class DSOBreadcrumbsService implements BreadcrumbsService) => childRD.hasSucceeded || childRD.statusCode === 204), - switchMap((childRD: RemoteData) => { - if (hasValue(childRD.payload)) { - const child = childRD.payload; - return this.getBreadcrumbs(child, getDSOPath(child)) + find((parentRD: RemoteData) => parentRD.hasSucceeded || parentRD.statusCode === 204), + switchMap((parentRD: RemoteData) => { + if (hasValue(parentRD.payload)) { + const parent = parentRD.payload; + return this.getBreadcrumbs(parent, getDSOPath(parent)) } return observableOf([]); diff --git a/src/app/core/breadcrumbs/i18n-breadcrumbs.service.spec.ts b/src/app/core/breadcrumbs/i18n-breadcrumbs.service.spec.ts new file mode 100644 index 0000000000..f29907312b --- /dev/null +++ b/src/app/core/breadcrumbs/i18n-breadcrumbs.service.spec.ts @@ -0,0 +1,31 @@ +import { async, TestBed } from '@angular/core/testing'; +import { Breadcrumb } from '../../breadcrumbs/breadcrumb/breadcrumb.model'; +import { getTestScheduler } from 'jasmine-marbles'; +import { BREADCRUMB_MESSAGE_POSTFIX, I18nBreadcrumbsService } from './i18n-breadcrumbs.service'; + +fdescribe('I18nBreadcrumbsService', () => { + let service: I18nBreadcrumbsService; + let exampleString; + let exampleURL; + + function init() { + exampleString = 'example.string'; + exampleURL = 'example.com'; + } + + beforeEach(async(() => { + init(); + TestBed.configureTestingModule({}).compileComponents(); + })); + + beforeEach(() => { + service = new I18nBreadcrumbsService(); + }); + + describe('getBreadcrumbs', () => { + it('should return a breadcrumb based on a string by adding the postfix', () => { + const breadcrumbs = service.getBreadcrumbs(exampleString, exampleURL); + getTestScheduler().expectObservable(breadcrumbs).toBe('(a|)', { a: [new Breadcrumb(exampleString + BREADCRUMB_MESSAGE_POSTFIX, exampleURL)] }); + }) + }); +}); diff --git a/src/app/core/breadcrumbs/i18n-breadcrumbs.service.ts b/src/app/core/breadcrumbs/i18n-breadcrumbs.service.ts index 6a9c8916a2..97ff89479a 100644 --- a/src/app/core/breadcrumbs/i18n-breadcrumbs.service.ts +++ b/src/app/core/breadcrumbs/i18n-breadcrumbs.service.ts @@ -3,11 +3,11 @@ import { BreadcrumbsService } from './breadcrumbs.service'; import { Observable, of as observableOf } from 'rxjs'; import { Injectable } from '@angular/core'; -export const BREADCRUMB_MESSAGE_PREFIX = 'breadcrumbs.'; +export const BREADCRUMB_MESSAGE_POSTFIX = '.breadcrumbs'; @Injectable() export class I18nBreadcrumbsService implements BreadcrumbsService { getBreadcrumbs(key: string, url: string): Observable { - return observableOf([new Breadcrumb(BREADCRUMB_MESSAGE_PREFIX + key, url)]); + return observableOf([new Breadcrumb(key + BREADCRUMB_MESSAGE_POSTFIX, url)]); } } diff --git a/src/app/shared/comcol-page-browse-by/comcol-page-browse-by.component.ts b/src/app/shared/comcol-page-browse-by/comcol-page-browse-by.component.ts index 1bc83d74a5..091e02723f 100644 --- a/src/app/shared/comcol-page-browse-by/comcol-page-browse-by.component.ts +++ b/src/app/shared/comcol-page-browse-by/comcol-page-browse-by.component.ts @@ -12,7 +12,7 @@ import { filter, map, startWith, tap } from 'rxjs/operators'; import { getCollectionPageRoute } from '../../+collection-page/collection-page-routing.module'; import { getCommunityPageRoute } from '../../+community-page/community-page-routing.module'; import { GLOBAL_CONFIG, GlobalConfig } from '../../../config'; -import { Router, ActivatedRoute, RouterModule, UrlSegment } from '@angular/router'; +import { Router, ActivatedRoute, RouterModule, UrlSegment, Params } from '@angular/router'; import { BrowseByTypeConfig } from '../../../config/browse-by-type-config.interface'; import { hasValue } from '../empty.util'; @@ -76,9 +76,8 @@ export class ComcolPageBrowseByComponent implements OnInit { }, ...this.allOptions ]; } - this.currentOptionId$ = this.route.url.pipe( - filter((urlSegments: UrlSegment[]) => hasValue(urlSegments)), - map((urlSegments: UrlSegment[]) => urlSegments[urlSegments.length - 1].path) + this.currentOptionId$ = this.route.params.pipe( + map((params: Params) => params.id) ); } From b554d40e9ca7514a35fb53aeef0bbd5602eb7829 Mon Sep 17 00:00:00 2001 From: lotte Date: Wed, 26 Feb 2020 16:53:10 +0100 Subject: [PATCH 08/12] added tests and docs --- .../collection-item-mapper.component.spec.ts | 2 +- src/app/breadcrumbs/breadcrumbs.component.ts | 27 ++++++++++++ .../core/breadcrumbs/breadcrumbs.service.ts | 9 ++++ .../collection-breadcrumb.resolver.ts | 2 +- .../community-breadcrumb.resolver.ts | 2 +- .../dso-breadcrumb.resolver.spec.ts | 34 +++++++++++++++ .../breadcrumbs/dso-breadcrumb.resolver.ts | 4 +- .../dso-breadcrumbs.service.spec.ts | 43 +++++++++++-------- .../breadcrumbs/dso-breadcrumbs.service.ts | 9 ++++ .../i18n-breadcrumb.resolver.spec.ts | 28 ++++++++++++ .../breadcrumbs/i18n-breadcrumb.resolver.ts | 2 +- .../i18n-breadcrumbs.service.spec.ts | 2 +- .../breadcrumbs/i18n-breadcrumbs.service.ts | 12 ++++++ .../breadcrumbs/item-breadcrumb.resolver.ts | 2 +- ...em-metadata-list-element.component.spec.ts | 2 +- 15 files changed, 152 insertions(+), 28 deletions(-) create mode 100644 src/app/core/breadcrumbs/dso-breadcrumb.resolver.spec.ts create mode 100644 src/app/core/breadcrumbs/i18n-breadcrumb.resolver.spec.ts diff --git a/src/app/+collection-page/collection-item-mapper/collection-item-mapper.component.spec.ts b/src/app/+collection-page/collection-item-mapper/collection-item-mapper.component.spec.ts index 8a07f5d235..19bdd2951e 100644 --- a/src/app/+collection-page/collection-item-mapper/collection-item-mapper.component.spec.ts +++ b/src/app/+collection-page/collection-item-mapper/collection-item-mapper.component.spec.ts @@ -83,7 +83,7 @@ describe('CollectionItemMapperComponent', () => { const itemDataServiceStub = { mapToCollection: () => of(new RestResponse(true, 200, 'OK')) }; - const activatedRouteStub = new ActivatedRouteStub({}, { collection: mockCollectionRD }); + const activatedRouteStub = new ActivatedRouteStub({}, { dso: mockCollectionRD }); const translateServiceStub = { get: () => of('test-message of collection ' + mockCollection.name), onLangChange: new EventEmitter(), diff --git a/src/app/breadcrumbs/breadcrumbs.component.ts b/src/app/breadcrumbs/breadcrumbs.component.ts index 19b77fe59c..861fda844b 100644 --- a/src/app/breadcrumbs/breadcrumbs.component.ts +++ b/src/app/breadcrumbs/breadcrumbs.component.ts @@ -5,14 +5,28 @@ import { hasNoValue, hasValue, isNotUndefined } from '../shared/empty.util'; import { filter, map, switchMap, tap } from 'rxjs/operators'; import { combineLatest, Observable, Subscription, of as observableOf } from 'rxjs'; +/** + * Component representing the breadcrumbs of a page + */ @Component({ selector: 'ds-breadcrumbs', templateUrl: './breadcrumbs.component.html', styleUrls: ['./breadcrumbs.component.scss'] }) export class BreadcrumbsComponent implements OnInit, OnDestroy { + /** + * List of breadcrumbs for this page + */ breadcrumbs: Breadcrumb[]; + + /** + * Whether or not to show breadcrumbs on this page + */ showBreadcrumbs: boolean; + + /** + * Subscription to unsubscribe from on destroy + */ subscription: Subscription; constructor( @@ -21,6 +35,9 @@ export class BreadcrumbsComponent implements OnInit, OnDestroy { ) { } + /** + * Sets the breadcrumbs on init for this page + */ ngOnInit(): void { this.subscription = this.router.events.pipe( filter((e): e is NavigationEnd => e instanceof NavigationEnd), @@ -32,6 +49,10 @@ export class BreadcrumbsComponent implements OnInit, OnDestroy { ) } + /** + * Method that recursively resolves breadcrumbs + * @param route The route to get the breadcrumb from + */ resolveBreadcrumbs(route: ActivatedRoute): Observable { const data = route.snapshot.data; const routeConfig = route.snapshot.routeConfig; @@ -56,12 +77,18 @@ export class BreadcrumbsComponent implements OnInit, OnDestroy { return !last ? this.resolveBreadcrumbs(route.firstChild) : observableOf([]); } + /** + * Unsubscribe from subscription + */ ngOnDestroy(): void { if (hasValue(this.subscription)) { this.subscription.unsubscribe(); } } + /** + * Resets the state of the breadcrumbs + */ reset() { this.breadcrumbs = []; this.showBreadcrumbs = true; diff --git a/src/app/core/breadcrumbs/breadcrumbs.service.ts b/src/app/core/breadcrumbs/breadcrumbs.service.ts index 30c6c44cf7..f274485d5d 100644 --- a/src/app/core/breadcrumbs/breadcrumbs.service.ts +++ b/src/app/core/breadcrumbs/breadcrumbs.service.ts @@ -1,6 +1,15 @@ import { Breadcrumb } from '../../breadcrumbs/breadcrumb/breadcrumb.model'; import { Observable } from 'rxjs'; +/** + * Service to calculate breadcrumbs for a single part of the route + */ export interface BreadcrumbsService { + + /** + * Method to calculate the breadcrumbs for a part of the route + * @param key The key used to resolve the breadcrumb + * @param url The url to use as a link for this breadcrumb + */ getBreadcrumbs(key: T, url: string): Observable; } diff --git a/src/app/core/breadcrumbs/collection-breadcrumb.resolver.ts b/src/app/core/breadcrumbs/collection-breadcrumb.resolver.ts index 78f90f149a..c662ead129 100644 --- a/src/app/core/breadcrumbs/collection-breadcrumb.resolver.ts +++ b/src/app/core/breadcrumbs/collection-breadcrumb.resolver.ts @@ -5,7 +5,7 @@ import { Collection } from '../shared/collection.model'; import { CollectionDataService } from '../data/collection-data.service'; /** - * The class that resolve the BreadcrumbConfig object for a route + * The class that resolves the BreadcrumbConfig object for a Collection */ @Injectable() export class CollectionBreadcrumbResolver extends DSOBreadcrumbResolver { diff --git a/src/app/core/breadcrumbs/community-breadcrumb.resolver.ts b/src/app/core/breadcrumbs/community-breadcrumb.resolver.ts index 3080823b97..1e4959f9e5 100644 --- a/src/app/core/breadcrumbs/community-breadcrumb.resolver.ts +++ b/src/app/core/breadcrumbs/community-breadcrumb.resolver.ts @@ -5,7 +5,7 @@ import { CommunityDataService } from '../data/community-data.service'; import { Community } from '../shared/community.model'; /** - * The class that resolve the BreadcrumbConfig object for a route + * The class that resolves the BreadcrumbConfig object for a Community */ @Injectable() export class CommunityBreadcrumbResolver extends DSOBreadcrumbResolver { diff --git a/src/app/core/breadcrumbs/dso-breadcrumb.resolver.spec.ts b/src/app/core/breadcrumbs/dso-breadcrumb.resolver.spec.ts new file mode 100644 index 0000000000..774fcd04d5 --- /dev/null +++ b/src/app/core/breadcrumbs/dso-breadcrumb.resolver.spec.ts @@ -0,0 +1,34 @@ +import { DSOBreadcrumbResolver } from './dso-breadcrumb.resolver'; +import { Collection } from '../shared/collection.model'; +import { createSuccessfulRemoteDataObject$ } from '../../shared/testing/utils'; +import { getTestScheduler } from 'jasmine-marbles'; + +describe('DSOBreadcrumbResolver', () => { + describe('resolve', () => { + let resolver: DSOBreadcrumbResolver; + let collectionService: any; + let dsoBreadcrumbService: any; + let testCollection: Collection; + let uuid; + let breadcrumbUrl; + let currentUrl; + + beforeEach(() => { + uuid = '1234-65487-12354-1235'; + breadcrumbUrl = '/collections/' + uuid; + currentUrl = breadcrumbUrl + '/edit'; + testCollection = Object.assign(new Collection(), { uuid }); + dsoBreadcrumbService = {}; + collectionService = { + findById: (id: string) => createSuccessfulRemoteDataObject$(testCollection) + }; + resolver = new DSOBreadcrumbResolver(dsoBreadcrumbService, collectionService); + }); + + it('should resolve a breadcrumb config for the correct DSO', () => { + const resolvedConfig = resolver.resolve({ params: { id: uuid } } as any, { url: currentUrl } as any); + const expectedConfig = { provider: dsoBreadcrumbService, key: testCollection, url: breadcrumbUrl }; + getTestScheduler().expectObservable(resolvedConfig).toBe('(a|)', { a: expectedConfig}) + }); + }); +}); diff --git a/src/app/core/breadcrumbs/dso-breadcrumb.resolver.ts b/src/app/core/breadcrumbs/dso-breadcrumb.resolver.ts index 2f4138d144..78ba349a5c 100644 --- a/src/app/core/breadcrumbs/dso-breadcrumb.resolver.ts +++ b/src/app/core/breadcrumbs/dso-breadcrumb.resolver.ts @@ -10,7 +10,7 @@ import { DSpaceObject } from '../shared/dspace-object.model'; import { ChildHALResource } from '../shared/child-hal-resource.model'; /** - * The class that resolve the BreadcrumbConfig object for a route + * The class that resolves the BreadcrumbConfig object for a DSpaceObject */ @Injectable() export class DSOBreadcrumbResolver implements Resolve> { @@ -18,7 +18,7 @@ export class DSOBreadcrumbResolver im } /** - * Method for resolving a site object + * Method for resolving a breadcrumb config object * @param {ActivatedRouteSnapshot} route The current ActivatedRouteSnapshot * @param {RouterStateSnapshot} state The current RouterStateSnapshot * @returns BreadcrumbConfig object diff --git a/src/app/core/breadcrumbs/dso-breadcrumbs.service.spec.ts b/src/app/core/breadcrumbs/dso-breadcrumbs.service.spec.ts index dc6195071a..101545cb14 100644 --- a/src/app/core/breadcrumbs/dso-breadcrumbs.service.spec.ts +++ b/src/app/core/breadcrumbs/dso-breadcrumbs.service.spec.ts @@ -5,20 +5,15 @@ import { LinkService } from '../cache/builders/link.service'; import { Item } from '../shared/item.model'; import { createSuccessfulRemoteDataObject, createSuccessfulRemoteDataObject$ } from '../../shared/testing/utils'; import { DSpaceObject } from '../shared/dspace-object.model'; -import { map } from 'rxjs/operators'; import { of as observableOf } from 'rxjs'; -import { RemoteData } from '../data/remote-data'; -import { hasValue } from '../../shared/empty.util'; import { Community } from '../shared/community.model'; import { Collection } from '../shared/collection.model'; import { Breadcrumb } from '../../breadcrumbs/breadcrumb/breadcrumb.model'; -import { getItemPageRoute } from '../../+item-page/item-page-routing.module'; -import { getCommunityPageRoute } from '../../+community-page/community-page-routing.module'; -import { getCollectionPageRoute } from '../../+collection-page/collection-page-routing.module'; -import { cold, getTestScheduler } from 'jasmine-marbles'; +import { getTestScheduler } from 'jasmine-marbles'; import { getDSOPath } from '../../app-routing.module'; +import { DSONameService } from './dso-name.service'; -fdescribe('DSOBreadcrumbsService', () => { +describe('DSOBreadcrumbsService', () => { let service: DSOBreadcrumbsService; let linkService: any; let testItem; @@ -33,7 +28,7 @@ fdescribe('DSOBreadcrumbsService', () => { let collectionUUID; let communityUUID; - let objects: DSpaceObject[]; + let dsoNameService; function init() { itemPath = '/items/'; @@ -47,7 +42,9 @@ fdescribe('DSOBreadcrumbsService', () => { testCommunity = Object.assign(new Community(), { type: 'community', - name: 'community', + metadata: { + 'dc.title': [{value: 'community'}] + }, uuid: communityUUID, parentCommunity: observableOf(Object.assign(createSuccessfulRemoteDataObject(undefined), { statusCode: 204 })), @@ -61,7 +58,9 @@ fdescribe('DSOBreadcrumbsService', () => { testCollection = Object.assign(new Collection(), { type: 'collection', - name: 'collection', + metadata: { + 'dc.title': [{value: 'collection'}] + }, uuid: collectionUUID, parentCommunity: createSuccessfulRemoteDataObject$(testCommunity), _links: { @@ -74,7 +73,9 @@ fdescribe('DSOBreadcrumbsService', () => { testItem = Object.assign(new Item(), { type: 'item', - name: 'item', + metadata: { + 'dc.title': [{value: 'item'}] + }, uuid: itemUUID, owningCollection: createSuccessfulRemoteDataObject$(testCollection), _links: { @@ -84,15 +85,15 @@ fdescribe('DSOBreadcrumbsService', () => { } ); - objects = [testItem, testCollection, testCommunity]; - + dsoNameService = { getName: (dso) => getName(dso) } } beforeEach(async(() => { init(); TestBed.configureTestingModule({ providers: [ - { provide: LinkService, useValue: getMockLinkService() } + { provide: LinkService, useValue: getMockLinkService() }, + { provide: DSONameService, useValue: dsoNameService } ] }).compileComponents(); })); @@ -100,18 +101,22 @@ fdescribe('DSOBreadcrumbsService', () => { beforeEach(() => { linkService = TestBed.get(LinkService); linkService.resolveLink.and.callFake((object, link) => object); - service = new DSOBreadcrumbsService(linkService); + service = new DSOBreadcrumbsService(linkService, dsoNameService); }); describe('getBreadcrumbs', () => { it('should return the breadcrumbs based on an Item', () => { const breadcrumbs = service.getBreadcrumbs(testItem, testItem._links.self); const expectedCrumbs = [ - new Breadcrumb(testCommunity.name, getDSOPath(testCommunity)), - new Breadcrumb(testCollection.name, getDSOPath(testCollection)), - new Breadcrumb(testItem.name, getDSOPath(testItem)), + new Breadcrumb(getName(testCommunity), getDSOPath(testCommunity)), + new Breadcrumb(getName(testCollection), getDSOPath(testCollection)), + new Breadcrumb(getName(testItem), getDSOPath(testItem)), ]; getTestScheduler().expectObservable(breadcrumbs).toBe('(a|)', { a: expectedCrumbs }); }) }); + + function getName(dso: DSpaceObject): string { + return dso.metadata['dc.title'][0].value + } }); diff --git a/src/app/core/breadcrumbs/dso-breadcrumbs.service.ts b/src/app/core/breadcrumbs/dso-breadcrumbs.service.ts index e4a22f1bf5..3cb73be876 100644 --- a/src/app/core/breadcrumbs/dso-breadcrumbs.service.ts +++ b/src/app/core/breadcrumbs/dso-breadcrumbs.service.ts @@ -12,6 +12,9 @@ import { RemoteData } from '../data/remote-data'; import { hasValue } from '../../shared/empty.util'; import { Injectable } from '@angular/core'; +/** + * Service to calculate DSpaceObject breadcrumbs for a single part of the route + */ @Injectable() export class DSOBreadcrumbsService implements BreadcrumbsService { constructor( @@ -21,6 +24,12 @@ export class DSOBreadcrumbsService implements BreadcrumbsService { const label = this.dsoNameService.getName(key); const crumb = new Breadcrumb(label, url); diff --git a/src/app/core/breadcrumbs/i18n-breadcrumb.resolver.spec.ts b/src/app/core/breadcrumbs/i18n-breadcrumb.resolver.spec.ts new file mode 100644 index 0000000000..d34d6d8a9b --- /dev/null +++ b/src/app/core/breadcrumbs/i18n-breadcrumb.resolver.spec.ts @@ -0,0 +1,28 @@ +import { I18nBreadcrumbResolver } from './i18n-breadcrumb.resolver'; + +describe('I18nBreadcrumbResolver', () => { + describe('resolve', () => { + let resolver: I18nBreadcrumbResolver; + let i18nBreadcrumbService: any; + let i18nKey: string; + let path: string; + beforeEach(() => { + i18nKey = 'example.key'; + path = 'rest.com/path/to/breadcrumb'; + i18nBreadcrumbService = {}; + resolver = new I18nBreadcrumbResolver(i18nBreadcrumbService); + }); + + it('should resolve the breadcrumb config', () => { + const resolvedConfig = resolver.resolve({ data: { breadcrumbKey: i18nKey }, url: [path] } as any, {} as any); + const expectedConfig = { provider: i18nBreadcrumbService, key: i18nKey, url: path }; + expect(resolvedConfig).toEqual(expectedConfig); + }); + + it('should resolve throw an error when no breadcrumbKey is defined', () => { + expect(() => { + resolver.resolve({ data: {} } as any, undefined) + }).toThrow(); + }); + }); +}); diff --git a/src/app/core/breadcrumbs/i18n-breadcrumb.resolver.ts b/src/app/core/breadcrumbs/i18n-breadcrumb.resolver.ts index 800a7b75d0..0978648ba3 100644 --- a/src/app/core/breadcrumbs/i18n-breadcrumb.resolver.ts +++ b/src/app/core/breadcrumbs/i18n-breadcrumb.resolver.ts @@ -5,7 +5,7 @@ import { I18nBreadcrumbsService } from './i18n-breadcrumbs.service'; import { hasNoValue } from '../../shared/empty.util'; /** - * The class that resolve the BreadcrumbConfig object for a route + * The class that resolves a BreadcrumbConfig object with an i18n key string for a route */ @Injectable() export class I18nBreadcrumbResolver implements Resolve> { diff --git a/src/app/core/breadcrumbs/i18n-breadcrumbs.service.spec.ts b/src/app/core/breadcrumbs/i18n-breadcrumbs.service.spec.ts index f29907312b..274389db3b 100644 --- a/src/app/core/breadcrumbs/i18n-breadcrumbs.service.spec.ts +++ b/src/app/core/breadcrumbs/i18n-breadcrumbs.service.spec.ts @@ -3,7 +3,7 @@ import { Breadcrumb } from '../../breadcrumbs/breadcrumb/breadcrumb.model'; import { getTestScheduler } from 'jasmine-marbles'; import { BREADCRUMB_MESSAGE_POSTFIX, I18nBreadcrumbsService } from './i18n-breadcrumbs.service'; -fdescribe('I18nBreadcrumbsService', () => { +describe('I18nBreadcrumbsService', () => { let service: I18nBreadcrumbsService; let exampleString; let exampleURL; diff --git a/src/app/core/breadcrumbs/i18n-breadcrumbs.service.ts b/src/app/core/breadcrumbs/i18n-breadcrumbs.service.ts index 97ff89479a..e07d9ed541 100644 --- a/src/app/core/breadcrumbs/i18n-breadcrumbs.service.ts +++ b/src/app/core/breadcrumbs/i18n-breadcrumbs.service.ts @@ -3,10 +3,22 @@ import { BreadcrumbsService } from './breadcrumbs.service'; import { Observable, of as observableOf } from 'rxjs'; import { Injectable } from '@angular/core'; +/** + * The postfix for i18n breadcrumbs + */ export const BREADCRUMB_MESSAGE_POSTFIX = '.breadcrumbs'; +/** + * Service to calculate i18n breadcrumbs for a single part of the route + */ @Injectable() export class I18nBreadcrumbsService implements BreadcrumbsService { + + /** + * Method to calculate the breadcrumbs + * @param key The key used to resolve the breadcrumb + * @param url The url to use as a link for this breadcrumb + */ getBreadcrumbs(key: string, url: string): Observable { return observableOf([new Breadcrumb(key + BREADCRUMB_MESSAGE_POSTFIX, url)]); } diff --git a/src/app/core/breadcrumbs/item-breadcrumb.resolver.ts b/src/app/core/breadcrumbs/item-breadcrumb.resolver.ts index fdfd3c11f9..c447f7de2a 100644 --- a/src/app/core/breadcrumbs/item-breadcrumb.resolver.ts +++ b/src/app/core/breadcrumbs/item-breadcrumb.resolver.ts @@ -5,7 +5,7 @@ import { Item } from '../shared/item.model'; import { DSOBreadcrumbResolver } from './dso-breadcrumb.resolver'; /** - * The class that resolve the BreadcrumbConfig object for a route + * The class that resolves the BreadcrumbConfig object for an Item */ @Injectable() export class ItemBreadcrumbResolver extends DSOBreadcrumbResolver { diff --git a/src/app/entity-groups/research-entities/metadata-representations/org-unit/org-unit-item-metadata-list-element.component.spec.ts b/src/app/entity-groups/research-entities/metadata-representations/org-unit/org-unit-item-metadata-list-element.component.spec.ts index 37cbe47c72..0354469372 100644 --- a/src/app/entity-groups/research-entities/metadata-representations/org-unit/org-unit-item-metadata-list-element.component.spec.ts +++ b/src/app/entity-groups/research-entities/metadata-representations/org-unit/org-unit-item-metadata-list-element.component.spec.ts @@ -13,7 +13,7 @@ const mockItem = Object.assign(new Item(), { metadata: { 'dc.description': [{ va const virtMD = Object.assign(new MetadataValue(), { value: organisation }); const mockItemMetadataRepresentation = Object.assign(new ItemMetadataRepresentation(virtMD), mockItem); -describe('OrgUnitItemMetadataListElementComponent', () => { +fdescribe('OrgUnitItemMetadataListElementComponent', () => { let comp: OrgUnitItemMetadataListElementComponent; let fixture: ComponentFixture; From d2fe506299240b41f6742c5f582e40374838a811 Mon Sep 17 00:00:00 2001 From: lotte Date: Wed, 26 Feb 2020 17:08:59 +0100 Subject: [PATCH 09/12] fixed time out --- .../org-unit-item-metadata-list-element.component.spec.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/app/entity-groups/research-entities/metadata-representations/org-unit/org-unit-item-metadata-list-element.component.spec.ts b/src/app/entity-groups/research-entities/metadata-representations/org-unit/org-unit-item-metadata-list-element.component.spec.ts index 0354469372..7d27b605ec 100644 --- a/src/app/entity-groups/research-entities/metadata-representations/org-unit/org-unit-item-metadata-list-element.component.spec.ts +++ b/src/app/entity-groups/research-entities/metadata-representations/org-unit/org-unit-item-metadata-list-element.component.spec.ts @@ -13,7 +13,7 @@ const mockItem = Object.assign(new Item(), { metadata: { 'dc.description': [{ va const virtMD = Object.assign(new MetadataValue(), { value: organisation }); const mockItemMetadataRepresentation = Object.assign(new ItemMetadataRepresentation(virtMD), mockItem); -fdescribe('OrgUnitItemMetadataListElementComponent', () => { +describe('OrgUnitItemMetadataListElementComponent', () => { let comp: OrgUnitItemMetadataListElementComponent; let fixture: ComponentFixture; @@ -23,7 +23,7 @@ fdescribe('OrgUnitItemMetadataListElementComponent', () => { declarations: [OrgUnitItemMetadataListElementComponent], schemas: [NO_ERRORS_SCHEMA] }).overrideComponent(OrgUnitItemMetadataListElementComponent, { - // set: { changeDetection: ChangeDetectionStrategy.Default } + set: { changeDetection: ChangeDetectionStrategy.Default } }).compileComponents(); })); From 3d1e93f5ef2d38e07c2929891ec33332086560bb Mon Sep 17 00:00:00 2001 From: lotte Date: Thu, 27 Feb 2020 16:53:47 +0100 Subject: [PATCH 10/12] feedback --- resources/i18n/en.json5 | 9 +++- .../browse-by-metadata-page.component.spec.ts | 1 - .../browse-by-dso-breadcrumb.resolver.ts | 41 +++++++++++++++++++ src/app/+browse-by/browse-by-guard.ts | 10 ++--- .../browse-by-i18n-breadcrumb.resolver.ts | 28 +++++++++++++ .../+browse-by/browse-by-routing.module.ts | 21 +++++++++- .../edit-collection-page.routing.module.ts | 9 ++-- .../edit-community-page.routing.module.ts | 7 ++-- .../edit-item-page.routing.module.ts | 12 +++--- src/app/+item-page/item-page.resolver.ts | 2 +- src/app/app-routing.module.ts | 3 +- .../breadcrumb/breadcrumb-config.model.ts | 14 +++++++ .../breadcrumb/breadcrumb.model.ts | 9 ++++ src/app/breadcrumbs/breadcrumbs.component.ts | 10 +++-- .../collection-breadcrumb.resolver.ts | 14 +++++++ .../community-breadcrumb.resolver.ts | 12 ++++++ .../dso-breadcrumb.resolver.spec.ts | 3 +- .../breadcrumbs/dso-breadcrumb.resolver.ts | 13 ++++-- .../breadcrumbs/i18n-breadcrumb.resolver.ts | 4 +- .../breadcrumbs/item-breadcrumb.resolver.ts | 17 ++++++++ .../core/shared/child-hal-resource.model.ts | 7 ++++ 21 files changed, 213 insertions(+), 33 deletions(-) create mode 100644 src/app/+browse-by/browse-by-dso-breadcrumb.resolver.ts create mode 100644 src/app/+browse-by/browse-by-i18n-breadcrumb.resolver.ts diff --git a/resources/i18n/en.json5 b/resources/i18n/en.json5 index 044cf216b8..cfa202fe4a 100644 --- a/resources/i18n/en.json5 +++ b/resources/i18n/en.json5 @@ -196,6 +196,14 @@ "browse.metadata.title": "Title", + "browse.metadata.author.breadcrumbs": "Browse by Author", + + "browse.metadata.dateissued.breadcrumbs": "Browse by Date", + + "browse.metadata.subject.breadcrumbs": "Browse by Subject", + + "browse.metadata.title.breadcrumbs": "Browse by Title", + "browse.startsWith.choose_start": "(Choose start)", "browse.startsWith.choose_year": "(Choose year)", @@ -237,7 +245,6 @@ "browse.title": "Browsing {{ collection }} by {{ field }} {{ value }}", - "chips.remove": "Remove chip", diff --git a/src/app/+browse-by/+browse-by-metadata-page/browse-by-metadata-page.component.spec.ts b/src/app/+browse-by/+browse-by-metadata-page/browse-by-metadata-page.component.spec.ts index 21e7e9ad99..601e7153e9 100644 --- a/src/app/+browse-by/+browse-by-metadata-page/browse-by-metadata-page.component.spec.ts +++ b/src/app/+browse-by/+browse-by-metadata-page/browse-by-metadata-page.component.spec.ts @@ -20,7 +20,6 @@ import { Item } from '../../core/shared/item.model'; import { DSpaceObjectDataService } from '../../core/data/dspace-object-data.service'; import { Community } from '../../core/shared/community.model'; import { MockRouter } from '../../shared/mocks/mock-router'; -import { ResourceType } from '../../core/shared/resource-type'; import { createSuccessfulRemoteDataObject$ } from '../../shared/testing/utils'; import { BrowseEntry } from '../../core/shared/browse-entry.model'; import { VarDirective } from '../../shared/utils/var.directive'; diff --git a/src/app/+browse-by/browse-by-dso-breadcrumb.resolver.ts b/src/app/+browse-by/browse-by-dso-breadcrumb.resolver.ts new file mode 100644 index 0000000000..5759e28754 --- /dev/null +++ b/src/app/+browse-by/browse-by-dso-breadcrumb.resolver.ts @@ -0,0 +1,41 @@ +import { Injectable } from '@angular/core'; +import { Community } from '../core/shared/community.model'; +import { DSpaceObjectDataService } from '../core/data/dspace-object-data.service'; +import { DSOBreadcrumbsService } from '../core/breadcrumbs/dso-breadcrumbs.service'; +import { Collection } from '../core/shared/collection.model'; +import { ActivatedRouteSnapshot, RouterStateSnapshot } from '@angular/router'; +import { BreadcrumbConfig } from '../breadcrumbs/breadcrumb/breadcrumb-config.model'; +import { Observable } from 'rxjs'; +import { getRemoteDataPayload, getSucceededRemoteData } from '../core/shared/operators'; +import { map } from 'rxjs/operators'; +import { hasValue } from '../shared/empty.util'; +import { getDSOPath } from '../app-routing.module'; + +/** + * The class that resolves the BreadcrumbConfig object for a DSpaceObject on a browse by page + */ +@Injectable() +export class BrowseByDSOBreadcrumbResolver { + constructor(protected breadcrumbService: DSOBreadcrumbsService, protected dataService: DSpaceObjectDataService) { + } + + /** + * Method for resolving a breadcrumb config object + * @param {ActivatedRouteSnapshot} route The current ActivatedRouteSnapshot + * @param {RouterStateSnapshot} state The current RouterStateSnapshot + * @returns BreadcrumbConfig object + */ + resolve(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): Observable> { + const uuid = route.queryParams.scope; + if (hasValue(uuid)) { + return this.dataService.findById(uuid).pipe( + getSucceededRemoteData(), + getRemoteDataPayload(), + map((object: Community | Collection) => { + return { provider: this.breadcrumbService, key: object, url: getDSOPath(object) }; + }) + ); + } + return undefined; + } +} diff --git a/src/app/+browse-by/browse-by-guard.ts b/src/app/+browse-by/browse-by-guard.ts index 3813f7e656..9659a8c7b4 100644 --- a/src/app/+browse-by/browse-by-guard.ts +++ b/src/app/+browse-by/browse-by-guard.ts @@ -37,24 +37,24 @@ export class BrowseByGuard implements CanActivate { return dsoAndMetadata$.pipe( map((dsoRD) => { const name = dsoRD.payload.name; - route.data = this.createData(title, id, metadataField, name, metadataTranslated, value); + route.data = this.createData(title, id, metadataField, name, metadataTranslated, value, route); return true; }) ); } else { - route.data = this.createData(title, id, metadataField, '', metadataTranslated, value); + route.data = this.createData(title, id, metadataField, '', metadataTranslated, value, route); return observableOf(true); } } - private createData(title, id, metadataField, collection, field, value) { - return { + private createData(title, id, metadataField, collection, field, value, route) { + return Object.assign({}, route.data, { title: title, id: id, metadataField: metadataField, collection: collection, field: field, value: hasValue(value) ? `"${value}"` : '' - } + }); } } diff --git a/src/app/+browse-by/browse-by-i18n-breadcrumb.resolver.ts b/src/app/+browse-by/browse-by-i18n-breadcrumb.resolver.ts new file mode 100644 index 0000000000..c173bd414e --- /dev/null +++ b/src/app/+browse-by/browse-by-i18n-breadcrumb.resolver.ts @@ -0,0 +1,28 @@ +import { Injectable } from '@angular/core'; +import { ActivatedRouteSnapshot, Resolve, RouterStateSnapshot } from '@angular/router'; +import { I18nBreadcrumbsService } from '../core/breadcrumbs/i18n-breadcrumbs.service'; +import { BreadcrumbConfig } from '../breadcrumbs/breadcrumb/breadcrumb-config.model'; +import { I18nBreadcrumbResolver } from '../core/breadcrumbs/i18n-breadcrumb.resolver'; + +/** + * This class resolves a BreadcrumbConfig object with an i18n key string for a route + * It adds the metadata field of the current browse-by page + */ +@Injectable() +export class BrowseByI18nBreadcrumbResolver extends I18nBreadcrumbResolver { + constructor(protected breadcrumbService: I18nBreadcrumbsService) { + super(breadcrumbService); + } + + /** + * Method for resolving a browse-by i18n breadcrumb configuration object + * @param {ActivatedRouteSnapshot} route The current ActivatedRouteSnapshot + * @param {RouterStateSnapshot} state The current RouterStateSnapshot + * @returns BreadcrumbConfig object for a browse-by page + */ + resolve(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): BreadcrumbConfig { + const extendedBreadcrumbKey = route.data.breadcrumbKey + '.' + route.params.id; + route.data = Object.assign({}, route.data, { breadcrumbKey: extendedBreadcrumbKey }); + return super.resolve(route, state); + } +} diff --git a/src/app/+browse-by/browse-by-routing.module.ts b/src/app/+browse-by/browse-by-routing.module.ts index e549c0f4e6..a686e7007e 100644 --- a/src/app/+browse-by/browse-by-routing.module.ts +++ b/src/app/+browse-by/browse-by-routing.module.ts @@ -2,12 +2,29 @@ import { RouterModule } from '@angular/router'; import { NgModule } from '@angular/core'; import { BrowseByGuard } from './browse-by-guard'; import { BrowseBySwitcherComponent } from './+browse-by-switcher/browse-by-switcher.component'; +import { BrowseByDSOBreadcrumbResolver } from './browse-by-dso-breadcrumb.resolver'; +import { BrowseByI18nBreadcrumbResolver } from './browse-by-i18n-breadcrumb.resolver'; @NgModule({ imports: [ RouterModule.forChild([ - { path: ':id', component: BrowseBySwitcherComponent, canActivate: [BrowseByGuard], data: { title: 'browse.title' } } - ]) + { + path: '', + resolve: { breadcrumb: BrowseByDSOBreadcrumbResolver }, + children: [ + { + path: ':id', + component: BrowseBySwitcherComponent, + canActivate: [BrowseByGuard], + resolve: { breadcrumb: BrowseByI18nBreadcrumbResolver }, + data: { title: 'browse.title', breadcrumbKey: 'browse.metadata' } + } + ] + }]) + ], + providers: [ + BrowseByI18nBreadcrumbResolver, + BrowseByDSOBreadcrumbResolver ] }) export class BrowseByRoutingModule { diff --git a/src/app/+collection-page/edit-collection-page/edit-collection-page.routing.module.ts b/src/app/+collection-page/edit-collection-page/edit-collection-page.routing.module.ts index e3d5749472..0569de9cd9 100644 --- a/src/app/+collection-page/edit-collection-page/edit-collection-page.routing.module.ts +++ b/src/app/+collection-page/edit-collection-page/edit-collection-page.routing.module.ts @@ -31,23 +31,24 @@ import { I18nBreadcrumbResolver } from '../../core/breadcrumbs/i18n-breadcrumb.r component: CollectionMetadataComponent, data: { title: 'collection.edit.tabs.metadata.title', - hideReturnButton: true + hideReturnButton: true, + showBreadcrumbs: true } }, { path: 'roles', component: CollectionRolesComponent, - data: { title: 'collection.edit.tabs.roles.title' } + data: { title: 'collection.edit.tabs.roles.title', showBreadcrumbs: true } }, { path: 'source', component: CollectionSourceComponent, - data: { title: 'collection.edit.tabs.source.title' } + data: { title: 'collection.edit.tabs.source.title', showBreadcrumbs: true } }, { path: 'curate', component: CollectionCurateComponent, - data: { title: 'collection.edit.tabs.curate.title' } + data: { title: 'collection.edit.tabs.curate.title', showBreadcrumbs: true } } ] } diff --git a/src/app/+community-page/edit-community-page/edit-community-page.routing.module.ts b/src/app/+community-page/edit-community-page/edit-community-page.routing.module.ts index f0a7813bac..3197e00829 100644 --- a/src/app/+community-page/edit-community-page/edit-community-page.routing.module.ts +++ b/src/app/+community-page/edit-community-page/edit-community-page.routing.module.ts @@ -31,18 +31,19 @@ import { I18nBreadcrumbResolver } from '../../core/breadcrumbs/i18n-breadcrumb.r component: CommunityMetadataComponent, data: { title: 'community.edit.tabs.metadata.title', - hideReturnButton: true + hideReturnButton: true, + showBreadcrumbs: true } }, { path: 'roles', component: CommunityRolesComponent, - data: { title: 'community.edit.tabs.roles.title' } + data: { title: 'community.edit.tabs.roles.title', showBreadcrumbs: true } }, { path: 'curate', component: CommunityCurateComponent, - data: { title: 'community.edit.tabs.curate.title' } + data: { title: 'community.edit.tabs.curate.title', showBreadcrumbs: true } } ] } diff --git a/src/app/+item-page/edit-item-page/edit-item-page.routing.module.ts b/src/app/+item-page/edit-item-page/edit-item-page.routing.module.ts index af78eeea6f..da667847f7 100644 --- a/src/app/+item-page/edit-item-page/edit-item-page.routing.module.ts +++ b/src/app/+item-page/edit-item-page/edit-item-page.routing.module.ts @@ -47,34 +47,34 @@ const ITEM_EDIT_MOVE_PATH = 'move'; { path: 'status', component: ItemStatusComponent, - data: { title: 'item.edit.tabs.status.title' } + data: { title: 'item.edit.tabs.status.title', showBreadcrumbs: true } }, { path: 'bitstreams', component: ItemBitstreamsComponent, - data: { title: 'item.edit.tabs.bitstreams.title' } + data: { title: 'item.edit.tabs.bitstreams.title', showBreadcrumbs: true } }, { path: 'metadata', component: ItemMetadataComponent, - data: { title: 'item.edit.tabs.metadata.title' } + data: { title: 'item.edit.tabs.metadata.title', showBreadcrumbs: true } }, { path: 'relationships', component: ItemRelationshipsComponent, - data: { title: 'item.edit.tabs.relationships.title' } + data: { title: 'item.edit.tabs.relationships.title', showBreadcrumbs: true } }, { path: 'view', /* TODO - change when view page exists */ component: ItemBitstreamsComponent, - data: { title: 'item.edit.tabs.view.title' } + data: { title: 'item.edit.tabs.view.title', showBreadcrumbs: true } }, { path: 'curate', /* TODO - change when curate page exists */ component: ItemBitstreamsComponent, - data: { title: 'item.edit.tabs.curate.title' } + data: { title: 'item.edit.tabs.curate.title', showBreadcrumbs: true } } ] }, diff --git a/src/app/+item-page/item-page.resolver.ts b/src/app/+item-page/item-page.resolver.ts index 2645f0228c..0f73dc6170 100644 --- a/src/app/+item-page/item-page.resolver.ts +++ b/src/app/+item-page/item-page.resolver.ts @@ -27,7 +27,7 @@ export class ItemPageResolver implements Resolve> { return this.itemService.findById(route.params.id, followLink('owningCollection'), followLink('bundles'), - followLink('relationships') + followLink('relationships'), ).pipe( find((RD) => hasValue(RD.error) || RD.hasSucceeded), ); diff --git a/src/app/app-routing.module.ts b/src/app/app-routing.module.ts index ede1879894..4cf5efae41 100644 --- a/src/app/app-routing.module.ts +++ b/src/app/app-routing.module.ts @@ -11,6 +11,7 @@ import { Collection } from './core/shared/collection.model'; import { Item } from './core/shared/item.model'; import { getItemPageRoute } from './+item-page/item-page-routing.module'; import { getCollectionPageRoute } from './+collection-page/collection-page-routing.module'; +import { BrowseByDSOBreadcrumbResolver } from './+browse-by/browse-by-dso-breadcrumb.resolver'; const ITEM_MODULE_PATH = 'items'; @@ -60,7 +61,7 @@ export function getDSOPath(dso: DSpaceObject): string { { path: ITEM_MODULE_PATH, loadChildren: './+item-page/item-page.module#ItemPageModule' }, { path: 'mydspace', loadChildren: './+my-dspace-page/my-dspace-page.module#MyDSpacePageModule', canActivate: [AuthenticatedGuard] }, { path: 'search', loadChildren: './+search-page/search-page.module#SearchPageModule' }, - { path: 'browse', loadChildren: './+browse-by/browse-by.module#BrowseByModule' }, + { path: 'browse', loadChildren: './+browse-by/browse-by.module#BrowseByModule'}, { path: ADMIN_MODULE_PATH, loadChildren: './+admin/admin.module#AdminModule', canActivate: [AuthenticatedGuard] }, { path: 'login', loadChildren: './+login-page/login-page.module#LoginPageModule' }, { path: 'logout', loadChildren: './+logout-page/logout-page.module#LogoutPageModule' }, diff --git a/src/app/breadcrumbs/breadcrumb/breadcrumb-config.model.ts b/src/app/breadcrumbs/breadcrumb/breadcrumb-config.model.ts index 17ec96e2bd..0ff8fc5033 100644 --- a/src/app/breadcrumbs/breadcrumb/breadcrumb-config.model.ts +++ b/src/app/breadcrumbs/breadcrumb/breadcrumb-config.model.ts @@ -1,7 +1,21 @@ import { BreadcrumbsService } from '../../core/breadcrumbs/breadcrumbs.service'; +/** + * Interface for breadcrumb configuration objects + */ export interface BreadcrumbConfig { + /** + * The service used to calculate the breadcrumb object + */ provider: BreadcrumbsService; + + /** + * The key that is used to calculate the breadcrumb display value + */ key: T; + + /** + * The url of the breadcrumb + */ url?: string; } diff --git a/src/app/breadcrumbs/breadcrumb/breadcrumb.model.ts b/src/app/breadcrumbs/breadcrumb/breadcrumb.model.ts index 99cf66533a..c6ab8491b4 100644 --- a/src/app/breadcrumbs/breadcrumb/breadcrumb.model.ts +++ b/src/app/breadcrumbs/breadcrumb/breadcrumb.model.ts @@ -1,6 +1,15 @@ +/** + * Class representing a single breadcrumb + */ export class Breadcrumb { constructor( + /** + * The display value of the breadcrumb + */ public text: string, + /** + * The optional url of the breadcrumb + */ public url?: string) { } } diff --git a/src/app/breadcrumbs/breadcrumbs.component.ts b/src/app/breadcrumbs/breadcrumbs.component.ts index 861fda844b..2bba3c76b6 100644 --- a/src/app/breadcrumbs/breadcrumbs.component.ts +++ b/src/app/breadcrumbs/breadcrumbs.component.ts @@ -1,7 +1,7 @@ import { Component, OnDestroy, OnInit } from '@angular/core'; import { ActivatedRoute, NavigationEnd, Router } from '@angular/router'; import { Breadcrumb } from './breadcrumb/breadcrumb.model'; -import { hasNoValue, hasValue, isNotUndefined } from '../shared/empty.util'; +import { hasNoValue, hasValue, isNotUndefined, isUndefined } from '../shared/empty.util'; import { filter, map, switchMap, tap } from 'rxjs/operators'; import { combineLatest, Observable, Subscription, of as observableOf } from 'rxjs'; @@ -58,8 +58,12 @@ export class BreadcrumbsComponent implements OnInit, OnDestroy { const routeConfig = route.snapshot.routeConfig; const last: boolean = hasNoValue(route.firstChild); - if (last && isNotUndefined(data.showBreadcrumbs)) { - this.showBreadcrumbs = data.showBreadcrumbs; + if (last) { + if (hasValue(data.showBreadcrumbs)) { + this.showBreadcrumbs = data.showBreadcrumbs; + } else if (isUndefined(data.breadcrumb)) { + this.showBreadcrumbs = false; + } } if ( diff --git a/src/app/core/breadcrumbs/collection-breadcrumb.resolver.ts b/src/app/core/breadcrumbs/collection-breadcrumb.resolver.ts index c662ead129..d9df7cd767 100644 --- a/src/app/core/breadcrumbs/collection-breadcrumb.resolver.ts +++ b/src/app/core/breadcrumbs/collection-breadcrumb.resolver.ts @@ -3,6 +3,7 @@ import { DSOBreadcrumbsService } from './dso-breadcrumbs.service'; import { DSOBreadcrumbResolver } from './dso-breadcrumb.resolver'; import { Collection } from '../shared/collection.model'; import { CollectionDataService } from '../data/collection-data.service'; +import { followLink, FollowLinkConfig } from '../../shared/utils/follow-link-config.model'; /** * The class that resolves the BreadcrumbConfig object for a Collection @@ -12,4 +13,17 @@ export class CollectionBreadcrumbResolver extends DSOBreadcrumbResolver> { + return [ + followLink('parentCommunity', undefined, + followLink('parentCommunity') + ) + ]; + } } diff --git a/src/app/core/breadcrumbs/community-breadcrumb.resolver.ts b/src/app/core/breadcrumbs/community-breadcrumb.resolver.ts index 1e4959f9e5..d1f21455f2 100644 --- a/src/app/core/breadcrumbs/community-breadcrumb.resolver.ts +++ b/src/app/core/breadcrumbs/community-breadcrumb.resolver.ts @@ -3,6 +3,7 @@ import { DSOBreadcrumbsService } from './dso-breadcrumbs.service'; import { DSOBreadcrumbResolver } from './dso-breadcrumb.resolver'; import { CommunityDataService } from '../data/community-data.service'; import { Community } from '../shared/community.model'; +import { followLink, FollowLinkConfig } from '../../shared/utils/follow-link-config.model'; /** * The class that resolves the BreadcrumbConfig object for a Community @@ -12,4 +13,15 @@ export class CommunityBreadcrumbResolver extends DSOBreadcrumbResolver> { + return [ + followLink('parentCommunity') + ]; + } } diff --git a/src/app/core/breadcrumbs/dso-breadcrumb.resolver.spec.ts b/src/app/core/breadcrumbs/dso-breadcrumb.resolver.spec.ts index 774fcd04d5..2a0005f548 100644 --- a/src/app/core/breadcrumbs/dso-breadcrumb.resolver.spec.ts +++ b/src/app/core/breadcrumbs/dso-breadcrumb.resolver.spec.ts @@ -2,6 +2,7 @@ import { DSOBreadcrumbResolver } from './dso-breadcrumb.resolver'; import { Collection } from '../shared/collection.model'; import { createSuccessfulRemoteDataObject$ } from '../../shared/testing/utils'; import { getTestScheduler } from 'jasmine-marbles'; +import { CollectionBreadcrumbResolver } from './collection-breadcrumb.resolver'; describe('DSOBreadcrumbResolver', () => { describe('resolve', () => { @@ -22,7 +23,7 @@ describe('DSOBreadcrumbResolver', () => { collectionService = { findById: (id: string) => createSuccessfulRemoteDataObject$(testCollection) }; - resolver = new DSOBreadcrumbResolver(dsoBreadcrumbService, collectionService); + resolver = new CollectionBreadcrumbResolver(dsoBreadcrumbService, collectionService); }); it('should resolve a breadcrumb config for the correct DSO', () => { diff --git a/src/app/core/breadcrumbs/dso-breadcrumb.resolver.ts b/src/app/core/breadcrumbs/dso-breadcrumb.resolver.ts index 78ba349a5c..80e68a16f5 100644 --- a/src/app/core/breadcrumbs/dso-breadcrumb.resolver.ts +++ b/src/app/core/breadcrumbs/dso-breadcrumb.resolver.ts @@ -8,12 +8,13 @@ import { map } from 'rxjs/operators'; import { Observable } from 'rxjs'; import { DSpaceObject } from '../shared/dspace-object.model'; import { ChildHALResource } from '../shared/child-hal-resource.model'; +import { FollowLinkConfig } from '../../shared/utils/follow-link-config.model'; /** * The class that resolves the BreadcrumbConfig object for a DSpaceObject */ @Injectable() -export class DSOBreadcrumbResolver implements Resolve> { +export abstract class DSOBreadcrumbResolver implements Resolve> { constructor(protected breadcrumbService: DSOBreadcrumbsService, protected dataService: DataService) { } @@ -25,7 +26,7 @@ export class DSOBreadcrumbResolver im */ resolve(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): Observable> { const uuid = route.params.id; - return this.dataService.findById(uuid).pipe( + return this.dataService.findById(uuid, ...this.followLinks).pipe( getSucceededRemoteData(), getRemoteDataPayload(), map((object: T) => { @@ -34,6 +35,12 @@ export class DSOBreadcrumbResolver im return { provider: this.breadcrumbService, key: object, url: url }; }) ); - } + + /** + * Method that returns the follow links to already resolve + * The self links defined in this list are expected to be requested somewhere in the near future + * Requesting them as embeds will limit the number of requests + */ + abstract get followLinks(): Array>; } diff --git a/src/app/core/breadcrumbs/i18n-breadcrumb.resolver.ts b/src/app/core/breadcrumbs/i18n-breadcrumb.resolver.ts index 0978648ba3..de7d061a3f 100644 --- a/src/app/core/breadcrumbs/i18n-breadcrumb.resolver.ts +++ b/src/app/core/breadcrumbs/i18n-breadcrumb.resolver.ts @@ -9,11 +9,11 @@ import { hasNoValue } from '../../shared/empty.util'; */ @Injectable() export class I18nBreadcrumbResolver implements Resolve> { - constructor(private breadcrumbService: I18nBreadcrumbsService) { + constructor(protected breadcrumbService: I18nBreadcrumbsService) { } /** - * Method for resolving a site object + * Method for resolving an I18n breadcrumb configuration object * @param {ActivatedRouteSnapshot} route The current ActivatedRouteSnapshot * @param {RouterStateSnapshot} state The current RouterStateSnapshot * @returns BreadcrumbConfig object diff --git a/src/app/core/breadcrumbs/item-breadcrumb.resolver.ts b/src/app/core/breadcrumbs/item-breadcrumb.resolver.ts index c447f7de2a..8390c0e001 100644 --- a/src/app/core/breadcrumbs/item-breadcrumb.resolver.ts +++ b/src/app/core/breadcrumbs/item-breadcrumb.resolver.ts @@ -3,6 +3,7 @@ import { DSOBreadcrumbsService } from './dso-breadcrumbs.service'; import { ItemDataService } from '../data/item-data.service'; import { Item } from '../shared/item.model'; import { DSOBreadcrumbResolver } from './dso-breadcrumb.resolver'; +import { followLink, FollowLinkConfig } from '../../shared/utils/follow-link-config.model'; /** * The class that resolves the BreadcrumbConfig object for an Item @@ -12,4 +13,20 @@ export class ItemBreadcrumbResolver extends DSOBreadcrumbResolver { constructor(protected breadcrumbService: DSOBreadcrumbsService, protected dataService: ItemDataService) { super(breadcrumbService, dataService); } + + /** + * Method that returns the follow links to already resolve + * The self links defined in this list are expected to be requested somewhere in the near future + * Requesting them as embeds will limit the number of requests + */ + get followLinks(): Array> { + return [ + followLink('owningCollection', undefined, + followLink('parentCommunity', undefined, + followLink('parentCommunity')) + ), + followLink('bundles'), + followLink('relationships') + ]; + } } diff --git a/src/app/core/shared/child-hal-resource.model.ts b/src/app/core/shared/child-hal-resource.model.ts index b7682e2631..ee022942bb 100644 --- a/src/app/core/shared/child-hal-resource.model.ts +++ b/src/app/core/shared/child-hal-resource.model.ts @@ -1,5 +1,12 @@ import { HALResource } from './hal-resource.model'; +/** + * Interface for HALResources with a parent object link + */ export interface ChildHALResource extends HALResource { + + /** + * Returns the key of the parent link + */ getParentLinkKey(): keyof this['_links']; } From aa5383ca5a7a8097fe00dc80c4bfcaedf32bbb31 Mon Sep 17 00:00:00 2001 From: lotte Date: Thu, 27 Feb 2020 16:56:51 +0100 Subject: [PATCH 11/12] remove unnecessary async that causes test timeout --- .../org-unit-item-metadata-list-element.component.spec.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/app/entity-groups/research-entities/metadata-representations/org-unit/org-unit-item-metadata-list-element.component.spec.ts b/src/app/entity-groups/research-entities/metadata-representations/org-unit/org-unit-item-metadata-list-element.component.spec.ts index 7d27b605ec..290fac7522 100644 --- a/src/app/entity-groups/research-entities/metadata-representations/org-unit/org-unit-item-metadata-list-element.component.spec.ts +++ b/src/app/entity-groups/research-entities/metadata-representations/org-unit/org-unit-item-metadata-list-element.component.spec.ts @@ -13,7 +13,7 @@ const mockItem = Object.assign(new Item(), { metadata: { 'dc.description': [{ va const virtMD = Object.assign(new MetadataValue(), { value: organisation }); const mockItemMetadataRepresentation = Object.assign(new ItemMetadataRepresentation(virtMD), mockItem); -describe('OrgUnitItemMetadataListElementComponent', () => { +fdescribe('OrgUnitItemMetadataListElementComponent', () => { let comp: OrgUnitItemMetadataListElementComponent; let fixture: ComponentFixture; @@ -27,12 +27,12 @@ describe('OrgUnitItemMetadataListElementComponent', () => { }).compileComponents(); })); - beforeEach(async(() => { + beforeEach(() => { fixture = TestBed.createComponent(OrgUnitItemMetadataListElementComponent); comp = fixture.componentInstance; comp.metadataRepresentation = mockItemMetadataRepresentation; fixture.detectChanges(); - })); + }); it('should show the name of the organisation as a link', () => { const linkText = fixture.debugElement.query(By.css('a')).nativeElement.textContent; From 8b6b478df9a8fdef570c6b0d7b04e01a339d58ee Mon Sep 17 00:00:00 2001 From: lotte Date: Thu, 27 Feb 2020 16:59:34 +0100 Subject: [PATCH 12/12] fixed other async --- .../org-unit-item-metadata-list-element.component.spec.ts | 2 +- .../person-item-metadata-list-element.component.spec.ts | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/app/entity-groups/research-entities/metadata-representations/org-unit/org-unit-item-metadata-list-element.component.spec.ts b/src/app/entity-groups/research-entities/metadata-representations/org-unit/org-unit-item-metadata-list-element.component.spec.ts index 290fac7522..2d28821738 100644 --- a/src/app/entity-groups/research-entities/metadata-representations/org-unit/org-unit-item-metadata-list-element.component.spec.ts +++ b/src/app/entity-groups/research-entities/metadata-representations/org-unit/org-unit-item-metadata-list-element.component.spec.ts @@ -13,7 +13,7 @@ const mockItem = Object.assign(new Item(), { metadata: { 'dc.description': [{ va const virtMD = Object.assign(new MetadataValue(), { value: organisation }); const mockItemMetadataRepresentation = Object.assign(new ItemMetadataRepresentation(virtMD), mockItem); -fdescribe('OrgUnitItemMetadataListElementComponent', () => { +describe('OrgUnitItemMetadataListElementComponent', () => { let comp: OrgUnitItemMetadataListElementComponent; let fixture: ComponentFixture; diff --git a/src/app/entity-groups/research-entities/metadata-representations/person/person-item-metadata-list-element.component.spec.ts b/src/app/entity-groups/research-entities/metadata-representations/person/person-item-metadata-list-element.component.spec.ts index 1081e45884..97087728f8 100644 --- a/src/app/entity-groups/research-entities/metadata-representations/person/person-item-metadata-list-element.component.spec.ts +++ b/src/app/entity-groups/research-entities/metadata-representations/person/person-item-metadata-list-element.component.spec.ts @@ -29,12 +29,12 @@ describe('PersonItemMetadataListElementComponent', () => { }).compileComponents(); })); - beforeEach(async(() => { + beforeEach(() => { fixture = TestBed.createComponent(PersonItemMetadataListElementComponent); comp = fixture.componentInstance; comp.metadataRepresentation = mockItemMetadataRepresentation; fixture.detectChanges(); - })); + }); it('should show the person\'s name as a link', () => { const linkText = fixture.debugElement.query(By.css('a')).nativeElement.textContent;