breadcrumbs for DSOs

This commit is contained in:
lotte
2020-02-21 17:06:50 +01:00
committed by Art Lowel
parent 4ea264dd7f
commit 725f20a9d0
18 changed files with 197 additions and 39 deletions

View File

@@ -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 {

View File

@@ -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 {

View File

@@ -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' },

View File

@@ -1,7 +1,7 @@
import { BreadcrumbsService } from '../../core/breadcrumbs/breadcrumbs.service';
export interface BreadcrumbConfig {
provider: BreadcrumbsService;
key: string;
export interface BreadcrumbConfig<T> {
provider: BreadcrumbsService<T>;
key: T;
url?: string;
}

View File

@@ -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<Breadcrumb[]> {
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 {

View File

@@ -1,6 +1,6 @@
import { Breadcrumb } from '../../breadcrumbs/breadcrumb/breadcrumb.model';
import { Observable } from 'rxjs';
export interface BreadcrumbsService {
getBreadcrumbs(key: string, url: string): Observable<Breadcrumb[]>;
export interface BreadcrumbsService<T> {
getBreadcrumbs(key: T, url: string): Observable<Breadcrumb[]>;
}

View File

@@ -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<Collection> {
constructor(protected breadcrumbService: DSOBreadcrumbsService, protected dataService: CollectionDataService) {
super(breadcrumbService, dataService);
}
}

View File

@@ -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<Community> {
constructor(protected breadcrumbService: DSOBreadcrumbsService, protected dataService: CommunityDataService) {
super(breadcrumbService, dataService);
}
}

View File

@@ -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<BreadcrumbConfig> {
constructor(private breadcrumbService: DSOBreadcrumbsService) {
export class DSOBreadcrumbResolver<T extends ChildHALResource & DSpaceObject> implements Resolve<BreadcrumbConfig<T>> {
constructor(protected breadcrumbService: DSOBreadcrumbsService, protected dataService: DataService<T>) {
}
/**
@@ -17,10 +23,17 @@ export class DSOBreadcrumbResolver implements Resolve<BreadcrumbConfig> {
* @param {RouterStateSnapshot} state The current RouterStateSnapshot
* @returns BreadcrumbConfig object
*/
resolve(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): BreadcrumbConfig {
resolve(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): Observable<BreadcrumbConfig<T>> {
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 };
})
);
}
}

View File

@@ -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<Breadcrumb[]> {
return undefined;
@Injectable()
export class DSOBreadcrumbsService implements BreadcrumbsService<ChildHALResource & DSpaceObject> {
constructor(private linkService: LinkService) {
}
getBreadcrumbs(key: ChildHALResource & DSpaceObject, url: string): Observable<Breadcrumb[]> {
const crumb = new Breadcrumb(key.name, url);
const propertyName = key.getParentLinkKey();
return this.linkService.resolveLink(key, followLink(propertyName))[propertyName].pipe(
filter((childRD: RemoteData<ChildHALResource & DSpaceObject>) => childRD.isSuccessful === true && childRD.requestPending === false && childRD.responsePending === false),
switchMap((childRD: RemoteData<ChildHALResource & DSpaceObject>) => {
if (hasValue(childRD.payload)) {
const child = childRD.payload;
return this.getBreadcrumbs(child, getDSOPath(child))
}
return observableOf([]);
}),
map((breadcrumbs: Breadcrumb[]) => [...breadcrumbs, crumb])
);
}
}

View File

@@ -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<BreadcrumbConfig> {
export class I18nBreadcrumbResolver implements Resolve<BreadcrumbConfig<string>> {
constructor(private breadcrumbService: I18nBreadcrumbsService) {
}
@@ -18,7 +18,7 @@ export class I18nBreadcrumbResolver implements Resolve<BreadcrumbConfig> {
* @param {RouterStateSnapshot} state The current RouterStateSnapshot
* @returns BreadcrumbConfig object
*/
resolve(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): BreadcrumbConfig {
resolve(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): BreadcrumbConfig<string> {
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')

View File

@@ -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<string> {
getBreadcrumbs(key: string, url: string): Observable<Breadcrumb[]> {
return observableOf([new Breadcrumb(BREADCRUMB_MESSAGE_PREFIX + key, url)]);
}

View File

@@ -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<Item> {
constructor(protected breadcrumbService: DSOBreadcrumbsService, protected dataService: ItemDataService) {
super(breadcrumbService, dataService);
}
}

View File

@@ -13,9 +13,9 @@ export enum RemoteDataState {
*/
export class RemoteData<T> {
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
) {

View File

@@ -0,0 +1,5 @@
import { HALResource } from './hal-resource.model';
export interface ChildHALResource extends HALResource {
getParentLinkKey(): keyof this['_links'];
}

View File

@@ -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<RemoteData<PaginatedList<ResourcePolicy>>>;
/**
* 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<RemoteData<Community>>;
/**
* 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';
}
}

View File

@@ -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<RemoteData<PaginatedList<Community>>>;
/**
* 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<RemoteData<Community>>;
/**
* 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';
}
}

View File

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