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'; + } }